@mostajs/auth 2.4.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/credentials-helpers.d.ts +3 -0
- package/dist/lib/credentials-helpers.js +23 -0
- package/dist/lib/credentials-provider.js +1 -13
- package/dist/lib/credentials-verify.d.ts +36 -0
- package/dist/lib/credentials-verify.js +96 -0
- package/dist/lib/remote-credentials-provider.d.ts +55 -0
- package/dist/lib/remote-credentials-provider.js +115 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// @mostajs/auth — Helpers partagés entre credentials providers (local, remote, server)
|
|
2
|
+
//
|
|
3
|
+
// Centralise les valeurs par défaut pour : check de status, résolution
|
|
4
|
+
// de role, résolution de display name. Utilisé par :
|
|
5
|
+
// - credentials-provider.ts (NextAuth provider local, ORM direct)
|
|
6
|
+
// - credentials-verify.ts (handler serveur côté Octonet)
|
|
7
|
+
// - remote-credentials-provider.ts (NextAuth provider gateway, ne fait
|
|
8
|
+
// pas de bcrypt — délègue au handler)
|
|
9
|
+
//
|
|
10
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
11
|
+
export function defaultStatusCheck(user) {
|
|
12
|
+
return user.status !== 'disabled' && user.status !== 'locked';
|
|
13
|
+
}
|
|
14
|
+
export function defaultRoleResolver(user, fallback) {
|
|
15
|
+
if (user.roles?.length > 0) {
|
|
16
|
+
const r = user.roles[0];
|
|
17
|
+
return typeof r === 'string' ? r : (r?.name ?? fallback);
|
|
18
|
+
}
|
|
19
|
+
return user.role ?? fallback;
|
|
20
|
+
}
|
|
21
|
+
export function defaultNameResolver(user) {
|
|
22
|
+
return `${user.firstName ?? ''} ${user.lastName ?? ''}`.trim() || (user.email ?? '');
|
|
23
|
+
}
|
|
@@ -15,19 +15,7 @@
|
|
|
15
15
|
//
|
|
16
16
|
// Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
17
17
|
import { comparePassword } from './password.js';
|
|
18
|
-
|
|
19
|
-
return user.status !== 'disabled' && user.status !== 'locked';
|
|
20
|
-
}
|
|
21
|
-
function defaultRoleResolver(user, fallback) {
|
|
22
|
-
if (user.roles?.length > 0) {
|
|
23
|
-
const r = user.roles[0];
|
|
24
|
-
return typeof r === 'string' ? r : (r?.name ?? fallback);
|
|
25
|
-
}
|
|
26
|
-
return user.role ?? fallback;
|
|
27
|
-
}
|
|
28
|
-
function defaultNameResolver(user) {
|
|
29
|
-
return `${user.firstName ?? ''} ${user.lastName ?? ''}`.trim() || (user.email ?? '');
|
|
30
|
-
}
|
|
18
|
+
import { defaultStatusCheck, defaultRoleResolver, defaultNameResolver } from './credentials-helpers.js';
|
|
31
19
|
/**
|
|
32
20
|
* Build a NextAuth Credentials provider config (returned as plain object,
|
|
33
21
|
* directly passable to `NextAuth({ providers: [provider], ... })` without
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface CredentialsVerifyConfig {
|
|
2
|
+
/** Lookup the user row by email (case-insensitive) — must return the user
|
|
3
|
+
* WITH the password hash field. Typically:
|
|
4
|
+
* `(email) => new UserRepository(dialect).findByEmail(email)`
|
|
5
|
+
* Le handler tournant côté serveur (Octonet), il bypasse le sanitizer
|
|
6
|
+
* qui ne strippe que les responses sortant par REST. */
|
|
7
|
+
findUserByEmail: (email: string) => Promise<any | null>;
|
|
8
|
+
/** Default role assigned when the user has no role relations (default: 'user'). */
|
|
9
|
+
defaultRole?: string;
|
|
10
|
+
/** Custom check before allowing login. Default: status !== 'disabled' && !== 'locked'. */
|
|
11
|
+
checkUserAllowed?: (user: any) => boolean | Promise<boolean>;
|
|
12
|
+
/** Custom role resolution. Default: first user.roles[].name or fallback. */
|
|
13
|
+
resolveRole?: (user: any) => string | Promise<string>;
|
|
14
|
+
/** Custom display name. Default: "<firstName> <lastName>" trimmed → email. */
|
|
15
|
+
resolveName?: (user: any) => string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Build a Web standard Request → Response handler that verifies (email, password)
|
|
19
|
+
* server-side and returns a sanitized user payload (no password) on success.
|
|
20
|
+
*
|
|
21
|
+
* Mount it on any HTTP framework that exposes Web Request/Response :
|
|
22
|
+
* Next.js : export const POST = createCredentialsVerifyHandler({...})
|
|
23
|
+
* Fastify : adapter via app.post(path, async (req,reply)=>{ const r = await handler(toRequest(req)); ... })
|
|
24
|
+
* mosta-net : monté automatiquement par le server quand auth est activé
|
|
25
|
+
*
|
|
26
|
+
* Response shape :
|
|
27
|
+
* 200 { ok: true, user: { id, email, name, role } }
|
|
28
|
+
* 400 { error: 'email and password are required' }
|
|
29
|
+
* 401 { error: 'invalid credentials' } // user not found, password wrong, status disabled
|
|
30
|
+
* 500 { error: 'internal error' }
|
|
31
|
+
*
|
|
32
|
+
* Le handler ne renvoie JAMAIS le password hash, ni d'indication sur quelle
|
|
33
|
+
* partie a échoué (user inexistant vs password faux) — défense contre
|
|
34
|
+
* l'énumération d'emails.
|
|
35
|
+
*/
|
|
36
|
+
export declare function createCredentialsVerifyHandler(config: CredentialsVerifyConfig): (req: Request) => Promise<Response>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// @mostajs/auth — Server-side credentials verification handler
|
|
2
|
+
//
|
|
3
|
+
// Frère symétrique de createCredentialsProvider. Là où celui-ci tourne
|
|
4
|
+
// CÔTÉ CLIENT NextAuth (Octocloud) en faisant bcrypt.compare localement,
|
|
5
|
+
// celui-ci tourne CÔTÉ SERVEUR (Octonet, là où vit la DB) et expose le
|
|
6
|
+
// même contrat sous forme de Web standard handler.
|
|
7
|
+
//
|
|
8
|
+
// Pourquoi ce frère existe :
|
|
9
|
+
// En mode gateway (data-plug → MOSTA_DATA=net), Octocloud n'a plus
|
|
10
|
+
// accès direct au password hash — le sanitizer middleware d'Octonet
|
|
11
|
+
// strippe `password` de toute response REST (à juste titre, c'est sa
|
|
12
|
+
// fonction de défense). Faire le bcrypt.compare côté Octocloud est
|
|
13
|
+
// alors impossible.
|
|
14
|
+
//
|
|
15
|
+
// La résolution propre : déplacer le bcrypt.compare LÀ où vit le hash
|
|
16
|
+
// (Octonet), et exposer juste un endpoint POST /api/auth/verify qui
|
|
17
|
+
// prend (email, password) et renvoie {ok:true, user:{id,email,name,role}}
|
|
18
|
+
// ou 401. Le hash ne quitte JAMAIS Octonet — propriété de sécurité forte.
|
|
19
|
+
//
|
|
20
|
+
// Côté Octocloud, on monte createRemoteCredentialsProvider qui POST
|
|
21
|
+
// sur cet endpoint au lieu de tenter le compare local.
|
|
22
|
+
//
|
|
23
|
+
// Auteur des deux côtés : @mostajs/auth (un seul module, deux rôles
|
|
24
|
+
// symétriques). @mostajs/net ne fait que router le HTTP.
|
|
25
|
+
//
|
|
26
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
27
|
+
import { comparePassword } from './password.js';
|
|
28
|
+
import { defaultStatusCheck, defaultRoleResolver, defaultNameResolver } from './credentials-helpers.js';
|
|
29
|
+
/**
|
|
30
|
+
* Build a Web standard Request → Response handler that verifies (email, password)
|
|
31
|
+
* server-side and returns a sanitized user payload (no password) on success.
|
|
32
|
+
*
|
|
33
|
+
* Mount it on any HTTP framework that exposes Web Request/Response :
|
|
34
|
+
* Next.js : export const POST = createCredentialsVerifyHandler({...})
|
|
35
|
+
* Fastify : adapter via app.post(path, async (req,reply)=>{ const r = await handler(toRequest(req)); ... })
|
|
36
|
+
* mosta-net : monté automatiquement par le server quand auth est activé
|
|
37
|
+
*
|
|
38
|
+
* Response shape :
|
|
39
|
+
* 200 { ok: true, user: { id, email, name, role } }
|
|
40
|
+
* 400 { error: 'email and password are required' }
|
|
41
|
+
* 401 { error: 'invalid credentials' } // user not found, password wrong, status disabled
|
|
42
|
+
* 500 { error: 'internal error' }
|
|
43
|
+
*
|
|
44
|
+
* Le handler ne renvoie JAMAIS le password hash, ni d'indication sur quelle
|
|
45
|
+
* partie a échoué (user inexistant vs password faux) — défense contre
|
|
46
|
+
* l'énumération d'emails.
|
|
47
|
+
*/
|
|
48
|
+
export function createCredentialsVerifyHandler(config) {
|
|
49
|
+
const defaultRole = config.defaultRole ?? 'user';
|
|
50
|
+
const checkUserAllowed = config.checkUserAllowed ?? defaultStatusCheck;
|
|
51
|
+
const resolveRole = config.resolveRole ?? ((u) => defaultRoleResolver(u, defaultRole));
|
|
52
|
+
const resolveName = config.resolveName ?? defaultNameResolver;
|
|
53
|
+
return async function POST(req) {
|
|
54
|
+
let body;
|
|
55
|
+
try {
|
|
56
|
+
body = await req.json();
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return Response.json({ error: 'invalid JSON body' }, { status: 400 });
|
|
60
|
+
}
|
|
61
|
+
const email = body?.email;
|
|
62
|
+
const password = body?.password;
|
|
63
|
+
if (!email || !password) {
|
|
64
|
+
return Response.json({ error: 'email and password are required' }, { status: 400 });
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const user = await config.findUserByEmail(String(email).toLowerCase().trim());
|
|
68
|
+
if (!user) {
|
|
69
|
+
return Response.json({ error: 'invalid credentials' }, { status: 401 });
|
|
70
|
+
}
|
|
71
|
+
const allowed = await checkUserAllowed(user);
|
|
72
|
+
if (!allowed) {
|
|
73
|
+
return Response.json({ error: 'invalid credentials' }, { status: 401 });
|
|
74
|
+
}
|
|
75
|
+
const valid = await comparePassword(String(password), user.password);
|
|
76
|
+
if (!valid) {
|
|
77
|
+
return Response.json({ error: 'invalid credentials' }, { status: 401 });
|
|
78
|
+
}
|
|
79
|
+
const role = await resolveRole(user);
|
|
80
|
+
const name = resolveName(user);
|
|
81
|
+
return Response.json({
|
|
82
|
+
ok: true,
|
|
83
|
+
user: {
|
|
84
|
+
id: user.id ?? user._id,
|
|
85
|
+
email: user.email,
|
|
86
|
+
name,
|
|
87
|
+
role,
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
console.error('[auth/verify] internal error:', e?.message || e);
|
|
93
|
+
return Response.json({ error: 'internal error' }, { status: 500 });
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export interface RemoteCredentialsProviderConfig {
|
|
2
|
+
/** URL of the verify endpoint exposed by the server (Octonet).
|
|
3
|
+
* Absolute or relative. Le serveur doit avoir monté
|
|
4
|
+
* createCredentialsVerifyHandler sur ce path (mosta-net le fait). */
|
|
5
|
+
verifyEndpoint: string;
|
|
6
|
+
/** API key identifiant le caller (REQUIS). Pour Octocloud c'est la portal
|
|
7
|
+
* apikey ; pour une app C#/Java/…, c'est leur propre apikey. Octonet
|
|
8
|
+
* est M2M : pas d'apikey = pas d'accès. */
|
|
9
|
+
apiKey: string;
|
|
10
|
+
/** Optional fetch implementation override (testing, custom transport). */
|
|
11
|
+
fetch?: typeof fetch;
|
|
12
|
+
/** Provider id (default: 'credentials'). */
|
|
13
|
+
id?: string;
|
|
14
|
+
/** Provider display name (default: 'Email + Password'). */
|
|
15
|
+
name?: string;
|
|
16
|
+
/** Optional hook called on successful auth — utile pour syncer un mirror
|
|
17
|
+
* local (ex: une table users local côté caller, en miroir d'Octonet). */
|
|
18
|
+
onAuthorized?: (user: {
|
|
19
|
+
id: string;
|
|
20
|
+
email: string;
|
|
21
|
+
name: string;
|
|
22
|
+
role: string;
|
|
23
|
+
}) => void | Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Build a NextAuth Credentials provider config that delegates verification
|
|
27
|
+
* to a remote HTTP endpoint instead of doing bcrypt.compare locally.
|
|
28
|
+
*
|
|
29
|
+
* Returned shape is identical to createCredentialsProvider — directly
|
|
30
|
+
* passable to `NextAuth({ providers: [provider], ... })`.
|
|
31
|
+
*
|
|
32
|
+
* Le password est POST en HTTPS, le hash bcrypt ne quitte jamais Octonet,
|
|
33
|
+
* et seules les infos sanitisées {id, email, name, role} reviennent.
|
|
34
|
+
*/
|
|
35
|
+
export declare function createRemoteCredentialsProvider(config: RemoteCredentialsProviderConfig): {
|
|
36
|
+
id: string;
|
|
37
|
+
name: string;
|
|
38
|
+
type: "credentials";
|
|
39
|
+
credentials: {
|
|
40
|
+
email: {
|
|
41
|
+
label: string;
|
|
42
|
+
type: string;
|
|
43
|
+
};
|
|
44
|
+
password: {
|
|
45
|
+
label: string;
|
|
46
|
+
type: string;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
authorize(credentials: any): Promise<{
|
|
50
|
+
id: any;
|
|
51
|
+
email: any;
|
|
52
|
+
name: any;
|
|
53
|
+
role: any;
|
|
54
|
+
} | null>;
|
|
55
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// @mostajs/auth — Remote credentials provider for NextAuth (gateway mode)
|
|
2
|
+
//
|
|
3
|
+
// Frère client de createCredentialsVerifyHandler. Là où celui-ci tourne
|
|
4
|
+
// CÔTÉ SERVEUR (Octonet, là où vit la DB) et fait bcrypt.compare local,
|
|
5
|
+
// celui-ci tourne CÔTÉ NextAuth (Octocloud, app C#, app Java, …) et
|
|
6
|
+
// délègue la vérification à un endpoint HTTP au lieu de tenter le
|
|
7
|
+
// compare en local.
|
|
8
|
+
//
|
|
9
|
+
// Pourquoi : en mode gateway (data-plug → MOSTA_DATA=net), le caller n'a
|
|
10
|
+
// pas accès au password hash (le sanitizer middleware d'Octonet le strippe
|
|
11
|
+
// à juste titre). On ne peut pas — et on ne DOIT pas — faire le bcrypt.compare
|
|
12
|
+
// côté caller. Le hash reste sur Octonet.
|
|
13
|
+
//
|
|
14
|
+
// L'`apiKey` est REQUIS : Octonet est M2M et toute requête s'identifie
|
|
15
|
+
// par une apikey. Le caller (Octocloud, ou autre app cliente du portail)
|
|
16
|
+
// envoie SA propre apikey — qui scope les users qu'il peut verify
|
|
17
|
+
// (multi-tenant β via account-scope-middleware).
|
|
18
|
+
//
|
|
19
|
+
// Drop-in replacement de createCredentialsProvider : même shape de retour,
|
|
20
|
+
// même UX NextAuth — seul le `findUserByEmail` callback est remplacé par
|
|
21
|
+
// un appel HTTP à `verifyEndpoint`.
|
|
22
|
+
//
|
|
23
|
+
// Usage typique (Octocloud) :
|
|
24
|
+
// NextAuth({
|
|
25
|
+
// providers: [
|
|
26
|
+
// createRemoteCredentialsProvider({
|
|
27
|
+
// verifyEndpoint: 'https://octonet.amia.fr/api/auth/verify',
|
|
28
|
+
// apiKey: process.env.OCTONET_PORTAL_API_KEY!,
|
|
29
|
+
// }),
|
|
30
|
+
// ],
|
|
31
|
+
// })
|
|
32
|
+
//
|
|
33
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
34
|
+
/**
|
|
35
|
+
* Build a NextAuth Credentials provider config that delegates verification
|
|
36
|
+
* to a remote HTTP endpoint instead of doing bcrypt.compare locally.
|
|
37
|
+
*
|
|
38
|
+
* Returned shape is identical to createCredentialsProvider — directly
|
|
39
|
+
* passable to `NextAuth({ providers: [provider], ... })`.
|
|
40
|
+
*
|
|
41
|
+
* Le password est POST en HTTPS, le hash bcrypt ne quitte jamais Octonet,
|
|
42
|
+
* et seules les infos sanitisées {id, email, name, role} reviennent.
|
|
43
|
+
*/
|
|
44
|
+
export function createRemoteCredentialsProvider(config) {
|
|
45
|
+
if (!config.apiKey) {
|
|
46
|
+
throw new Error('@mostajs/auth/createRemoteCredentialsProvider: `apiKey` is required. ' +
|
|
47
|
+
'Octonet is M2M — every caller (Octocloud, NetClient app, etc.) must identify with its own apikey.');
|
|
48
|
+
}
|
|
49
|
+
if (!config.verifyEndpoint) {
|
|
50
|
+
throw new Error('@mostajs/auth/createRemoteCredentialsProvider: `verifyEndpoint` is required.');
|
|
51
|
+
}
|
|
52
|
+
const fetchImpl = config.fetch ?? fetch;
|
|
53
|
+
return {
|
|
54
|
+
id: config.id ?? 'credentials',
|
|
55
|
+
name: config.name ?? 'Email + Password',
|
|
56
|
+
type: 'credentials',
|
|
57
|
+
credentials: {
|
|
58
|
+
email: { label: 'Email', type: 'email' },
|
|
59
|
+
password: { label: 'Password', type: 'password' },
|
|
60
|
+
},
|
|
61
|
+
async authorize(credentials) {
|
|
62
|
+
if (!credentials?.email || !credentials?.password)
|
|
63
|
+
return null;
|
|
64
|
+
const headers = {
|
|
65
|
+
'Content-Type': 'application/json',
|
|
66
|
+
'X-API-Key': config.apiKey,
|
|
67
|
+
};
|
|
68
|
+
let resp;
|
|
69
|
+
try {
|
|
70
|
+
resp = await fetchImpl(config.verifyEndpoint, {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers,
|
|
73
|
+
body: JSON.stringify({
|
|
74
|
+
email: String(credentials.email).toLowerCase().trim(),
|
|
75
|
+
password: String(credentials.password),
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
console.error('[remote-credentials] network error:', e?.message || e);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
if (!resp.ok) {
|
|
84
|
+
// 401 = invalid credentials, 400 = bad request, 500 = server error.
|
|
85
|
+
// NextAuth attend null pour reject — on ne distingue pas (defense
|
|
86
|
+
// contre l'énumération d'emails côté UI).
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
let payload;
|
|
90
|
+
try {
|
|
91
|
+
payload = await resp.json();
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
if (!payload?.ok || !payload?.user)
|
|
97
|
+
return null;
|
|
98
|
+
const user = payload.user;
|
|
99
|
+
if (config.onAuthorized) {
|
|
100
|
+
try {
|
|
101
|
+
await config.onAuthorized(user);
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
console.warn('[remote-credentials] onAuthorized hook failed:', e?.message || e);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
id: user.id,
|
|
109
|
+
email: user.email,
|
|
110
|
+
name: user.name,
|
|
111
|
+
role: user.role,
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
package/dist/server.d.ts
CHANGED
|
@@ -22,5 +22,9 @@ export { createApiKeyProvider } from './lib/apikey-provider';
|
|
|
22
22
|
export type { ApiKeyProviderConfig } from './lib/apikey-provider';
|
|
23
23
|
export { createCredentialsProvider } from './lib/credentials-provider';
|
|
24
24
|
export type { CredentialsProviderConfig } from './lib/credentials-provider';
|
|
25
|
+
export { createCredentialsVerifyHandler } from './lib/credentials-verify';
|
|
26
|
+
export type { CredentialsVerifyConfig } from './lib/credentials-verify';
|
|
27
|
+
export { createRemoteCredentialsProvider } from './lib/remote-credentials-provider';
|
|
28
|
+
export type { RemoteCredentialsProviderConfig } from './lib/remote-credentials-provider';
|
|
25
29
|
export { enrichTokenWithPlan, enrichSessionWithPlan } from './lib/session-enrichment';
|
|
26
30
|
export type { SessionEnrichmentConfig } from './lib/session-enrichment';
|
package/dist/server.js
CHANGED
|
@@ -28,5 +28,7 @@ export { createPasswordResetHandlers, generateResetToken } from './lib/password-
|
|
|
28
28
|
// API key provider
|
|
29
29
|
export { createApiKeyProvider } from './lib/apikey-provider.js';
|
|
30
30
|
export { createCredentialsProvider } from './lib/credentials-provider.js';
|
|
31
|
+
export { createCredentialsVerifyHandler } from './lib/credentials-verify.js';
|
|
32
|
+
export { createRemoteCredentialsProvider } from './lib/remote-credentials-provider.js';
|
|
31
33
|
// Session enrichment
|
|
32
34
|
export { enrichTokenWithPlan, enrichSessionWithPlan } from './lib/session-enrichment.js';
|