@technomoron/api-server-base 2.0.0-beta.17 → 2.0.0-beta.19
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/README.txt +48 -35
- package/dist/cjs/api-module.cjs +9 -0
- package/dist/cjs/api-module.d.ts +4 -2
- package/dist/cjs/api-server-base.cjs +178 -57
- package/dist/cjs/api-server-base.d.ts +31 -2
- package/dist/cjs/auth-api/auth-module.d.ts +12 -1
- package/dist/cjs/auth-api/auth-module.js +77 -35
- package/dist/cjs/auth-api/mem-auth-store.js +2 -23
- package/dist/cjs/auth-api/sql-auth-store.js +4 -31
- package/dist/cjs/auth-api/user-id.d.ts +4 -0
- package/dist/cjs/auth-api/user-id.js +31 -0
- package/dist/cjs/auth-cookie-options.d.ts +11 -0
- package/dist/cjs/auth-cookie-options.js +57 -0
- package/dist/cjs/oauth/memory.js +4 -10
- package/dist/cjs/oauth/models.js +4 -15
- package/dist/cjs/oauth/sequelize.js +8 -23
- package/dist/cjs/passkey/config.d.ts +2 -0
- package/dist/cjs/passkey/config.js +26 -0
- package/dist/cjs/passkey/memory.js +2 -9
- package/dist/cjs/passkey/models.js +4 -15
- package/dist/cjs/passkey/sequelize.js +6 -22
- package/dist/cjs/passkey/service.js +1 -1
- package/dist/cjs/passkey/types.d.ts +5 -0
- package/dist/cjs/sequelize-utils.d.ts +3 -0
- package/dist/cjs/sequelize-utils.js +17 -0
- package/dist/cjs/token/memory.d.ts +4 -0
- package/dist/cjs/token/memory.js +90 -25
- package/dist/cjs/token/sequelize.js +16 -22
- package/dist/cjs/token/types.d.ts +7 -0
- package/dist/cjs/user/memory.js +2 -9
- package/dist/cjs/user/sequelize.js +6 -22
- package/dist/esm/api-module.d.ts +4 -2
- package/dist/esm/api-module.js +9 -0
- package/dist/esm/api-server-base.d.ts +31 -2
- package/dist/esm/api-server-base.js +178 -57
- package/dist/esm/auth-api/auth-module.d.ts +12 -1
- package/dist/esm/auth-api/auth-module.js +77 -35
- package/dist/esm/auth-api/mem-auth-store.js +1 -22
- package/dist/esm/auth-api/sql-auth-store.js +2 -29
- package/dist/esm/auth-api/user-id.d.ts +4 -0
- package/dist/esm/auth-api/user-id.js +26 -0
- package/dist/esm/auth-cookie-options.d.ts +11 -0
- package/dist/esm/auth-cookie-options.js +54 -0
- package/dist/esm/oauth/memory.js +4 -10
- package/dist/esm/oauth/models.js +1 -12
- package/dist/esm/oauth/sequelize.js +5 -20
- package/dist/esm/passkey/config.d.ts +2 -0
- package/dist/esm/passkey/config.js +23 -0
- package/dist/esm/passkey/memory.js +2 -9
- package/dist/esm/passkey/models.js +1 -12
- package/dist/esm/passkey/sequelize.js +3 -19
- package/dist/esm/passkey/service.js +1 -1
- package/dist/esm/passkey/types.d.ts +5 -0
- package/dist/esm/sequelize-utils.d.ts +3 -0
- package/dist/esm/sequelize-utils.js +12 -0
- package/dist/esm/token/memory.d.ts +4 -0
- package/dist/esm/token/memory.js +90 -25
- package/dist/esm/token/sequelize.js +12 -18
- package/dist/esm/token/types.d.ts +7 -0
- package/dist/esm/user/memory.js +2 -9
- package/dist/esm/user/sequelize.js +3 -19
- package/docs/swagger/openapi.json +11 -145
- package/package.json +12 -12
|
@@ -100,7 +100,21 @@ export interface ApiServerConf {
|
|
|
100
100
|
swaggerPath?: string;
|
|
101
101
|
accessSecret: string;
|
|
102
102
|
refreshSecret: string;
|
|
103
|
+
/** Cookie domain for auth cookies. Prefer leaving empty for localhost/development. */
|
|
103
104
|
cookieDomain: string;
|
|
105
|
+
/** Cookie path for auth cookies. */
|
|
106
|
+
cookiePath?: string;
|
|
107
|
+
/** Cookie SameSite attribute for auth cookies. */
|
|
108
|
+
cookieSameSite?: 'lax' | 'strict' | 'none';
|
|
109
|
+
/**
|
|
110
|
+
* Cookie Secure attribute for auth cookies.
|
|
111
|
+
* - true: always secure
|
|
112
|
+
* - false: never secure
|
|
113
|
+
* - 'auto': secure when request is HTTPS (or forwarded as HTTPS)
|
|
114
|
+
*/
|
|
115
|
+
cookieSecure?: boolean | 'auto';
|
|
116
|
+
/** Cookie HttpOnly attribute for auth cookies. */
|
|
117
|
+
cookieHttpOnly?: boolean;
|
|
104
118
|
accessCookie: string;
|
|
105
119
|
refreshCookie: string;
|
|
106
120
|
accessExpiry: number;
|
|
@@ -115,24 +129,38 @@ export interface ApiServerConf {
|
|
|
115
129
|
minClientVersion: string;
|
|
116
130
|
tokenStore?: TokenStore;
|
|
117
131
|
authStores?: ApiServerAuthStores;
|
|
132
|
+
onStartError?: (error: Error) => void;
|
|
118
133
|
}
|
|
119
134
|
export declare class ApiServer {
|
|
120
135
|
app: Application;
|
|
121
|
-
currReq: ApiRequest | null;
|
|
122
136
|
readonly config: ApiServerConf;
|
|
123
137
|
readonly startedAt: number;
|
|
124
138
|
private readonly apiBasePath;
|
|
139
|
+
private readonly apiRouter;
|
|
140
|
+
private finalized;
|
|
125
141
|
private storageAdapter;
|
|
126
142
|
private moduleAdapter;
|
|
127
143
|
private serverAuthAdapter;
|
|
128
144
|
private apiNotFoundHandler;
|
|
145
|
+
private apiErrorHandlerInstalled;
|
|
129
146
|
private tokenStoreAdapter;
|
|
130
147
|
private userStoreAdapter;
|
|
131
148
|
private passkeyServiceAdapter;
|
|
132
149
|
private oauthStoreAdapter;
|
|
133
150
|
private canImpersonateAdapter;
|
|
134
151
|
private readonly jwtHelper;
|
|
152
|
+
private currReqDeprecationWarned;
|
|
153
|
+
/**
|
|
154
|
+
* @deprecated ApiServer does not track a global "current request". This value is always null.
|
|
155
|
+
* Use the per-request ApiRequest passed to handlers, or `req.apiReq` / `res.locals.apiReq`
|
|
156
|
+
* when mounting raw Express endpoints.
|
|
157
|
+
*/
|
|
158
|
+
get currReq(): ApiRequest | null;
|
|
159
|
+
set currReq(_value: ApiRequest | null);
|
|
135
160
|
constructor(config?: Partial<ApiServerConf>);
|
|
161
|
+
private assertNotFinalized;
|
|
162
|
+
private toApiRouterPath;
|
|
163
|
+
finalize(): this;
|
|
136
164
|
authStorage<UserRow, SafeUser>(storage: AuthAdapter<UserRow, SafeUser>): this;
|
|
137
165
|
/**
|
|
138
166
|
* @deprecated Use {@link ApiServer.authStorage} instead.
|
|
@@ -200,9 +228,10 @@ export declare class ApiServer {
|
|
|
200
228
|
private installSwaggerHandler;
|
|
201
229
|
private normalizeApiBasePath;
|
|
202
230
|
private installApiNotFoundHandler;
|
|
203
|
-
private
|
|
231
|
+
private installApiErrorHandler;
|
|
204
232
|
private describeMissingEndpoint;
|
|
205
233
|
start(): this;
|
|
234
|
+
private internalServerErrorMessage;
|
|
206
235
|
private verifyJWT;
|
|
207
236
|
private jwtCookieOptions;
|
|
208
237
|
private setAccessCookie;
|
|
@@ -10,10 +10,16 @@ interface CanImpersonateContext<UserEntity> {
|
|
|
10
10
|
targetUser: UserEntity;
|
|
11
11
|
effectiveUserId: AuthIdentifier;
|
|
12
12
|
}
|
|
13
|
+
type AuthRateLimitEndpoint = 'login' | 'passkey-challenge' | 'oauth-token';
|
|
13
14
|
interface AuthModuleOptions<UserEntity> {
|
|
14
15
|
namespace?: string;
|
|
15
16
|
defaultDomain?: string;
|
|
16
17
|
canImpersonate?: (context: CanImpersonateContext<UserEntity>) => Promise<boolean> | boolean;
|
|
18
|
+
rateLimit?: (context: {
|
|
19
|
+
apiReq: ApiRequest;
|
|
20
|
+
endpoint: AuthRateLimitEndpoint;
|
|
21
|
+
}) => Promise<void> | void;
|
|
22
|
+
allowInsecurePkcePlain?: boolean;
|
|
17
23
|
}
|
|
18
24
|
type TokenMetadata = Partial<Token> & {
|
|
19
25
|
sessionCookie?: boolean;
|
|
@@ -42,9 +48,12 @@ type AuthCapableServer<PublicUser> = ApiServer & {
|
|
|
42
48
|
};
|
|
43
49
|
export default class AuthModule<UserEntity, PublicUser> extends BaseAuthModule<UserEntity> implements AuthProviderModule<UserEntity> {
|
|
44
50
|
static defaultNamespace: string;
|
|
45
|
-
server: AuthCapableServer<PublicUser>;
|
|
51
|
+
get server(): AuthCapableServer<PublicUser>;
|
|
52
|
+
set server(value: AuthCapableServer<PublicUser>);
|
|
46
53
|
private readonly defaultDomain?;
|
|
47
54
|
private readonly canImpersonateHook?;
|
|
55
|
+
private readonly rateLimitHook?;
|
|
56
|
+
private readonly allowInsecurePkcePlain;
|
|
48
57
|
constructor(options?: AuthModuleOptions<UserEntity>);
|
|
49
58
|
protected get storage(): AuthAdapter<UserEntity, PublicUser>;
|
|
50
59
|
protected canImpersonate(apiReq: ApiRequest, realUser: UserEntity, targetUser: UserEntity): Promise<boolean>;
|
|
@@ -100,6 +109,8 @@ export default class AuthModule<UserEntity, PublicUser> extends BaseAuthModule<U
|
|
|
100
109
|
private hasOAuthStore;
|
|
101
110
|
private storageImplements;
|
|
102
111
|
private storageImplementsAll;
|
|
112
|
+
private applyRateLimit;
|
|
113
|
+
private resolvePkceChallengeMethod;
|
|
103
114
|
defineRoutes(): ApiRoute[];
|
|
104
115
|
}
|
|
105
116
|
export {};
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
const node_crypto_1 = require("node:crypto");
|
|
4
4
|
const helpers_1 = require("@simplewebauthn/server/helpers");
|
|
5
5
|
const api_server_base_js_1 = require("../api-server-base.js");
|
|
6
|
+
const auth_cookie_options_js_1 = require("../auth-cookie-options.js");
|
|
6
7
|
const module_js_1 = require("./module.js");
|
|
7
8
|
const storage_js_1 = require("./storage.js");
|
|
8
9
|
function isAuthIdentifier(value) {
|
|
@@ -32,10 +33,18 @@ function sha256Base64Url(value) {
|
|
|
32
33
|
return base64UrlEncode(hash);
|
|
33
34
|
}
|
|
34
35
|
class AuthModule extends module_js_1.BaseAuthModule {
|
|
36
|
+
get server() {
|
|
37
|
+
return super.server;
|
|
38
|
+
}
|
|
39
|
+
set server(value) {
|
|
40
|
+
super.server = value;
|
|
41
|
+
}
|
|
35
42
|
constructor(options = {}) {
|
|
36
43
|
super({ namespace: options.namespace ?? AuthModule.defaultNamespace });
|
|
37
44
|
this.defaultDomain = options.defaultDomain;
|
|
38
45
|
this.canImpersonateHook = options.canImpersonate;
|
|
46
|
+
this.rateLimitHook = options.rateLimit;
|
|
47
|
+
this.allowInsecurePkcePlain = options.allowInsecurePkcePlain ?? true;
|
|
39
48
|
}
|
|
40
49
|
get storage() {
|
|
41
50
|
return this.server.getAuthStorage();
|
|
@@ -236,29 +245,7 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
236
245
|
return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
|
|
237
246
|
}
|
|
238
247
|
cookieOptions(apiReq) {
|
|
239
|
-
|
|
240
|
-
const forwarded = apiReq.req.headers['x-forwarded-proto'];
|
|
241
|
-
const referer = apiReq.req.headers['origin'] ?? apiReq.req.headers['referer'];
|
|
242
|
-
const origin = typeof referer === 'string' ? referer : '';
|
|
243
|
-
const isHttps = forwarded === 'https' || apiReq.req.protocol === 'https';
|
|
244
|
-
const isLocalhost = origin.includes('localhost');
|
|
245
|
-
const options = {
|
|
246
|
-
httpOnly: true,
|
|
247
|
-
secure: true,
|
|
248
|
-
sameSite: 'strict',
|
|
249
|
-
domain: conf.cookieDomain || undefined,
|
|
250
|
-
path: '/',
|
|
251
|
-
maxAge: undefined
|
|
252
|
-
};
|
|
253
|
-
if (conf.devMode) {
|
|
254
|
-
options.secure = isHttps;
|
|
255
|
-
options.httpOnly = false;
|
|
256
|
-
options.sameSite = 'lax';
|
|
257
|
-
if (isLocalhost) {
|
|
258
|
-
options.domain = undefined;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
return options;
|
|
248
|
+
return (0, auth_cookie_options_js_1.buildAuthCookieOptions)(this.server.config, apiReq.req);
|
|
262
249
|
}
|
|
263
250
|
setJwtCookies(apiReq, tokens, preferences = {}) {
|
|
264
251
|
const conf = this.server.config;
|
|
@@ -285,7 +272,10 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
285
272
|
async issueTokens(apiReq, user, metadata = {}) {
|
|
286
273
|
const conf = this.server.config;
|
|
287
274
|
const enrichedMetadata = this.enrichTokenMetadata(apiReq, metadata);
|
|
288
|
-
const payload =
|
|
275
|
+
const payload = {
|
|
276
|
+
...this.buildTokenPayload(user, enrichedMetadata),
|
|
277
|
+
jti: (0, node_crypto_1.randomUUID)()
|
|
278
|
+
};
|
|
289
279
|
const access = this.server.jwtSign(payload, conf.accessSecret, conf.accessExpiry);
|
|
290
280
|
if (!access.success || !access.token) {
|
|
291
281
|
throw new api_server_base_js_1.ApiError({ code: 500, message: access.error ?? 'Unable to sign access token' });
|
|
@@ -455,6 +445,7 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
455
445
|
return undefined;
|
|
456
446
|
}
|
|
457
447
|
async postLogin(apiReq) {
|
|
448
|
+
await this.applyRateLimit(apiReq, 'login');
|
|
458
449
|
this.assertAuthReady();
|
|
459
450
|
const { login, password, ...metadata } = this.parseLoginBody(apiReq);
|
|
460
451
|
const user = await this.storage.getUser(login);
|
|
@@ -552,10 +543,39 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
552
543
|
apiReq.req.cookies[conf.accessCookie].trim().length > 0);
|
|
553
544
|
const shouldRefresh = Boolean(body.refresh) || !hasAccessToken;
|
|
554
545
|
if (shouldRefresh) {
|
|
555
|
-
const
|
|
546
|
+
const updateToken = this.storage.updateToken;
|
|
547
|
+
if (typeof updateToken !== 'function' || !this.storageImplements('updateToken')) {
|
|
548
|
+
throw new api_server_base_js_1.ApiError({ code: 501, message: 'Token update storage is not configured' });
|
|
549
|
+
}
|
|
550
|
+
// Sign a new access token without embedding stored token secrets into the JWT payload.
|
|
551
|
+
const metadata = {
|
|
552
|
+
ruid: stored.ruid,
|
|
553
|
+
domain: stored.domain,
|
|
554
|
+
fingerprint: stored.fingerprint,
|
|
555
|
+
label: stored.label,
|
|
556
|
+
clientId: stored.clientId,
|
|
557
|
+
scope: stored.scope,
|
|
558
|
+
browser: stored.browser,
|
|
559
|
+
device: stored.device,
|
|
560
|
+
ip: stored.ip,
|
|
561
|
+
os: stored.os,
|
|
562
|
+
loginType: stored.loginType,
|
|
563
|
+
refreshTtlSeconds: this.normalizeRefreshTtlSeconds(stored.refreshTtlSeconds),
|
|
564
|
+
sessionCookie: stored.sessionCookie
|
|
565
|
+
};
|
|
566
|
+
const enrichedMetadata = this.enrichTokenMetadata(apiReq, metadata);
|
|
567
|
+
const access = this.server.jwtSign(this.buildTokenPayload(user, enrichedMetadata), conf.accessSecret, conf.accessExpiry);
|
|
556
568
|
if (!access.success || !access.token) {
|
|
557
569
|
throw new api_server_base_js_1.ApiError({ code: 500, message: access.error ?? 'Unable to sign access token' });
|
|
558
570
|
}
|
|
571
|
+
const updated = await updateToken.call(this.storage, {
|
|
572
|
+
refreshToken,
|
|
573
|
+
accessToken: access.token,
|
|
574
|
+
lastSeenAt: new Date()
|
|
575
|
+
});
|
|
576
|
+
if (!updated) {
|
|
577
|
+
throw new api_server_base_js_1.ApiError({ code: 500, message: 'Unable to persist refreshed access token' });
|
|
578
|
+
}
|
|
559
579
|
const cookiePrefs = this.mergeSessionPreferences({
|
|
560
580
|
sessionCookie: stored.sessionCookie,
|
|
561
581
|
refreshTtlSeconds: this.normalizeRefreshTtlSeconds(stored.refreshTtlSeconds)
|
|
@@ -589,6 +609,7 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
589
609
|
];
|
|
590
610
|
}
|
|
591
611
|
async postPasskeyChallenge(apiReq) {
|
|
612
|
+
await this.applyRateLimit(apiReq, 'passkey-challenge');
|
|
592
613
|
if (typeof this.storage.createPasskeyChallenge !== 'function') {
|
|
593
614
|
throw new api_server_base_js_1.ApiError({ code: 501, message: 'Passkey support is not configured' });
|
|
594
615
|
}
|
|
@@ -726,7 +747,8 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
726
747
|
async deleteImpersonation(apiReq) {
|
|
727
748
|
this.assertAuthReady();
|
|
728
749
|
const actor = await this.resolveActorContext(apiReq);
|
|
729
|
-
const
|
|
750
|
+
const query = (apiReq.req.query ?? {});
|
|
751
|
+
const metadata = this.buildImpersonationMetadata(query);
|
|
730
752
|
metadata.loginType = metadata.loginType ?? 'impersonation-end';
|
|
731
753
|
const tokens = await this.issueTokens(apiReq, actor.user, metadata);
|
|
732
754
|
const publicUser = this.storage.filterUser(actor.user);
|
|
@@ -798,6 +820,7 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
798
820
|
const state = toStringOrNull(body.state) ?? undefined;
|
|
799
821
|
const codeChallenge = toStringOrNull(body.codeChallenge) ?? undefined;
|
|
800
822
|
const codeChallengeMethod = toStringOrNull(body.codeChallengeMethod) ?? undefined;
|
|
823
|
+
const resolvedCodeChallengeMethod = this.resolvePkceChallengeMethod(codeChallengeMethod);
|
|
801
824
|
if (!clientId) {
|
|
802
825
|
throw new api_server_base_js_1.ApiError({ code: 400, message: 'clientId is required' });
|
|
803
826
|
}
|
|
@@ -817,7 +840,7 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
817
840
|
redirectUri,
|
|
818
841
|
scope: resolvedScope,
|
|
819
842
|
codeChallenge,
|
|
820
|
-
codeChallengeMethod:
|
|
843
|
+
codeChallengeMethod: resolvedCodeChallengeMethod,
|
|
821
844
|
expiresInSeconds: 300
|
|
822
845
|
});
|
|
823
846
|
const redirect = new URL(redirectUri);
|
|
@@ -828,6 +851,7 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
828
851
|
return [200, { code: codeRecord.code, redirectUri: redirect.toString(), state }];
|
|
829
852
|
}
|
|
830
853
|
async postOAuthToken(apiReq) {
|
|
854
|
+
await this.applyRateLimit(apiReq, 'oauth-token');
|
|
831
855
|
if (typeof this.storage.getClient !== 'function' || typeof this.storage.consumeAuthCode !== 'function') {
|
|
832
856
|
throw new api_server_base_js_1.ApiError({ code: 501, message: 'OAuth token storage is not configured' });
|
|
833
857
|
}
|
|
@@ -881,6 +905,9 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
881
905
|
}
|
|
882
906
|
}
|
|
883
907
|
else if (record.codeChallengeMethod === 'plain') {
|
|
908
|
+
if (!this.allowInsecurePkcePlain) {
|
|
909
|
+
throw new api_server_base_js_1.ApiError({ code: 400, message: 'PKCE plain is not permitted' });
|
|
910
|
+
}
|
|
884
911
|
if (codeVerifier !== record.codeChallenge) {
|
|
885
912
|
throw new api_server_base_js_1.ApiError({ code: 400, message: 'code_verifier does not match challenge' });
|
|
886
913
|
}
|
|
@@ -1001,14 +1028,11 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
1001
1028
|
if (!secretProvided) {
|
|
1002
1029
|
throw new api_server_base_js_1.ApiError({ code: 400, message: 'Client authentication is required' });
|
|
1003
1030
|
}
|
|
1004
|
-
|
|
1005
|
-
if (this.
|
|
1006
|
-
|
|
1007
|
-
valid = await verifySecret(client, clientSecret);
|
|
1008
|
-
}
|
|
1009
|
-
else {
|
|
1010
|
-
valid = client.clientSecret === clientSecret;
|
|
1031
|
+
const verifySecret = this.storage.verifyClientSecret;
|
|
1032
|
+
if (typeof verifySecret !== 'function' || !this.storageImplements('verifyClientSecret')) {
|
|
1033
|
+
throw new api_server_base_js_1.ApiError({ code: 501, message: 'OAuth client secret verification is not configured' });
|
|
1011
1034
|
}
|
|
1035
|
+
const valid = await verifySecret.call(this.storage, client, clientSecret);
|
|
1012
1036
|
if (!valid) {
|
|
1013
1037
|
throw new api_server_base_js_1.ApiError({ code: 401, message: 'Invalid client credentials' });
|
|
1014
1038
|
}
|
|
@@ -1084,6 +1108,24 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
1084
1108
|
storageImplementsAll(keys) {
|
|
1085
1109
|
return keys.every((key) => this.storageImplements(key));
|
|
1086
1110
|
}
|
|
1111
|
+
async applyRateLimit(apiReq, endpoint) {
|
|
1112
|
+
if (!this.rateLimitHook) {
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
await this.rateLimitHook({ apiReq, endpoint });
|
|
1116
|
+
}
|
|
1117
|
+
resolvePkceChallengeMethod(value) {
|
|
1118
|
+
if (value === 'S256') {
|
|
1119
|
+
return 'S256';
|
|
1120
|
+
}
|
|
1121
|
+
if (value === 'plain') {
|
|
1122
|
+
if (!this.allowInsecurePkcePlain) {
|
|
1123
|
+
throw new api_server_base_js_1.ApiError({ code: 400, message: 'PKCE plain is not permitted' });
|
|
1124
|
+
}
|
|
1125
|
+
return 'plain';
|
|
1126
|
+
}
|
|
1127
|
+
return undefined;
|
|
1128
|
+
}
|
|
1087
1129
|
defineRoutes() {
|
|
1088
1130
|
const routes = [];
|
|
1089
1131
|
const coreAuthSupported = this.storageImplementsAll([
|
|
@@ -1156,7 +1198,7 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
1156
1198
|
auth: { type: 'strict', req: 'any' }
|
|
1157
1199
|
}, {
|
|
1158
1200
|
method: 'delete',
|
|
1159
|
-
path: '/v1/passkeys/:credentialId
|
|
1201
|
+
path: '/v1/passkeys/:credentialId',
|
|
1160
1202
|
handler: (req) => this.deletePasskey(req),
|
|
1161
1203
|
auth: { type: 'strict', req: 'any' }
|
|
1162
1204
|
});
|
|
@@ -2,32 +2,11 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MemAuthStore = void 0;
|
|
4
4
|
const memory_js_1 = require("../oauth/memory.js");
|
|
5
|
+
const config_js_1 = require("../passkey/config.js");
|
|
5
6
|
const memory_js_2 = require("../passkey/memory.js");
|
|
6
7
|
const memory_js_3 = require("../token/memory.js");
|
|
7
8
|
const memory_js_4 = require("../user/memory.js");
|
|
8
9
|
const compat_auth_storage_js_1 = require("./compat-auth-storage.js");
|
|
9
|
-
const DEFAULT_PASSKEY_CONFIG = {
|
|
10
|
-
rpId: 'localhost',
|
|
11
|
-
rpName: 'API Server',
|
|
12
|
-
origins: ['http://localhost:5173'],
|
|
13
|
-
timeoutMs: 5 * 60 * 1000,
|
|
14
|
-
userVerification: 'preferred'
|
|
15
|
-
};
|
|
16
|
-
function isOriginString(origin) {
|
|
17
|
-
return typeof origin === 'string' && origin.trim().length > 0;
|
|
18
|
-
}
|
|
19
|
-
function normalizePasskeyConfig(config = {}) {
|
|
20
|
-
const candidateOrigins = Array.isArray(config.origins) && config.origins.length > 0 ? config.origins.filter(isOriginString) : null;
|
|
21
|
-
return {
|
|
22
|
-
rpId: config.rpId?.trim() || DEFAULT_PASSKEY_CONFIG.rpId,
|
|
23
|
-
rpName: config.rpName?.trim() || DEFAULT_PASSKEY_CONFIG.rpName,
|
|
24
|
-
origins: candidateOrigins ? candidateOrigins.map((origin) => origin.trim()) : DEFAULT_PASSKEY_CONFIG.origins,
|
|
25
|
-
timeoutMs: typeof config.timeoutMs === 'number' && config.timeoutMs > 0
|
|
26
|
-
? config.timeoutMs
|
|
27
|
-
: DEFAULT_PASSKEY_CONFIG.timeoutMs,
|
|
28
|
-
userVerification: config.userVerification ?? DEFAULT_PASSKEY_CONFIG.userVerification
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
10
|
class MemAuthStore {
|
|
32
11
|
constructor(params = {}) {
|
|
33
12
|
this.userStore = new memory_js_4.MemoryUserStore({
|
|
@@ -42,7 +21,7 @@ class MemAuthStore {
|
|
|
42
21
|
let passkeyStore;
|
|
43
22
|
let passkeyConfig;
|
|
44
23
|
if (params.passkeys !== false) {
|
|
45
|
-
passkeyConfig = normalizePasskeyConfig(params.passkeys ?? {});
|
|
24
|
+
passkeyConfig = (0, config_js_1.normalizePasskeyConfig)(params.passkeys ?? {});
|
|
46
25
|
const resolveUser = async (lookup) => {
|
|
47
26
|
const found = await this.userStore.findUser(lookup.userId ?? lookup.login ?? '');
|
|
48
27
|
if (!found) {
|
|
@@ -2,48 +2,21 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.SqlAuthStore = void 0;
|
|
4
4
|
const sequelize_js_1 = require("../oauth/sequelize.js");
|
|
5
|
+
const config_js_1 = require("../passkey/config.js");
|
|
5
6
|
const sequelize_js_2 = require("../passkey/sequelize.js");
|
|
7
|
+
const sequelize_utils_js_1 = require("../sequelize-utils.js");
|
|
6
8
|
const sequelize_js_3 = require("../token/sequelize.js");
|
|
7
9
|
const sequelize_js_4 = require("../user/sequelize.js");
|
|
8
10
|
const compat_auth_storage_js_1 = require("./compat-auth-storage.js");
|
|
9
|
-
const DEFAULT_PASSKEY_CONFIG = {
|
|
10
|
-
rpId: 'localhost',
|
|
11
|
-
rpName: 'API Server',
|
|
12
|
-
origins: ['http://localhost:5173'],
|
|
13
|
-
timeoutMs: 5 * 60 * 1000,
|
|
14
|
-
userVerification: 'preferred'
|
|
15
|
-
};
|
|
16
|
-
function normalizeTablePrefix(prefix) {
|
|
17
|
-
if (!prefix) {
|
|
18
|
-
return undefined;
|
|
19
|
-
}
|
|
20
|
-
const trimmed = prefix.trim();
|
|
21
|
-
return trimmed.length > 0 ? trimmed : undefined;
|
|
22
|
-
}
|
|
23
11
|
function resolveTablePrefix(...prefixes) {
|
|
24
12
|
for (const prefix of prefixes) {
|
|
25
|
-
const normalized = normalizeTablePrefix(prefix);
|
|
13
|
+
const normalized = (0, sequelize_utils_js_1.normalizeTablePrefix)(prefix);
|
|
26
14
|
if (normalized) {
|
|
27
15
|
return normalized;
|
|
28
16
|
}
|
|
29
17
|
}
|
|
30
18
|
return undefined;
|
|
31
19
|
}
|
|
32
|
-
function isOriginString(origin) {
|
|
33
|
-
return typeof origin === 'string' && origin.trim().length > 0;
|
|
34
|
-
}
|
|
35
|
-
function normalizePasskeyConfig(config = {}) {
|
|
36
|
-
const candidateOrigins = Array.isArray(config.origins) && config.origins.length > 0 ? config.origins.filter(isOriginString) : null;
|
|
37
|
-
return {
|
|
38
|
-
rpId: config.rpId?.trim() || DEFAULT_PASSKEY_CONFIG.rpId,
|
|
39
|
-
rpName: config.rpName?.trim() || DEFAULT_PASSKEY_CONFIG.rpName,
|
|
40
|
-
origins: candidateOrigins ? candidateOrigins.map((origin) => origin.trim()) : DEFAULT_PASSKEY_CONFIG.origins,
|
|
41
|
-
timeoutMs: typeof config.timeoutMs === 'number' && config.timeoutMs > 0
|
|
42
|
-
? config.timeoutMs
|
|
43
|
-
: DEFAULT_PASSKEY_CONFIG.timeoutMs,
|
|
44
|
-
userVerification: config.userVerification ?? DEFAULT_PASSKEY_CONFIG.userVerification
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
20
|
class SqlAuthStore {
|
|
48
21
|
constructor(params) {
|
|
49
22
|
this.closed = false;
|
|
@@ -83,7 +56,7 @@ class SqlAuthStore {
|
|
|
83
56
|
let passkeyConfig;
|
|
84
57
|
if (params.passkeys !== false) {
|
|
85
58
|
const passkeyTablePrefix = resolveTablePrefix(moduleTablePrefixes.passkey, params.tablePrefix);
|
|
86
|
-
passkeyConfig = normalizePasskeyConfig(params.passkeys ?? {});
|
|
59
|
+
passkeyConfig = (0, config_js_1.normalizePasskeyConfig)(params.passkeys ?? {});
|
|
87
60
|
const resolveUser = async (lookup) => {
|
|
88
61
|
const found = await this.userStore.findUser(lookup.userId ?? lookup.login ?? '');
|
|
89
62
|
if (!found) {
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AuthIdentifier } from './types.js';
|
|
2
|
+
export declare function normalizeComparableUserId(identifier: AuthIdentifier): string;
|
|
3
|
+
export declare function normalizeNumericUserId(identifier: AuthIdentifier): number;
|
|
4
|
+
export declare function normalizeStringUserId(identifier: AuthIdentifier): string;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeComparableUserId = normalizeComparableUserId;
|
|
4
|
+
exports.normalizeNumericUserId = normalizeNumericUserId;
|
|
5
|
+
exports.normalizeStringUserId = normalizeStringUserId;
|
|
6
|
+
function normalizeComparableUserId(identifier) {
|
|
7
|
+
if (typeof identifier === 'number' && Number.isFinite(identifier)) {
|
|
8
|
+
return String(identifier);
|
|
9
|
+
}
|
|
10
|
+
if (typeof identifier === 'string') {
|
|
11
|
+
const trimmed = identifier.trim();
|
|
12
|
+
if (trimmed.length === 0) {
|
|
13
|
+
throw new Error(`Unable to normalise user identifier: ${identifier}`);
|
|
14
|
+
}
|
|
15
|
+
if (/^\d+$/.test(trimmed)) {
|
|
16
|
+
return String(Number(trimmed));
|
|
17
|
+
}
|
|
18
|
+
return trimmed;
|
|
19
|
+
}
|
|
20
|
+
throw new Error(`Unable to normalise user identifier: ${identifier}`);
|
|
21
|
+
}
|
|
22
|
+
function normalizeNumericUserId(identifier) {
|
|
23
|
+
const normalized = normalizeComparableUserId(identifier);
|
|
24
|
+
if (/^\d+$/.test(normalized)) {
|
|
25
|
+
return Number(normalized);
|
|
26
|
+
}
|
|
27
|
+
throw new Error(`Unable to normalise user identifier: ${identifier}`);
|
|
28
|
+
}
|
|
29
|
+
function normalizeStringUserId(identifier) {
|
|
30
|
+
return normalizeComparableUserId(identifier);
|
|
31
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Request } from 'express';
|
|
2
|
+
import type { CookieOptions } from 'express-serve-static-core';
|
|
3
|
+
export interface AuthCookieConfig {
|
|
4
|
+
cookieSecure?: boolean | 'auto';
|
|
5
|
+
cookieSameSite?: 'lax' | 'strict' | 'none';
|
|
6
|
+
cookieHttpOnly?: boolean;
|
|
7
|
+
cookieDomain?: string;
|
|
8
|
+
cookiePath?: string;
|
|
9
|
+
devMode?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function buildAuthCookieOptions(config: AuthCookieConfig, req: Pick<Request, 'headers' | 'protocol'>): CookieOptions;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildAuthCookieOptions = buildAuthCookieOptions;
|
|
4
|
+
function firstHeaderValue(value) {
|
|
5
|
+
if (typeof value === 'string') {
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
if (Array.isArray(value)) {
|
|
9
|
+
return value[0] ?? '';
|
|
10
|
+
}
|
|
11
|
+
return '';
|
|
12
|
+
}
|
|
13
|
+
function resolveOriginHostname(origin) {
|
|
14
|
+
try {
|
|
15
|
+
const url = new URL(origin);
|
|
16
|
+
const hostname = url.hostname.trim().toLowerCase();
|
|
17
|
+
return hostname.length > 0 ? hostname : null;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function isLocalhostOrigin(origin) {
|
|
24
|
+
const hostname = resolveOriginHostname(origin);
|
|
25
|
+
if (!hostname) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
return hostname === 'localhost' || hostname.endsWith('.localhost');
|
|
29
|
+
}
|
|
30
|
+
function buildAuthCookieOptions(config, req) {
|
|
31
|
+
const forwardedProto = firstHeaderValue(req.headers['x-forwarded-proto']).split(',')[0].trim().toLowerCase();
|
|
32
|
+
const isHttps = forwardedProto === 'https' || req.protocol === 'https';
|
|
33
|
+
const origin = firstHeaderValue(req.headers.origin ?? req.headers.referer);
|
|
34
|
+
const secure = config.cookieSecure === true ? true : config.cookieSecure === false ? false : /* auto */ Boolean(isHttps);
|
|
35
|
+
let sameSite = config.cookieSameSite ?? 'lax';
|
|
36
|
+
if (sameSite !== 'lax' && sameSite !== 'strict' && sameSite !== 'none') {
|
|
37
|
+
sameSite = 'lax';
|
|
38
|
+
}
|
|
39
|
+
let resolvedSecure = secure;
|
|
40
|
+
if (sameSite === 'none' && resolvedSecure !== true) {
|
|
41
|
+
// Modern browsers reject SameSite=None cookies unless Secure is set.
|
|
42
|
+
resolvedSecure = true;
|
|
43
|
+
}
|
|
44
|
+
const options = {
|
|
45
|
+
httpOnly: config.cookieHttpOnly ?? true,
|
|
46
|
+
secure: resolvedSecure,
|
|
47
|
+
sameSite,
|
|
48
|
+
domain: config.cookieDomain || undefined,
|
|
49
|
+
path: config.cookiePath || '/',
|
|
50
|
+
maxAge: undefined
|
|
51
|
+
};
|
|
52
|
+
if (config.devMode && isLocalhostOrigin(origin)) {
|
|
53
|
+
// Domain cookies do not work on localhost; avoid breaking local development when cookieDomain is set.
|
|
54
|
+
options.domain = undefined;
|
|
55
|
+
}
|
|
56
|
+
return options;
|
|
57
|
+
}
|
package/dist/cjs/oauth/memory.js
CHANGED
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.MemoryOAuthStore = void 0;
|
|
7
7
|
const bcryptjs_1 = __importDefault(require("bcryptjs"));
|
|
8
|
+
const user_id_js_1 = require("../auth-api/user-id.js");
|
|
8
9
|
const base_js_1 = require("./base.js");
|
|
9
10
|
function cloneClient(client) {
|
|
10
11
|
if (!client) {
|
|
@@ -12,7 +13,8 @@ function cloneClient(client) {
|
|
|
12
13
|
}
|
|
13
14
|
return {
|
|
14
15
|
clientId: client.clientId,
|
|
15
|
-
clientSecret
|
|
16
|
+
// clientSecret is stored hashed; do not return the hash.
|
|
17
|
+
clientSecret: client.clientSecret ? '__stored__' : undefined,
|
|
16
18
|
name: client.name,
|
|
17
19
|
redirectUris: [...client.redirectUris],
|
|
18
20
|
scope: client.scope ? [...client.scope] : undefined,
|
|
@@ -28,15 +30,7 @@ function cloneCode(code) {
|
|
|
28
30
|
metadata: code.metadata ? { ...code.metadata } : undefined
|
|
29
31
|
};
|
|
30
32
|
}
|
|
31
|
-
|
|
32
|
-
if (typeof identifier === 'number' && Number.isFinite(identifier)) {
|
|
33
|
-
return identifier;
|
|
34
|
-
}
|
|
35
|
-
if (typeof identifier === 'string' && /^\d+$/.test(identifier)) {
|
|
36
|
-
return Number(identifier);
|
|
37
|
-
}
|
|
38
|
-
throw new Error(`Unable to normalise user identifier: ${identifier}`);
|
|
39
|
-
}
|
|
33
|
+
const normalizeUserId = user_id_js_1.normalizeNumericUserId;
|
|
40
34
|
class MemoryOAuthStore extends base_js_1.OAuthStore {
|
|
41
35
|
constructor(options = {}) {
|
|
42
36
|
super();
|
package/dist/cjs/oauth/models.js
CHANGED
|
@@ -4,27 +4,16 @@ exports.OAuthCodeModel = exports.OAuthClientModel = void 0;
|
|
|
4
4
|
exports.initOAuthClientModel = initOAuthClientModel;
|
|
5
5
|
exports.initOAuthCodeModel = initOAuthCodeModel;
|
|
6
6
|
const sequelize_1 = require("sequelize");
|
|
7
|
-
const
|
|
8
|
-
function normalizeTablePrefix(prefix) {
|
|
9
|
-
if (!prefix) {
|
|
10
|
-
return undefined;
|
|
11
|
-
}
|
|
12
|
-
const trimmed = prefix.trim();
|
|
13
|
-
return trimmed.length > 0 ? trimmed : undefined;
|
|
14
|
-
}
|
|
15
|
-
function applyTablePrefix(prefix, tableName) {
|
|
16
|
-
const normalized = normalizeTablePrefix(prefix);
|
|
17
|
-
return normalized ? `${normalized}${tableName}` : tableName;
|
|
18
|
-
}
|
|
7
|
+
const sequelize_utils_js_1 = require("../sequelize-utils.js");
|
|
19
8
|
function integerIdType(sequelize) {
|
|
20
|
-
return DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect()) ? sequelize_1.DataTypes.INTEGER.UNSIGNED : sequelize_1.DataTypes.INTEGER;
|
|
9
|
+
return sequelize_utils_js_1.DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect()) ? sequelize_1.DataTypes.INTEGER.UNSIGNED : sequelize_1.DataTypes.INTEGER;
|
|
21
10
|
}
|
|
22
11
|
function tableOptions(sequelize, tableName, tablePrefix, extra) {
|
|
23
|
-
const opts = { sequelize, tableName: applyTablePrefix(tablePrefix, tableName) };
|
|
12
|
+
const opts = { sequelize, tableName: (0, sequelize_utils_js_1.applyTablePrefix)(tablePrefix, tableName) };
|
|
24
13
|
if (extra) {
|
|
25
14
|
Object.assign(opts, extra);
|
|
26
15
|
}
|
|
27
|
-
if (DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect())) {
|
|
16
|
+
if (sequelize_utils_js_1.DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect())) {
|
|
28
17
|
opts.charset = 'utf8mb4';
|
|
29
18
|
opts.collate = 'utf8mb4_unicode_ci';
|
|
30
19
|
}
|