@technomoron/api-server-base 2.0.0-beta.2 → 2.0.0-beta.21

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.
Files changed (115) hide show
  1. package/README.txt +81 -28
  2. package/dist/cjs/api-module.cjs +9 -0
  3. package/dist/cjs/api-module.d.ts +7 -4
  4. package/dist/cjs/api-server-base.cjs +607 -99
  5. package/dist/cjs/api-server-base.d.ts +80 -23
  6. package/dist/cjs/auth-api/auth-module.d.ts +23 -3
  7. package/dist/cjs/auth-api/auth-module.js +320 -124
  8. package/dist/cjs/auth-api/compat-auth-storage.d.ts +7 -5
  9. package/dist/cjs/auth-api/compat-auth-storage.js +15 -3
  10. package/dist/cjs/auth-api/mem-auth-store.d.ts +5 -3
  11. package/dist/cjs/auth-api/mem-auth-store.js +14 -28
  12. package/dist/cjs/auth-api/module.d.ts +1 -1
  13. package/dist/cjs/auth-api/sql-auth-store.d.ts +16 -4
  14. package/dist/cjs/auth-api/sql-auth-store.js +43 -30
  15. package/dist/cjs/auth-api/storage.d.ts +6 -4
  16. package/dist/cjs/auth-api/storage.js +15 -5
  17. package/dist/cjs/auth-api/types.d.ts +7 -2
  18. package/dist/cjs/auth-api/user-id.d.ts +5 -0
  19. package/dist/cjs/auth-api/user-id.js +38 -0
  20. package/dist/cjs/auth-cookie-options.d.ts +11 -0
  21. package/dist/cjs/auth-cookie-options.js +66 -0
  22. package/dist/cjs/index.cjs +4 -14
  23. package/dist/cjs/index.d.ts +4 -9
  24. package/dist/cjs/oauth/memory.d.ts +6 -0
  25. package/dist/cjs/oauth/memory.js +44 -11
  26. package/dist/cjs/oauth/models.d.ts +7 -2
  27. package/dist/cjs/oauth/models.js +10 -21
  28. package/dist/cjs/oauth/sequelize.d.ts +10 -48
  29. package/dist/cjs/oauth/sequelize.js +44 -99
  30. package/dist/cjs/oauth/types.d.ts +1 -0
  31. package/dist/cjs/passkey/base.d.ts +2 -0
  32. package/dist/cjs/passkey/config.d.ts +2 -0
  33. package/dist/cjs/passkey/config.js +26 -0
  34. package/dist/cjs/passkey/memory.d.ts +8 -0
  35. package/dist/cjs/passkey/memory.js +57 -16
  36. package/dist/cjs/passkey/models.d.ts +13 -4
  37. package/dist/cjs/passkey/models.js +41 -14
  38. package/dist/cjs/passkey/sequelize.d.ts +13 -25
  39. package/dist/cjs/passkey/sequelize.js +68 -153
  40. package/dist/cjs/passkey/service.d.ts +6 -2
  41. package/dist/cjs/passkey/service.js +205 -27
  42. package/dist/cjs/passkey/types.d.ts +18 -9
  43. package/dist/cjs/sequelize-utils.d.ts +8 -0
  44. package/dist/cjs/sequelize-utils.js +57 -0
  45. package/dist/cjs/token/base.d.ts +2 -1
  46. package/dist/cjs/token/base.js +3 -1
  47. package/dist/cjs/token/memory.d.ts +10 -0
  48. package/dist/cjs/token/memory.js +122 -32
  49. package/dist/cjs/token/sequelize.d.ts +4 -4
  50. package/dist/cjs/token/sequelize.js +67 -85
  51. package/dist/cjs/token/types.d.ts +8 -1
  52. package/dist/cjs/user/base.d.ts +1 -0
  53. package/dist/cjs/user/base.js +11 -4
  54. package/dist/cjs/user/memory.d.ts +2 -0
  55. package/dist/cjs/user/memory.js +9 -10
  56. package/dist/cjs/user/sequelize.d.ts +7 -2
  57. package/dist/cjs/user/sequelize.js +19 -32
  58. package/dist/esm/api-module.d.ts +7 -4
  59. package/dist/esm/api-module.js +9 -0
  60. package/dist/esm/api-server-base.d.ts +80 -23
  61. package/dist/esm/api-server-base.js +608 -100
  62. package/dist/esm/auth-api/auth-module.d.ts +23 -3
  63. package/dist/esm/auth-api/auth-module.js +321 -125
  64. package/dist/esm/auth-api/compat-auth-storage.d.ts +7 -5
  65. package/dist/esm/auth-api/compat-auth-storage.js +13 -1
  66. package/dist/esm/auth-api/mem-auth-store.d.ts +5 -3
  67. package/dist/esm/auth-api/mem-auth-store.js +14 -28
  68. package/dist/esm/auth-api/module.d.ts +1 -1
  69. package/dist/esm/auth-api/sql-auth-store.d.ts +16 -4
  70. package/dist/esm/auth-api/sql-auth-store.js +43 -30
  71. package/dist/esm/auth-api/storage.d.ts +6 -4
  72. package/dist/esm/auth-api/storage.js +13 -3
  73. package/dist/esm/auth-api/types.d.ts +7 -2
  74. package/dist/esm/auth-api/user-id.d.ts +5 -0
  75. package/dist/esm/auth-api/user-id.js +32 -0
  76. package/dist/esm/auth-cookie-options.d.ts +11 -0
  77. package/dist/esm/auth-cookie-options.js +63 -0
  78. package/dist/esm/index.d.ts +4 -9
  79. package/dist/esm/index.js +2 -7
  80. package/dist/esm/oauth/memory.d.ts +6 -0
  81. package/dist/esm/oauth/memory.js +44 -11
  82. package/dist/esm/oauth/models.d.ts +7 -2
  83. package/dist/esm/oauth/models.js +6 -19
  84. package/dist/esm/oauth/sequelize.d.ts +10 -48
  85. package/dist/esm/oauth/sequelize.js +32 -87
  86. package/dist/esm/oauth/types.d.ts +1 -0
  87. package/dist/esm/passkey/base.d.ts +2 -0
  88. package/dist/esm/passkey/config.d.ts +2 -0
  89. package/dist/esm/passkey/config.js +23 -0
  90. package/dist/esm/passkey/memory.d.ts +8 -0
  91. package/dist/esm/passkey/memory.js +57 -16
  92. package/dist/esm/passkey/models.d.ts +13 -4
  93. package/dist/esm/passkey/models.js +39 -12
  94. package/dist/esm/passkey/sequelize.d.ts +13 -25
  95. package/dist/esm/passkey/sequelize.js +69 -154
  96. package/dist/esm/passkey/service.d.ts +6 -2
  97. package/dist/esm/passkey/service.js +173 -28
  98. package/dist/esm/passkey/types.d.ts +18 -9
  99. package/dist/esm/sequelize-utils.d.ts +8 -0
  100. package/dist/esm/sequelize-utils.js +48 -0
  101. package/dist/esm/token/base.d.ts +2 -1
  102. package/dist/esm/token/base.js +3 -1
  103. package/dist/esm/token/memory.d.ts +10 -0
  104. package/dist/esm/token/memory.js +122 -32
  105. package/dist/esm/token/sequelize.d.ts +4 -4
  106. package/dist/esm/token/sequelize.js +67 -85
  107. package/dist/esm/token/types.d.ts +8 -1
  108. package/dist/esm/user/base.d.ts +1 -0
  109. package/dist/esm/user/base.js +11 -4
  110. package/dist/esm/user/memory.d.ts +2 -0
  111. package/dist/esm/user/memory.js +9 -10
  112. package/dist/esm/user/sequelize.d.ts +7 -2
  113. package/dist/esm/user/sequelize.js +19 -32
  114. package/docs/swagger/openapi.json +1876 -0
  115. package/package.json +84 -34
@@ -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: metadata.domain ?? this.defaultDomain ?? '',
79
- fingerprint: metadata.fingerprint ?? metadata.clientId ?? '',
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
- cookieOptions(apiReq) {
187
- const conf = this.server.config;
188
- const forwarded = apiReq.req.headers['x-forwarded-proto'];
189
- const referer = apiReq.req.headers['origin'] ?? apiReq.req.headers['referer'];
190
- const origin = typeof referer === 'string' ? referer : '';
191
- const isHttps = forwarded === 'https' || apiReq.req.protocol === 'https';
192
- const isLocalhost = origin.includes('localhost');
193
- const options = {
194
- httpOnly: true,
195
- secure: true,
196
- sameSite: 'strict',
197
- domain: conf.cookieDomain || undefined,
198
- path: '/',
199
- maxAge: undefined
200
- };
201
- if (conf.devMode) {
202
- options.secure = isHttps;
203
- options.httpOnly = false;
204
- options.sameSite = 'lax';
205
- if (isLocalhost) {
206
- options.domain = undefined;
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
- return options;
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 = this.buildTokenPayload(user, enrichedMetadata);
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
- if (!user) {
410
- throw new api_server_base_js_1.ApiError({
411
- code: 400,
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 access = this.server.jwtSign(this.buildTokenPayload(user, stored), conf.accessSecret, conf.accessExpiry);
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
- return [200, this.storage.filterUser(user)];
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 params = {
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 metadata = this.buildImpersonationMetadata((apiReq.req.body ?? {}));
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: codeChallengeMethod === 'S256' ? 'S256' : codeChallengeMethod === 'plain' ? 'plain' : undefined,
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 = !!client.clientSecret;
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
- let valid = false;
895
- if (this.storage.verifyClientSecret) {
896
- const verifySecret = this.storage.verifyClientSecret.bind(this.storage);
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
- if (!user) {
929
- throw new api_server_base_js_1.ApiError({ code: 400, message: 'Invalid credentials', errors: { login: 'Unknown user' } });
930
- }
931
- const hash = this.storage.getUserPasswordHash(user);
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
- defineRoutes() {
945
- const routes = [
946
- {
947
- method: 'post',
948
- path: '/v1/login',
949
- handler: (req) => this.postLogin(req),
950
- auth: { type: 'none', req: 'any' }
951
- },
952
- {
953
- method: 'post',
954
- path: '/v1/refresh',
955
- handler: (req) => this.postRefresh(req),
956
- auth: { type: 'none', req: 'any' }
957
- },
958
- {
959
- method: 'post',
960
- path: '/v1/logout',
961
- handler: (req) => this.postLogout(req),
962
- auth: { type: 'maybe', req: 'any' }
963
- },
964
- {
965
- method: 'post',
966
- path: '/v1/whoami',
967
- handler: (req) => this.postWhoAmI(req),
968
- auth: { type: 'maybe', req: 'any' }
969
- },
970
- {
971
- method: 'post',
972
- path: '/v1/impersonations',
973
- handler: (req) => this.postImpersonation(req),
974
- auth: { type: 'strict', req: 'any' }
975
- },
976
- {
977
- method: 'delete',
978
- path: '/v1/impersonations',
979
- handler: (req) => this.deleteImpersonation(req),
980
- auth: { type: 'strict', req: 'any' }
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
- const storage = this.storage;
984
- const passkeysSupported = typeof storage.createPasskeyChallenge === 'function' && typeof storage.verifyPasskeyResponse === 'function';
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 = typeof storage.getClient === 'function' &&
1013
- typeof storage.createAuthCode === 'function' &&
1014
- typeof storage.consumeAuthCode === 'function';
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',