@technomoron/api-server-base 2.0.0-beta.2 → 2.0.0-beta.20
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 +81 -28
- package/dist/cjs/api-module.cjs +9 -0
- package/dist/cjs/api-module.d.ts +7 -4
- package/dist/cjs/api-server-base.cjs +607 -99
- package/dist/cjs/api-server-base.d.ts +80 -23
- package/dist/cjs/auth-api/auth-module.d.ts +23 -3
- package/dist/cjs/auth-api/auth-module.js +320 -124
- package/dist/cjs/auth-api/compat-auth-storage.d.ts +7 -5
- package/dist/cjs/auth-api/compat-auth-storage.js +15 -3
- package/dist/cjs/auth-api/mem-auth-store.d.ts +5 -3
- package/dist/cjs/auth-api/mem-auth-store.js +14 -28
- package/dist/cjs/auth-api/module.d.ts +1 -1
- package/dist/cjs/auth-api/sql-auth-store.d.ts +16 -4
- package/dist/cjs/auth-api/sql-auth-store.js +43 -30
- package/dist/cjs/auth-api/storage.d.ts +6 -4
- package/dist/cjs/auth-api/storage.js +15 -5
- package/dist/cjs/auth-api/types.d.ts +7 -2
- package/dist/cjs/auth-api/user-id.d.ts +5 -0
- package/dist/cjs/auth-api/user-id.js +38 -0
- package/dist/cjs/auth-cookie-options.d.ts +11 -0
- package/dist/cjs/auth-cookie-options.js +66 -0
- package/dist/cjs/index.cjs +4 -14
- package/dist/cjs/index.d.ts +4 -9
- package/dist/cjs/oauth/memory.d.ts +6 -0
- package/dist/cjs/oauth/memory.js +44 -11
- package/dist/cjs/oauth/models.d.ts +7 -2
- package/dist/cjs/oauth/models.js +10 -21
- package/dist/cjs/oauth/sequelize.d.ts +10 -48
- package/dist/cjs/oauth/sequelize.js +44 -99
- package/dist/cjs/oauth/types.d.ts +1 -0
- package/dist/cjs/passkey/base.d.ts +2 -0
- package/dist/cjs/passkey/config.d.ts +2 -0
- package/dist/cjs/passkey/config.js +26 -0
- package/dist/cjs/passkey/memory.d.ts +8 -0
- package/dist/cjs/passkey/memory.js +57 -16
- package/dist/cjs/passkey/models.d.ts +13 -4
- package/dist/cjs/passkey/models.js +41 -14
- package/dist/cjs/passkey/sequelize.d.ts +13 -25
- package/dist/cjs/passkey/sequelize.js +68 -153
- package/dist/cjs/passkey/service.d.ts +6 -2
- package/dist/cjs/passkey/service.js +205 -27
- package/dist/cjs/passkey/types.d.ts +18 -9
- package/dist/cjs/sequelize-utils.d.ts +8 -0
- package/dist/cjs/sequelize-utils.js +57 -0
- package/dist/cjs/token/base.d.ts +2 -1
- package/dist/cjs/token/base.js +3 -1
- package/dist/cjs/token/memory.d.ts +10 -0
- package/dist/cjs/token/memory.js +122 -32
- package/dist/cjs/token/sequelize.d.ts +4 -4
- package/dist/cjs/token/sequelize.js +67 -85
- package/dist/cjs/token/types.d.ts +8 -1
- package/dist/cjs/user/base.d.ts +1 -0
- package/dist/cjs/user/base.js +11 -4
- package/dist/cjs/user/memory.d.ts +2 -0
- package/dist/cjs/user/memory.js +9 -10
- package/dist/cjs/user/sequelize.d.ts +7 -2
- package/dist/cjs/user/sequelize.js +19 -32
- package/dist/esm/api-module.d.ts +7 -4
- package/dist/esm/api-module.js +9 -0
- package/dist/esm/api-server-base.d.ts +80 -23
- package/dist/esm/api-server-base.js +608 -100
- package/dist/esm/auth-api/auth-module.d.ts +23 -3
- package/dist/esm/auth-api/auth-module.js +321 -125
- package/dist/esm/auth-api/compat-auth-storage.d.ts +7 -5
- package/dist/esm/auth-api/compat-auth-storage.js +13 -1
- package/dist/esm/auth-api/mem-auth-store.d.ts +5 -3
- package/dist/esm/auth-api/mem-auth-store.js +14 -28
- package/dist/esm/auth-api/module.d.ts +1 -1
- package/dist/esm/auth-api/sql-auth-store.d.ts +16 -4
- package/dist/esm/auth-api/sql-auth-store.js +43 -30
- package/dist/esm/auth-api/storage.d.ts +6 -4
- package/dist/esm/auth-api/storage.js +13 -3
- package/dist/esm/auth-api/types.d.ts +7 -2
- package/dist/esm/auth-api/user-id.d.ts +5 -0
- package/dist/esm/auth-api/user-id.js +32 -0
- package/dist/esm/auth-cookie-options.d.ts +11 -0
- package/dist/esm/auth-cookie-options.js +63 -0
- package/dist/esm/index.d.ts +4 -9
- package/dist/esm/index.js +2 -7
- package/dist/esm/oauth/memory.d.ts +6 -0
- package/dist/esm/oauth/memory.js +44 -11
- package/dist/esm/oauth/models.d.ts +7 -2
- package/dist/esm/oauth/models.js +6 -19
- package/dist/esm/oauth/sequelize.d.ts +10 -48
- package/dist/esm/oauth/sequelize.js +32 -87
- package/dist/esm/oauth/types.d.ts +1 -0
- package/dist/esm/passkey/base.d.ts +2 -0
- package/dist/esm/passkey/config.d.ts +2 -0
- package/dist/esm/passkey/config.js +23 -0
- package/dist/esm/passkey/memory.d.ts +8 -0
- package/dist/esm/passkey/memory.js +57 -16
- package/dist/esm/passkey/models.d.ts +13 -4
- package/dist/esm/passkey/models.js +39 -12
- package/dist/esm/passkey/sequelize.d.ts +13 -25
- package/dist/esm/passkey/sequelize.js +69 -154
- package/dist/esm/passkey/service.d.ts +6 -2
- package/dist/esm/passkey/service.js +173 -28
- package/dist/esm/passkey/types.d.ts +18 -9
- package/dist/esm/sequelize-utils.d.ts +8 -0
- package/dist/esm/sequelize-utils.js +48 -0
- package/dist/esm/token/base.d.ts +2 -1
- package/dist/esm/token/base.js +3 -1
- package/dist/esm/token/memory.d.ts +10 -0
- package/dist/esm/token/memory.js +122 -32
- package/dist/esm/token/sequelize.d.ts +4 -4
- package/dist/esm/token/sequelize.js +67 -85
- package/dist/esm/token/types.d.ts +8 -1
- package/dist/esm/user/base.d.ts +1 -0
- package/dist/esm/user/base.js +11 -4
- package/dist/esm/user/memory.d.ts +2 -0
- package/dist/esm/user/memory.js +9 -10
- package/dist/esm/user/sequelize.d.ts +7 -2
- package/dist/esm/user/sequelize.js +19 -32
- package/docs/swagger/openapi.json +1876 -0
- package/package.json +81 -32
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const node_crypto_1 = require("node:crypto");
|
|
4
|
+
const helpers_1 = require("@simplewebauthn/server/helpers");
|
|
4
5
|
const api_server_base_js_1 = require("../api-server-base.js");
|
|
6
|
+
const auth_cookie_options_js_1 = require("../auth-cookie-options.js");
|
|
5
7
|
const module_js_1 = require("./module.js");
|
|
8
|
+
const storage_js_1 = require("./storage.js");
|
|
6
9
|
function isAuthIdentifier(value) {
|
|
7
10
|
return typeof value === 'string' || typeof value === 'number';
|
|
8
11
|
}
|
|
@@ -30,10 +33,18 @@ function sha256Base64Url(value) {
|
|
|
30
33
|
return base64UrlEncode(hash);
|
|
31
34
|
}
|
|
32
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
|
+
}
|
|
33
42
|
constructor(options = {}) {
|
|
34
43
|
super({ namespace: options.namespace ?? AuthModule.defaultNamespace });
|
|
35
44
|
this.defaultDomain = options.defaultDomain;
|
|
36
45
|
this.canImpersonateHook = options.canImpersonate;
|
|
46
|
+
this.rateLimitHook = options.rateLimit;
|
|
47
|
+
this.allowInsecurePkcePlain = options.allowInsecurePkcePlain ?? true;
|
|
37
48
|
}
|
|
38
49
|
get storage() {
|
|
39
50
|
return this.server.getAuthStorage();
|
|
@@ -74,9 +85,21 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
74
85
|
}
|
|
75
86
|
buildTokenMetadata(metadata = {}) {
|
|
76
87
|
const scope = metadata.scope;
|
|
88
|
+
const domain = metadata.domain ?? this.defaultDomain ?? '';
|
|
89
|
+
let fingerprint = metadata.fingerprint ?? metadata.clientId ?? '';
|
|
90
|
+
if (typeof fingerprint === 'string') {
|
|
91
|
+
fingerprint = fingerprint.trim();
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
fingerprint = '';
|
|
95
|
+
}
|
|
96
|
+
// Avoid every client sharing the empty-string fingerprint which collapses sessions into one bucket.
|
|
97
|
+
if (!fingerprint) {
|
|
98
|
+
fingerprint = `srv-${(0, node_crypto_1.randomUUID)()}`;
|
|
99
|
+
}
|
|
77
100
|
return {
|
|
78
|
-
domain
|
|
79
|
-
fingerprint
|
|
101
|
+
domain,
|
|
102
|
+
fingerprint,
|
|
80
103
|
label: metadata.label ?? (Array.isArray(scope) ? scope.join(' ') : typeof scope === 'string' ? scope : ''),
|
|
81
104
|
clientId: metadata.clientId,
|
|
82
105
|
ruid: metadata.ruid,
|
|
@@ -134,6 +157,9 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
134
157
|
return candidate ? {} : { sessionCookie: true, refreshTtlSeconds: this.sessionRefreshTtlSeconds() };
|
|
135
158
|
}
|
|
136
159
|
if (typeof candidate === 'number') {
|
|
160
|
+
if (candidate === 0) {
|
|
161
|
+
return { sessionCookie: true, refreshTtlSeconds: this.sessionRefreshTtlSeconds() };
|
|
162
|
+
}
|
|
137
163
|
const ttl = this.normalizeRefreshTtlSeconds(candidate);
|
|
138
164
|
return ttl ? { sessionCookie: false, refreshTtlSeconds: ttl } : {};
|
|
139
165
|
}
|
|
@@ -183,30 +209,46 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
183
209
|
}
|
|
184
210
|
return prefs;
|
|
185
211
|
}
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
212
|
+
validateCredentialId(apiReq) {
|
|
213
|
+
const paramId = toStringOrNull(apiReq.req.params?.credentialId);
|
|
214
|
+
const bodyId = toStringOrNull(apiReq.req.body?.credentialId);
|
|
215
|
+
const credentialId = paramId ?? bodyId;
|
|
216
|
+
if (!credentialId) {
|
|
217
|
+
throw new api_server_base_js_1.ApiError({ code: 400, message: 'credentialId is required' });
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
helpers_1.isoBase64URL.toBuffer(credentialId);
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
throw new api_server_base_js_1.ApiError({ code: 400, message: 'Invalid credentialId' });
|
|
224
|
+
}
|
|
225
|
+
return credentialId;
|
|
226
|
+
}
|
|
227
|
+
normalizeCredentialId(value) {
|
|
228
|
+
if (Buffer.isBuffer(value)) {
|
|
229
|
+
return value;
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
return Buffer.from(helpers_1.isoBase64URL.toBuffer(value));
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
try {
|
|
236
|
+
return Buffer.from(value, 'base64');
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return Buffer.from(value);
|
|
207
240
|
}
|
|
208
241
|
}
|
|
209
|
-
|
|
242
|
+
}
|
|
243
|
+
toIsoDate(value) {
|
|
244
|
+
if (!value) {
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
248
|
+
return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
|
|
249
|
+
}
|
|
250
|
+
cookieOptions(apiReq) {
|
|
251
|
+
return (0, auth_cookie_options_js_1.buildAuthCookieOptions)(this.server.config, apiReq.req);
|
|
210
252
|
}
|
|
211
253
|
setJwtCookies(apiReq, tokens, preferences = {}) {
|
|
212
254
|
const conf = this.server.config;
|
|
@@ -233,7 +275,10 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
233
275
|
async issueTokens(apiReq, user, metadata = {}) {
|
|
234
276
|
const conf = this.server.config;
|
|
235
277
|
const enrichedMetadata = this.enrichTokenMetadata(apiReq, metadata);
|
|
236
|
-
const payload =
|
|
278
|
+
const payload = {
|
|
279
|
+
...this.buildTokenPayload(user, enrichedMetadata),
|
|
280
|
+
jti: (0, node_crypto_1.randomUUID)()
|
|
281
|
+
};
|
|
237
282
|
const access = this.server.jwtSign(payload, conf.accessSecret, conf.accessExpiry);
|
|
238
283
|
if (!access.success || !access.token) {
|
|
239
284
|
throw new api_server_base_js_1.ApiError({ code: 500, message: access.error ?? 'Unable to sign access token' });
|
|
@@ -403,23 +448,16 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
403
448
|
return undefined;
|
|
404
449
|
}
|
|
405
450
|
async postLogin(apiReq) {
|
|
451
|
+
await this.applyRateLimit(apiReq, 'login');
|
|
406
452
|
this.assertAuthReady();
|
|
407
453
|
const { login, password, ...metadata } = this.parseLoginBody(apiReq);
|
|
408
454
|
const user = await this.storage.getUser(login);
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
message: 'Invalid credentials',
|
|
413
|
-
errors: { login: 'Unknown user' }
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
const hash = this.storage.getUserPasswordHash(user);
|
|
417
|
-
const verified = await this.storage.verifyPassword(password, hash);
|
|
418
|
-
if (!verified) {
|
|
455
|
+
const hash = user ? this.storage.getUserPasswordHash(user) : '';
|
|
456
|
+
const verified = user ? await this.storage.verifyPassword(password, hash) : false;
|
|
457
|
+
if (!user || !verified) {
|
|
419
458
|
throw new api_server_base_js_1.ApiError({
|
|
420
459
|
code: 400,
|
|
421
|
-
message: 'Invalid credentials'
|
|
422
|
-
errors: { password: 'Wrong password' }
|
|
460
|
+
message: 'Invalid credentials'
|
|
423
461
|
});
|
|
424
462
|
}
|
|
425
463
|
const pair = await this.issueTokens(apiReq, user, metadata);
|
|
@@ -461,6 +499,7 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
461
499
|
refreshTtlSeconds: sessionPrefs.refreshTtlSeconds ?? stored.refreshTtlSeconds,
|
|
462
500
|
sessionCookie: sessionPrefs.sessionCookie ?? stored.sessionCookie
|
|
463
501
|
};
|
|
502
|
+
await this.storage.deleteToken({ refreshToken: providedToken });
|
|
464
503
|
const pair = await this.issueTokens(apiReq, user, metadata);
|
|
465
504
|
const publicUser = this.storage.filterUser(user);
|
|
466
505
|
return [200, { ...pair, user: publicUser }];
|
|
@@ -500,10 +539,39 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
500
539
|
apiReq.req.cookies[conf.accessCookie].trim().length > 0);
|
|
501
540
|
const shouldRefresh = Boolean(body.refresh) || !hasAccessToken;
|
|
502
541
|
if (shouldRefresh) {
|
|
503
|
-
const
|
|
542
|
+
const updateToken = this.storage.updateToken;
|
|
543
|
+
if (typeof updateToken !== 'function' || !this.storageImplements('updateToken')) {
|
|
544
|
+
throw new api_server_base_js_1.ApiError({ code: 501, message: 'Token update storage is not configured' });
|
|
545
|
+
}
|
|
546
|
+
// Sign a new access token without embedding stored token secrets into the JWT payload.
|
|
547
|
+
const metadata = {
|
|
548
|
+
ruid: stored.ruid,
|
|
549
|
+
domain: stored.domain,
|
|
550
|
+
fingerprint: stored.fingerprint,
|
|
551
|
+
label: stored.label,
|
|
552
|
+
clientId: stored.clientId,
|
|
553
|
+
scope: stored.scope,
|
|
554
|
+
browser: stored.browser,
|
|
555
|
+
device: stored.device,
|
|
556
|
+
ip: stored.ip,
|
|
557
|
+
os: stored.os,
|
|
558
|
+
loginType: stored.loginType,
|
|
559
|
+
refreshTtlSeconds: this.normalizeRefreshTtlSeconds(stored.refreshTtlSeconds),
|
|
560
|
+
sessionCookie: stored.sessionCookie
|
|
561
|
+
};
|
|
562
|
+
const enrichedMetadata = this.enrichTokenMetadata(apiReq, metadata);
|
|
563
|
+
const access = this.server.jwtSign(this.buildTokenPayload(user, enrichedMetadata), conf.accessSecret, conf.accessExpiry);
|
|
504
564
|
if (!access.success || !access.token) {
|
|
505
565
|
throw new api_server_base_js_1.ApiError({ code: 500, message: access.error ?? 'Unable to sign access token' });
|
|
506
566
|
}
|
|
567
|
+
const updated = await updateToken.call(this.storage, {
|
|
568
|
+
refreshToken,
|
|
569
|
+
accessToken: access.token,
|
|
570
|
+
lastSeenAt: new Date()
|
|
571
|
+
});
|
|
572
|
+
if (!updated) {
|
|
573
|
+
throw new api_server_base_js_1.ApiError({ code: 500, message: 'Unable to persist refreshed access token' });
|
|
574
|
+
}
|
|
507
575
|
const cookiePrefs = this.mergeSessionPreferences({
|
|
508
576
|
sessionCookie: stored.sessionCookie,
|
|
509
577
|
refreshTtlSeconds: this.normalizeRefreshTtlSeconds(stored.refreshTtlSeconds)
|
|
@@ -513,9 +581,31 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
513
581
|
: cookiePrefs.refreshTtlSeconds;
|
|
514
582
|
this.setJwtCookies(apiReq, { accessToken: access.token, refreshToken }, { sessionCookie: cookiePrefs.sessionCookie ?? false, refreshTtlSeconds: refreshTtlForCookie });
|
|
515
583
|
}
|
|
516
|
-
|
|
584
|
+
const tokenClaims = verify.data;
|
|
585
|
+
const effectiveUserId = this.storage.getUserId(user);
|
|
586
|
+
const effectiveId = String(effectiveUserId);
|
|
587
|
+
const rawRealId = stored.ruid ?? tokenClaims.ruid;
|
|
588
|
+
const normalizedRealId = rawRealId === undefined || rawRealId === null ? null : String(rawRealId).trim() || null;
|
|
589
|
+
const isImpersonating = normalizedRealId !== null && normalizedRealId !== effectiveId;
|
|
590
|
+
let realUser;
|
|
591
|
+
let realUserId;
|
|
592
|
+
if (isImpersonating && normalizedRealId !== null) {
|
|
593
|
+
const realUserEntity = await this.getUserOrThrow(normalizedRealId, 'Real user not found');
|
|
594
|
+
realUser = this.storage.filterUser(realUserEntity);
|
|
595
|
+
realUserId = this.storage.getUserId(realUserEntity);
|
|
596
|
+
}
|
|
597
|
+
return [
|
|
598
|
+
200,
|
|
599
|
+
{
|
|
600
|
+
user: this.storage.filterUser(user),
|
|
601
|
+
isImpersonating,
|
|
602
|
+
realUser,
|
|
603
|
+
realUserId
|
|
604
|
+
}
|
|
605
|
+
];
|
|
517
606
|
}
|
|
518
607
|
async postPasskeyChallenge(apiReq) {
|
|
608
|
+
await this.applyRateLimit(apiReq, 'passkey-challenge');
|
|
519
609
|
if (typeof this.storage.createPasskeyChallenge !== 'function') {
|
|
520
610
|
throw new api_server_base_js_1.ApiError({ code: 501, message: 'Passkey support is not configured' });
|
|
521
611
|
}
|
|
@@ -527,15 +617,7 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
527
617
|
const params = {
|
|
528
618
|
action,
|
|
529
619
|
login: toStringOrNull(body.login) ?? undefined,
|
|
530
|
-
userId: isAuthIdentifier(body.userId) ? body.userId : undefined
|
|
531
|
-
userAgent: toStringOrNull(body.userAgent) ?? undefined,
|
|
532
|
-
domain: toStringOrNull(body.domain) ?? undefined,
|
|
533
|
-
fingerprint: toStringOrNull(body.fingerprint) ?? undefined,
|
|
534
|
-
label: toStringOrNull(body.label) ?? undefined,
|
|
535
|
-
browser: toStringOrNull(body.browser) ?? undefined,
|
|
536
|
-
device: toStringOrNull(body.device) ?? undefined,
|
|
537
|
-
ip: toStringOrNull(body.ip) ?? undefined,
|
|
538
|
-
os: toStringOrNull(body.os) ?? undefined
|
|
620
|
+
userId: isAuthIdentifier(body.userId) ? body.userId : undefined
|
|
539
621
|
};
|
|
540
622
|
const challenge = await this.storage.createPasskeyChallenge(params);
|
|
541
623
|
return [200, challenge];
|
|
@@ -551,18 +633,25 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
551
633
|
if (!expectedChallenge || typeof response !== 'object' || response === null) {
|
|
552
634
|
throw new api_server_base_js_1.ApiError({ code: 400, message: 'Malformed passkey verification payload' });
|
|
553
635
|
}
|
|
554
|
-
const
|
|
555
|
-
expectedChallenge,
|
|
556
|
-
response: response,
|
|
557
|
-
login: toStringOrNull(body.login) ?? undefined,
|
|
558
|
-
userId: isAuthIdentifier(body.userId) ? body.userId : undefined,
|
|
636
|
+
const rawMetadata = {
|
|
559
637
|
domain: toStringOrNull(body.domain) ?? undefined,
|
|
560
638
|
fingerprint: toStringOrNull(body.fingerprint) ?? undefined,
|
|
561
639
|
label: toStringOrNull(body.label) ?? undefined,
|
|
562
640
|
browser: toStringOrNull(body.browser) ?? undefined,
|
|
563
641
|
device: toStringOrNull(body.device) ?? undefined,
|
|
564
642
|
ip: toStringOrNull(body.ip) ?? undefined,
|
|
565
|
-
os: toStringOrNull(body.os) ?? undefined
|
|
643
|
+
os: toStringOrNull(body.os) ?? undefined
|
|
644
|
+
};
|
|
645
|
+
const clientInfo = apiReq.getClientInfo();
|
|
646
|
+
const userAgent = toStringOrNull(body.userAgent) ?? (clientInfo.ua ? clientInfo.ua : null);
|
|
647
|
+
const requestMetadata = this.enrichTokenMetadata(apiReq, rawMetadata);
|
|
648
|
+
const params = {
|
|
649
|
+
expectedChallenge,
|
|
650
|
+
response: response,
|
|
651
|
+
login: toStringOrNull(body.login) ?? undefined,
|
|
652
|
+
userId: isAuthIdentifier(body.userId) ? body.userId : undefined,
|
|
653
|
+
userAgent: userAgent ?? undefined,
|
|
654
|
+
...requestMetadata,
|
|
566
655
|
...sessionPrefs
|
|
567
656
|
};
|
|
568
657
|
const result = await this.storage.verifyPasskeyResponse(params);
|
|
@@ -598,6 +687,44 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
598
687
|
const publicUser = this.storage.filterUser(user);
|
|
599
688
|
return [200, { ...tokens, user: publicUser }];
|
|
600
689
|
}
|
|
690
|
+
async getPasskeys(apiReq) {
|
|
691
|
+
if (typeof this.storage.listUserCredentials !== 'function') {
|
|
692
|
+
throw new api_server_base_js_1.ApiError({ code: 501, message: 'Passkey credential listing is not configured' });
|
|
693
|
+
}
|
|
694
|
+
const { userId } = await this.resolveActorContext(apiReq);
|
|
695
|
+
const credentials = await this.storage.listUserCredentials(userId);
|
|
696
|
+
const safeCredentials = credentials.map((credential) => {
|
|
697
|
+
const bufferId = this.normalizeCredentialId(credential.credentialId);
|
|
698
|
+
return {
|
|
699
|
+
id: helpers_1.isoBase64URL.fromBuffer(new Uint8Array(bufferId)),
|
|
700
|
+
transports: credential.transports,
|
|
701
|
+
backedUp: credential.backedUp,
|
|
702
|
+
deviceType: credential.deviceType,
|
|
703
|
+
createdAt: this.toIsoDate(credential.createdAt),
|
|
704
|
+
updatedAt: this.toIsoDate(credential.updatedAt)
|
|
705
|
+
};
|
|
706
|
+
});
|
|
707
|
+
return [200, { credentials: safeCredentials }];
|
|
708
|
+
}
|
|
709
|
+
async deletePasskey(apiReq) {
|
|
710
|
+
if (typeof this.storage.listUserCredentials !== 'function' ||
|
|
711
|
+
typeof this.storage.deletePasskeyCredential !== 'function') {
|
|
712
|
+
throw new api_server_base_js_1.ApiError({ code: 501, message: 'Passkey credential management is not configured' });
|
|
713
|
+
}
|
|
714
|
+
const { userId } = await this.resolveActorContext(apiReq);
|
|
715
|
+
const credentialId = this.validateCredentialId(apiReq);
|
|
716
|
+
const bufferId = Buffer.from(helpers_1.isoBase64URL.toBuffer(credentialId));
|
|
717
|
+
const credentials = await this.storage.listUserCredentials(userId);
|
|
718
|
+
const owns = credentials.some((credential) => {
|
|
719
|
+
const candidateId = this.normalizeCredentialId(credential.credentialId);
|
|
720
|
+
return helpers_1.isoBase64URL.fromBuffer(new Uint8Array(candidateId)) === credentialId;
|
|
721
|
+
});
|
|
722
|
+
if (!owns) {
|
|
723
|
+
throw new api_server_base_js_1.ApiError({ code: 404, message: 'Passkey not found' });
|
|
724
|
+
}
|
|
725
|
+
const deleted = await this.storage.deletePasskeyCredential(bufferId);
|
|
726
|
+
return [200, { deleted }];
|
|
727
|
+
}
|
|
601
728
|
async postImpersonation(apiReq) {
|
|
602
729
|
this.assertAuthReady();
|
|
603
730
|
const { targetIdentifier, metadata } = this.parseImpersonationRequest(apiReq);
|
|
@@ -616,7 +743,8 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
616
743
|
async deleteImpersonation(apiReq) {
|
|
617
744
|
this.assertAuthReady();
|
|
618
745
|
const actor = await this.resolveActorContext(apiReq);
|
|
619
|
-
const
|
|
746
|
+
const query = (apiReq.req.query ?? {});
|
|
747
|
+
const metadata = this.buildImpersonationMetadata(query);
|
|
620
748
|
metadata.loginType = metadata.loginType ?? 'impersonation-end';
|
|
621
749
|
const tokens = await this.issueTokens(apiReq, actor.user, metadata);
|
|
622
750
|
const publicUser = this.storage.filterUser(actor.user);
|
|
@@ -688,6 +816,7 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
688
816
|
const state = toStringOrNull(body.state) ?? undefined;
|
|
689
817
|
const codeChallenge = toStringOrNull(body.codeChallenge) ?? undefined;
|
|
690
818
|
const codeChallengeMethod = toStringOrNull(body.codeChallengeMethod) ?? undefined;
|
|
819
|
+
const resolvedCodeChallengeMethod = this.resolvePkceChallengeMethod(codeChallengeMethod);
|
|
691
820
|
if (!clientId) {
|
|
692
821
|
throw new api_server_base_js_1.ApiError({ code: 400, message: 'clientId is required' });
|
|
693
822
|
}
|
|
@@ -707,7 +836,7 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
707
836
|
redirectUri,
|
|
708
837
|
scope: resolvedScope,
|
|
709
838
|
codeChallenge,
|
|
710
|
-
codeChallengeMethod:
|
|
839
|
+
codeChallengeMethod: resolvedCodeChallengeMethod,
|
|
711
840
|
expiresInSeconds: 300
|
|
712
841
|
});
|
|
713
842
|
const redirect = new URL(redirectUri);
|
|
@@ -718,6 +847,7 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
718
847
|
return [200, { code: codeRecord.code, redirectUri: redirect.toString(), state }];
|
|
719
848
|
}
|
|
720
849
|
async postOAuthToken(apiReq) {
|
|
850
|
+
await this.applyRateLimit(apiReq, 'oauth-token');
|
|
721
851
|
if (typeof this.storage.getClient !== 'function' || typeof this.storage.consumeAuthCode !== 'function') {
|
|
722
852
|
throw new api_server_base_js_1.ApiError({ code: 501, message: 'OAuth token storage is not configured' });
|
|
723
853
|
}
|
|
@@ -771,12 +901,15 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
771
901
|
}
|
|
772
902
|
}
|
|
773
903
|
else if (record.codeChallengeMethod === 'plain') {
|
|
904
|
+
if (!this.allowInsecurePkcePlain) {
|
|
905
|
+
throw new api_server_base_js_1.ApiError({ code: 400, message: 'PKCE plain is not permitted' });
|
|
906
|
+
}
|
|
774
907
|
if (codeVerifier !== record.codeChallenge) {
|
|
775
908
|
throw new api_server_base_js_1.ApiError({ code: 400, message: 'code_verifier does not match challenge' });
|
|
776
909
|
}
|
|
777
910
|
}
|
|
778
911
|
}
|
|
779
|
-
else if (!clientSecretProvided && client.clientSecret) {
|
|
912
|
+
else if (!clientSecretProvided && (client.hasSecret ?? Boolean(client.clientSecret))) {
|
|
780
913
|
throw new api_server_base_js_1.ApiError({ code: 400, message: 'Client authentication required when no PKCE challenge present' });
|
|
781
914
|
}
|
|
782
915
|
const user = await this.getUserOrThrow(record.userId, 'User not found');
|
|
@@ -811,6 +944,7 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
811
944
|
throw new api_server_base_js_1.ApiError({ code: 400, message: 'Refresh token issued to another client' });
|
|
812
945
|
}
|
|
813
946
|
const user = await this.getUserOrThrow(stored.userId ?? verify.data.uid, 'User not found');
|
|
947
|
+
await this.storage.deleteToken({ refreshToken });
|
|
814
948
|
const tokens = await this.issueTokens(apiReq, user, {
|
|
815
949
|
clientId: client.clientId,
|
|
816
950
|
scope: stored.scope,
|
|
@@ -819,11 +953,7 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
819
953
|
loginType: stored.loginType ?? 'oauth'
|
|
820
954
|
});
|
|
821
955
|
this.clearOAuthCookies(apiReq);
|
|
822
|
-
const scope = Array.isArray(stored.scope)
|
|
823
|
-
? stored.scope
|
|
824
|
-
: typeof stored.scope === 'string'
|
|
825
|
-
? stored.scope.split(/\s+/).filter((entry) => entry.length > 0)
|
|
826
|
-
: [];
|
|
956
|
+
const scope = Array.isArray(stored.scope) ? stored.scope : [];
|
|
827
957
|
return [200, this.buildTokenResponse(tokens, client, scope)];
|
|
828
958
|
}
|
|
829
959
|
clearOAuthCookies(apiReq) {
|
|
@@ -886,19 +1016,16 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
886
1016
|
if (!client) {
|
|
887
1017
|
throw new api_server_base_js_1.ApiError({ code: 400, message: 'Unknown client_id' });
|
|
888
1018
|
}
|
|
889
|
-
const requiresSecret =
|
|
1019
|
+
const requiresSecret = client.hasSecret ?? Boolean(client.clientSecret);
|
|
890
1020
|
if (requiresSecret) {
|
|
891
1021
|
if (!secretProvided) {
|
|
892
1022
|
throw new api_server_base_js_1.ApiError({ code: 400, message: 'Client authentication is required' });
|
|
893
1023
|
}
|
|
894
|
-
|
|
895
|
-
if (this.
|
|
896
|
-
|
|
897
|
-
valid = await verifySecret(client, clientSecret);
|
|
898
|
-
}
|
|
899
|
-
else {
|
|
900
|
-
valid = client.clientSecret === clientSecret;
|
|
1024
|
+
const verifySecret = this.storage.verifyClientSecret;
|
|
1025
|
+
if (typeof verifySecret !== 'function' || !this.storageImplements('verifyClientSecret')) {
|
|
1026
|
+
throw new api_server_base_js_1.ApiError({ code: 501, message: 'OAuth client secret verification is not configured' });
|
|
901
1027
|
}
|
|
1028
|
+
const valid = await verifySecret.call(this.storage, client, clientSecret);
|
|
902
1029
|
if (!valid) {
|
|
903
1030
|
throw new api_server_base_js_1.ApiError({ code: 401, message: 'Invalid client credentials' });
|
|
904
1031
|
}
|
|
@@ -925,63 +1052,118 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
925
1052
|
const password = toStringOrNull(body.password);
|
|
926
1053
|
if (login && password) {
|
|
927
1054
|
const user = await this.storage.getUser(login);
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
const verified = await this.storage.verifyPassword(password, hash);
|
|
933
|
-
if (!verified) {
|
|
934
|
-
throw new api_server_base_js_1.ApiError({
|
|
935
|
-
code: 400,
|
|
936
|
-
message: 'Invalid credentials',
|
|
937
|
-
errors: { password: 'Wrong password' }
|
|
938
|
-
});
|
|
1055
|
+
const hash = user ? this.storage.getUserPasswordHash(user) : '';
|
|
1056
|
+
const verified = user ? await this.storage.verifyPassword(password, hash) : false;
|
|
1057
|
+
if (!user || !verified) {
|
|
1058
|
+
throw new api_server_base_js_1.ApiError({ code: 400, message: 'Invalid credentials' });
|
|
939
1059
|
}
|
|
940
1060
|
return user;
|
|
941
1061
|
}
|
|
942
1062
|
throw new api_server_base_js_1.ApiError({ code: 401, message: 'Authorization requires user authentication' });
|
|
943
1063
|
}
|
|
944
|
-
|
|
945
|
-
const
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1064
|
+
hasPasskeyService() {
|
|
1065
|
+
const storageHints = this.storage;
|
|
1066
|
+
if (storageHints.passkeyService || storageHints.passkeyStore) {
|
|
1067
|
+
return true;
|
|
1068
|
+
}
|
|
1069
|
+
if (storageHints.adapter?.passkeyService || storageHints.adapter?.passkeyStore) {
|
|
1070
|
+
return true;
|
|
1071
|
+
}
|
|
1072
|
+
const serverHints = this.server;
|
|
1073
|
+
return !!serverHints.passkeyServiceAdapter;
|
|
1074
|
+
}
|
|
1075
|
+
hasOAuthStore() {
|
|
1076
|
+
const storageHints = this.storage;
|
|
1077
|
+
if (storageHints.oauthStore) {
|
|
1078
|
+
return true;
|
|
1079
|
+
}
|
|
1080
|
+
if (storageHints.adapter?.oauthStore) {
|
|
1081
|
+
return true;
|
|
1082
|
+
}
|
|
1083
|
+
const serverHints = this.server;
|
|
1084
|
+
return !!serverHints.oauthStoreAdapter;
|
|
1085
|
+
}
|
|
1086
|
+
storageImplements(key) {
|
|
1087
|
+
const candidate = this.storage[key];
|
|
1088
|
+
if (typeof candidate !== 'function') {
|
|
1089
|
+
return false;
|
|
1090
|
+
}
|
|
1091
|
+
const baseImpl = storage_js_1.BaseAuthAdapter.prototype[key];
|
|
1092
|
+
return candidate !== baseImpl;
|
|
1093
|
+
}
|
|
1094
|
+
storageImplementsAll(keys) {
|
|
1095
|
+
return keys.every((key) => this.storageImplements(key));
|
|
1096
|
+
}
|
|
1097
|
+
async applyRateLimit(apiReq, endpoint) {
|
|
1098
|
+
if (!this.rateLimitHook) {
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
await this.rateLimitHook({ apiReq, endpoint });
|
|
1102
|
+
}
|
|
1103
|
+
resolvePkceChallengeMethod(value) {
|
|
1104
|
+
if (value === 'S256') {
|
|
1105
|
+
return 'S256';
|
|
1106
|
+
}
|
|
1107
|
+
if (value === 'plain') {
|
|
1108
|
+
if (!this.allowInsecurePkcePlain) {
|
|
1109
|
+
throw new api_server_base_js_1.ApiError({ code: 400, message: 'PKCE plain is not permitted' });
|
|
981
1110
|
}
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1111
|
+
return 'plain';
|
|
1112
|
+
}
|
|
1113
|
+
return undefined;
|
|
1114
|
+
}
|
|
1115
|
+
defineRoutes() {
|
|
1116
|
+
const routes = [];
|
|
1117
|
+
const coreAuthSupported = this.storageImplementsAll([
|
|
1118
|
+
'getUser',
|
|
1119
|
+
'getUserPasswordHash',
|
|
1120
|
+
'getUserId',
|
|
1121
|
+
'verifyPassword',
|
|
1122
|
+
'filterUser',
|
|
1123
|
+
'storeToken',
|
|
1124
|
+
'getToken',
|
|
1125
|
+
'deleteToken'
|
|
1126
|
+
]);
|
|
1127
|
+
if (!coreAuthSupported) {
|
|
1128
|
+
return routes;
|
|
1129
|
+
}
|
|
1130
|
+
routes.push({
|
|
1131
|
+
method: 'post',
|
|
1132
|
+
path: '/v1/login',
|
|
1133
|
+
handler: (req) => this.postLogin(req),
|
|
1134
|
+
auth: { type: 'none', req: 'any' }
|
|
1135
|
+
}, {
|
|
1136
|
+
method: 'post',
|
|
1137
|
+
path: '/v1/refresh',
|
|
1138
|
+
handler: (req) => this.postRefresh(req),
|
|
1139
|
+
auth: { type: 'none', req: 'any' }
|
|
1140
|
+
}, {
|
|
1141
|
+
method: 'post',
|
|
1142
|
+
path: '/v1/logout',
|
|
1143
|
+
handler: (req) => this.postLogout(req),
|
|
1144
|
+
auth: { type: 'maybe', req: 'any' }
|
|
1145
|
+
}, {
|
|
1146
|
+
method: 'post',
|
|
1147
|
+
path: '/v1/whoami',
|
|
1148
|
+
handler: (req) => this.postWhoAmI(req),
|
|
1149
|
+
auth: { type: 'maybe', req: 'any' }
|
|
1150
|
+
}, {
|
|
1151
|
+
method: 'post',
|
|
1152
|
+
path: '/v1/impersonations',
|
|
1153
|
+
handler: (req) => this.postImpersonation(req),
|
|
1154
|
+
auth: { type: 'strict', req: 'any' }
|
|
1155
|
+
}, {
|
|
1156
|
+
method: 'delete',
|
|
1157
|
+
path: '/v1/impersonations',
|
|
1158
|
+
handler: (req) => this.deleteImpersonation(req),
|
|
1159
|
+
auth: { type: 'strict', req: 'any' }
|
|
1160
|
+
});
|
|
1161
|
+
const passkeysSupported = this.hasPasskeyService() &&
|
|
1162
|
+
this.storageImplements('createPasskeyChallenge') &&
|
|
1163
|
+
this.storageImplements('verifyPasskeyResponse');
|
|
1164
|
+
const passkeyCredentialsSupported = passkeysSupported &&
|
|
1165
|
+
this.storageImplements('listUserCredentials') &&
|
|
1166
|
+
this.storageImplements('deletePasskeyCredential');
|
|
985
1167
|
if (passkeysSupported) {
|
|
986
1168
|
routes.push({
|
|
987
1169
|
method: 'post',
|
|
@@ -994,6 +1176,19 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
994
1176
|
handler: (req) => this.postPasskeyVerify(req),
|
|
995
1177
|
auth: { type: 'none', req: 'any' }
|
|
996
1178
|
});
|
|
1179
|
+
if (passkeyCredentialsSupported) {
|
|
1180
|
+
routes.push({
|
|
1181
|
+
method: 'get',
|
|
1182
|
+
path: '/v1/passkeys',
|
|
1183
|
+
handler: (req) => this.getPasskeys(req),
|
|
1184
|
+
auth: { type: 'strict', req: 'any' }
|
|
1185
|
+
}, {
|
|
1186
|
+
method: 'delete',
|
|
1187
|
+
path: '/v1/passkeys/:credentialId',
|
|
1188
|
+
handler: (req) => this.deletePasskey(req),
|
|
1189
|
+
auth: { type: 'strict', req: 'any' }
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
997
1192
|
}
|
|
998
1193
|
const externalOAuthSupported = typeof this.server.initiateOAuth === 'function' && typeof this.server.completeOAuth === 'function';
|
|
999
1194
|
if (externalOAuthSupported) {
|
|
@@ -1009,9 +1204,10 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
1009
1204
|
auth: { type: 'none', req: 'any' }
|
|
1010
1205
|
});
|
|
1011
1206
|
}
|
|
1012
|
-
const oauthStorageSupported =
|
|
1013
|
-
|
|
1014
|
-
|
|
1207
|
+
const oauthStorageSupported = this.hasOAuthStore() &&
|
|
1208
|
+
this.storageImplements('getClient') &&
|
|
1209
|
+
this.storageImplements('createAuthCode') &&
|
|
1210
|
+
this.storageImplements('consumeAuthCode');
|
|
1015
1211
|
if (oauthStorageSupported) {
|
|
1016
1212
|
routes.push({
|
|
1017
1213
|
method: 'post',
|