@mostajs/auth 2.3.2 → 2.4.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.
|
@@ -24,6 +24,16 @@ export interface CheckRequestParams {
|
|
|
24
24
|
}>;
|
|
25
25
|
/** Allow request through without apikey (legacy / public-mode). */
|
|
26
26
|
openMode?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* When no apikey is provided, fall back to a labeled apikey stored in
|
|
29
|
+
* the api_keys table (typically `public-default`). The fallback's
|
|
30
|
+
* permissions still apply — so a public-default key with read-only
|
|
31
|
+
* scope keeps writes blocked even when no key is sent.
|
|
32
|
+
*
|
|
33
|
+
* Use case : MCP endpoint that wants to preserve compat with existing
|
|
34
|
+
* mcp.so / Claude Desktop users who haven't published the apikey yet.
|
|
35
|
+
*/
|
|
36
|
+
fallbackPublicLabel?: string;
|
|
27
37
|
}
|
|
28
38
|
export interface CheckRequestResult {
|
|
29
39
|
ok: boolean;
|
|
@@ -36,7 +36,7 @@ function pickQuery(q, name) {
|
|
|
36
36
|
* (e.g. `if (!result.ok) reply.code(result.status).send(result.body)`).
|
|
37
37
|
*/
|
|
38
38
|
export async function checkRequest(params) {
|
|
39
|
-
const { dialect, headers, query, ip, transport, checks = [], openMode = false } = params;
|
|
39
|
+
const { dialect, headers, query, ip, transport, checks = [], openMode = false, fallbackPublicLabel } = params;
|
|
40
40
|
// Extract auth identifiers from the request
|
|
41
41
|
const apiKey = pickHeader(headers, 'x-api-key') ??
|
|
42
42
|
(pickHeader(headers, 'authorization')?.replace(/^Bearer\s+/i, '').trim() || undefined) ??
|
|
@@ -53,8 +53,53 @@ export async function checkRequest(params) {
|
|
|
53
53
|
projectName,
|
|
54
54
|
meta: { ip: resolvedIp },
|
|
55
55
|
};
|
|
56
|
-
// No apikey → either openMode
|
|
56
|
+
// No apikey → either fallback to a public labeled key, or openMode pass-through, or 401
|
|
57
57
|
if (!apiKey) {
|
|
58
|
+
// Fallback to a labeled public apikey (e.g. 'public-default') if configured.
|
|
59
|
+
// Looks up the row by label, applies its scope checks. Useful for MCP /
|
|
60
|
+
// demo endpoints that want to preserve compat with users who haven't yet
|
|
61
|
+
// published the public apikey in their config.
|
|
62
|
+
if (fallbackPublicLabel && dialect) {
|
|
63
|
+
try {
|
|
64
|
+
const { getApiKeyRepo, isScopeAuthorized } = await import('@mostajs/api-keys/server');
|
|
65
|
+
const repo = getApiKeyRepo(dialect);
|
|
66
|
+
const fallbackKey = await repo.findOne({ label: fallbackPublicLabel, enabled: true });
|
|
67
|
+
if (fallbackKey) {
|
|
68
|
+
// Normalize legacy permission shape (move projects/operations/transports into scopes)
|
|
69
|
+
const perms = fallbackKey.permissions || {};
|
|
70
|
+
const scopes = { ...(perms.scopes ?? {}) };
|
|
71
|
+
for (const k of ['projects', 'operations', 'transports']) {
|
|
72
|
+
if (perms[k] !== undefined && scopes[k] === undefined)
|
|
73
|
+
scopes[k] = perms[k];
|
|
74
|
+
}
|
|
75
|
+
const normPerms = { ...perms, scopes };
|
|
76
|
+
// Run the same scope checks against the public key's perms
|
|
77
|
+
for (const c of checks) {
|
|
78
|
+
if (!isScopeAuthorized(normPerms, c.scope, c.value)) {
|
|
79
|
+
return {
|
|
80
|
+
ok: false, status: 403,
|
|
81
|
+
body: { status: 'error', error: { code: 'FORBIDDEN',
|
|
82
|
+
message: `Public access not authorized for ${c.scope}="${c.value}"`, } },
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
ok: true, status: 200,
|
|
88
|
+
apikey: fallbackKey,
|
|
89
|
+
ctx: {
|
|
90
|
+
...baseCtx,
|
|
91
|
+
subscription: fallbackKey.label || fallbackKey.id,
|
|
92
|
+
permissions: normPerms.scopes ?? {},
|
|
93
|
+
accountId: fallbackKey.account || fallbackKey.accountId,
|
|
94
|
+
apikeyId: fallbackKey.id,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// Fallback failed — fall through to standard 401 / openMode
|
|
101
|
+
}
|
|
102
|
+
}
|
|
58
103
|
if (openMode)
|
|
59
104
|
return { ok: true, status: 200, ctx: baseCtx };
|
|
60
105
|
return {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export interface CredentialsProviderConfig {
|
|
2
|
+
/** Lookup the user row by email (case-insensitive). Return null if not found.
|
|
3
|
+
* Conseillé : passer une fonction qui utilise UserRepository de @mostajs/rbac
|
|
4
|
+
* pour bénéficier de findByEmail (qui lowercase l'email côté repo). */
|
|
5
|
+
findUserByEmail: (email: string) => Promise<any | null>;
|
|
6
|
+
/** Default role assigned when the user has no role relations (default: 'user'). */
|
|
7
|
+
defaultRole?: string;
|
|
8
|
+
/** Custom check before allowing login (e.g. email verified, 2FA).
|
|
9
|
+
* Return false to block. Default: status !== 'disabled' && status !== 'locked'. */
|
|
10
|
+
checkUserAllowed?: (user: any) => boolean | Promise<boolean>;
|
|
11
|
+
/** Custom role resolution. Default: first user.roles[].name or fallback. */
|
|
12
|
+
resolveRole?: (user: any) => string | Promise<string>;
|
|
13
|
+
/** Custom display name. Default: "<firstName> <lastName>" trimmed → email. */
|
|
14
|
+
resolveName?: (user: any) => string;
|
|
15
|
+
/** Provider id (default: 'credentials'). Useful if multiple providers coexist. */
|
|
16
|
+
id?: string;
|
|
17
|
+
/** Provider display name (default: 'Email + Password'). */
|
|
18
|
+
name?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Build a NextAuth Credentials provider config (returned as plain object,
|
|
22
|
+
* directly passable to `NextAuth({ providers: [provider], ... })` without
|
|
23
|
+
* any next-auth imports côté consumer).
|
|
24
|
+
*
|
|
25
|
+
* Usage :
|
|
26
|
+
* import { createCredentialsProvider } from '@mostajs/auth/server'
|
|
27
|
+
*
|
|
28
|
+
* NextAuth({
|
|
29
|
+
* providers: [
|
|
30
|
+
* createCredentialsProvider({
|
|
31
|
+
* findUserByEmail: async (email) => userRepo.findByEmail(email),
|
|
32
|
+
* defaultRole: 'user',
|
|
33
|
+
* }),
|
|
34
|
+
* ],
|
|
35
|
+
* })
|
|
36
|
+
*/
|
|
37
|
+
export declare function createCredentialsProvider(config: CredentialsProviderConfig): {
|
|
38
|
+
id: string;
|
|
39
|
+
name: string;
|
|
40
|
+
type: "credentials";
|
|
41
|
+
credentials: {
|
|
42
|
+
email: {
|
|
43
|
+
label: string;
|
|
44
|
+
type: string;
|
|
45
|
+
};
|
|
46
|
+
password: {
|
|
47
|
+
label: string;
|
|
48
|
+
type: string;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
authorize(credentials: any): Promise<{
|
|
52
|
+
id: any;
|
|
53
|
+
email: any;
|
|
54
|
+
name: string;
|
|
55
|
+
role: string;
|
|
56
|
+
} | null>;
|
|
57
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// @mostajs/auth — Email + password credentials provider for NextAuth
|
|
2
|
+
//
|
|
3
|
+
// Factorise le boilerplate NextAuth Credentials que chaque app refait. Le
|
|
4
|
+
// consumer obtient un provider PRÊT À L'EMPLOI à passer à NextAuth({providers}) :
|
|
5
|
+
// il n'importe ni `next-auth/providers/credentials`, ni bcrypt, ni le
|
|
6
|
+
// UserRepository — tout est encapsulé.
|
|
7
|
+
//
|
|
8
|
+
// Pendant naturel de createApiKeyProvider (même style de retour : objet
|
|
9
|
+
// provider config plat). Différence : authentication via email/password
|
|
10
|
+
// (UI register/signin) vs API key (programmatic).
|
|
11
|
+
//
|
|
12
|
+
// Le caller fournit `findUserByEmail` (ou laisse passer pour un default
|
|
13
|
+
// fail-closed). Pour leverager directement @mostajs/rbac, il suffit de
|
|
14
|
+
// passer `(email) => new UserRepository(dialect).findByEmail(email)`.
|
|
15
|
+
//
|
|
16
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
17
|
+
import { comparePassword } from './password.js';
|
|
18
|
+
function defaultStatusCheck(user) {
|
|
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
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Build a NextAuth Credentials provider config (returned as plain object,
|
|
33
|
+
* directly passable to `NextAuth({ providers: [provider], ... })` without
|
|
34
|
+
* any next-auth imports côté consumer).
|
|
35
|
+
*
|
|
36
|
+
* Usage :
|
|
37
|
+
* import { createCredentialsProvider } from '@mostajs/auth/server'
|
|
38
|
+
*
|
|
39
|
+
* NextAuth({
|
|
40
|
+
* providers: [
|
|
41
|
+
* createCredentialsProvider({
|
|
42
|
+
* findUserByEmail: async (email) => userRepo.findByEmail(email),
|
|
43
|
+
* defaultRole: 'user',
|
|
44
|
+
* }),
|
|
45
|
+
* ],
|
|
46
|
+
* })
|
|
47
|
+
*/
|
|
48
|
+
export function createCredentialsProvider(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 {
|
|
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 email = String(credentials.email).toLowerCase().trim();
|
|
65
|
+
const user = await config.findUserByEmail(email);
|
|
66
|
+
if (!user)
|
|
67
|
+
return null;
|
|
68
|
+
const allowed = await checkUserAllowed(user);
|
|
69
|
+
if (!allowed)
|
|
70
|
+
return null;
|
|
71
|
+
const valid = await comparePassword(String(credentials.password), user.password);
|
|
72
|
+
if (!valid)
|
|
73
|
+
return null;
|
|
74
|
+
const role = await resolveRole(user);
|
|
75
|
+
const name = resolveName(user);
|
|
76
|
+
return {
|
|
77
|
+
id: user.id,
|
|
78
|
+
email: user.email,
|
|
79
|
+
name,
|
|
80
|
+
role,
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
package/dist/server.d.ts
CHANGED
|
@@ -20,5 +20,7 @@ export { createPasswordResetHandlers, generateResetToken } from './lib/password-
|
|
|
20
20
|
export type { PasswordResetConfig } from './lib/password-reset';
|
|
21
21
|
export { createApiKeyProvider } from './lib/apikey-provider';
|
|
22
22
|
export type { ApiKeyProviderConfig } from './lib/apikey-provider';
|
|
23
|
+
export { createCredentialsProvider } from './lib/credentials-provider';
|
|
24
|
+
export type { CredentialsProviderConfig } from './lib/credentials-provider';
|
|
23
25
|
export { enrichTokenWithPlan, enrichSessionWithPlan } from './lib/session-enrichment';
|
|
24
26
|
export type { SessionEnrichmentConfig } from './lib/session-enrichment';
|
package/dist/server.js
CHANGED
|
@@ -27,5 +27,6 @@ export { createVerificationHandlers, generateVerifyToken } from './lib/email-ver
|
|
|
27
27
|
export { createPasswordResetHandlers, generateResetToken } from './lib/password-reset.js';
|
|
28
28
|
// API key provider
|
|
29
29
|
export { createApiKeyProvider } from './lib/apikey-provider.js';
|
|
30
|
+
export { createCredentialsProvider } from './lib/credentials-provider.js';
|
|
30
31
|
// Session enrichment
|
|
31
32
|
export { enrichTokenWithPlan, enrichSessionWithPlan } from './lib/session-enrichment.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mostajs/auth",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Authentication — NextAuth, password hashing, session management",
|
|
5
5
|
"author": "Dr Hamid MADANI <drmdh@msn.com>",
|
|
6
6
|
"license": "AGPL-3.0-or-later",
|
|
@@ -83,6 +83,11 @@
|
|
|
83
83
|
"import": "./dist/lib/apikey-provider.js",
|
|
84
84
|
"default": "./dist/lib/apikey-provider.js"
|
|
85
85
|
},
|
|
86
|
+
"./lib/credentials-provider": {
|
|
87
|
+
"types": "./dist/lib/credentials-provider.d.ts",
|
|
88
|
+
"import": "./dist/lib/credentials-provider.js",
|
|
89
|
+
"default": "./dist/lib/credentials-provider.js"
|
|
90
|
+
},
|
|
86
91
|
"./lib/check-request": {
|
|
87
92
|
"types": "./dist/lib/check-request.d.ts",
|
|
88
93
|
"import": "./dist/lib/check-request.js",
|
|
@@ -120,7 +125,7 @@
|
|
|
120
125
|
},
|
|
121
126
|
"scripts": {
|
|
122
127
|
"build": "tsc && npm run fix-esm",
|
|
123
|
-
"fix-esm": "find dist -name '*.js' -exec sed -i -E \"s|from '(\\\\.{1,2}/[^']+)'(;?)|from '\\\\1.js'\\\\2|g; s|from \\\"(\\\\.{1,2}/[^\\\"]+)\\\"(;?)|from \\\"\\\\1.js\\\"\\\\2|g\" {} \\; && find dist -name '*.js' -exec sed -i -E \"s|\\\\.js\\\\.js|.js|g\" {} \\;",
|
|
128
|
+
"fix-esm": "find dist -name '*.js' -exec sed -i -E \"s|from '(\\\\.{1,2}/[^']+)'(;?)|from '\\\\1.js'\\\\2|g; s|from \\\"(\\\\.{1,2}/[^\\\"]+)\\\"(;?)|from \\\"\\\\1.js\\\"\\\\2|g\" {} \\; && find dist -name '*.js' -exec sed -i -E \"s|\\\\.js\\\\.js|.js|g; s|\\\\.json\\\\.js|.json|g; s|\\\\.css\\\\.js|.css|g\" {} \\;",
|
|
124
129
|
"prepublishOnly": "npm run build"
|
|
125
130
|
},
|
|
126
131
|
"dependencies": {
|