@technomoron/api-server-base 2.0.0-beta.1 → 2.0.0-beta.11
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 +25 -2
- package/dist/cjs/api-server-base.cjs +277 -41
- package/dist/cjs/api-server-base.d.ts +27 -7
- package/dist/cjs/auth-api/auth-module.d.ts +11 -2
- package/dist/cjs/auth-api/auth-module.js +215 -46
- 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 +7 -1
- package/dist/cjs/auth-api/sql-auth-store.d.ts +5 -3
- package/dist/cjs/auth-api/sql-auth-store.js +7 -1
- 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/index.cjs +4 -4
- package/dist/cjs/index.d.ts +4 -4
- package/dist/cjs/oauth/sequelize.js +1 -1
- package/dist/cjs/passkey/base.d.ts +1 -0
- package/dist/cjs/passkey/memory.d.ts +1 -0
- package/dist/cjs/passkey/memory.js +4 -0
- package/dist/cjs/passkey/sequelize.d.ts +1 -0
- package/dist/cjs/passkey/sequelize.js +11 -2
- package/dist/cjs/passkey/service.d.ts +5 -2
- package/dist/cjs/passkey/service.js +145 -10
- package/dist/cjs/passkey/types.d.ts +3 -0
- package/dist/cjs/user/base.js +2 -1
- package/dist/esm/api-server-base.d.ts +27 -7
- package/dist/esm/api-server-base.js +278 -42
- package/dist/esm/auth-api/auth-module.d.ts +11 -2
- package/dist/esm/auth-api/auth-module.js +216 -47
- 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 +8 -2
- package/dist/esm/auth-api/sql-auth-store.d.ts +5 -3
- package/dist/esm/auth-api/sql-auth-store.js +8 -2
- 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/index.d.ts +4 -4
- package/dist/esm/index.js +2 -2
- package/dist/esm/oauth/sequelize.js +1 -1
- package/dist/esm/passkey/base.d.ts +1 -0
- package/dist/esm/passkey/memory.d.ts +1 -0
- package/dist/esm/passkey/memory.js +4 -0
- package/dist/esm/passkey/sequelize.d.ts +1 -0
- package/dist/esm/passkey/sequelize.js +11 -2
- package/dist/esm/passkey/service.d.ts +5 -2
- package/dist/esm/passkey/service.js +113 -11
- package/dist/esm/passkey/types.d.ts +3 -0
- package/dist/esm/user/base.js +2 -1
- package/package.json +13 -11
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { isoBase64URL } from '@simplewebauthn/server/helpers';
|
|
2
3
|
import { ApiError } from '../api-server-base.js';
|
|
3
4
|
import { BaseAuthModule } from './module.js';
|
|
5
|
+
import { BaseAuthAdapter } from './storage.js';
|
|
4
6
|
function isAuthIdentifier(value) {
|
|
5
7
|
return typeof value === 'string' || typeof value === 'number';
|
|
6
8
|
}
|
|
@@ -72,9 +74,21 @@ class AuthModule extends BaseAuthModule {
|
|
|
72
74
|
}
|
|
73
75
|
buildTokenMetadata(metadata = {}) {
|
|
74
76
|
const scope = metadata.scope;
|
|
77
|
+
const domain = metadata.domain ?? this.defaultDomain ?? '';
|
|
78
|
+
let fingerprint = metadata.fingerprint ?? metadata.clientId ?? '';
|
|
79
|
+
if (typeof fingerprint === 'string') {
|
|
80
|
+
fingerprint = fingerprint.trim();
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
fingerprint = '';
|
|
84
|
+
}
|
|
85
|
+
// Avoid every client sharing the empty-string fingerprint which collapses sessions into one bucket.
|
|
86
|
+
if (!fingerprint) {
|
|
87
|
+
fingerprint = `srv-${randomUUID()}`;
|
|
88
|
+
}
|
|
75
89
|
return {
|
|
76
|
-
domain
|
|
77
|
-
fingerprint
|
|
90
|
+
domain,
|
|
91
|
+
fingerprint,
|
|
78
92
|
label: metadata.label ?? (Array.isArray(scope) ? scope.join(' ') : typeof scope === 'string' ? scope : ''),
|
|
79
93
|
clientId: metadata.clientId,
|
|
80
94
|
ruid: metadata.ruid,
|
|
@@ -181,6 +195,44 @@ class AuthModule extends BaseAuthModule {
|
|
|
181
195
|
}
|
|
182
196
|
return prefs;
|
|
183
197
|
}
|
|
198
|
+
validateCredentialId(apiReq) {
|
|
199
|
+
const paramId = toStringOrNull(apiReq.req.params?.credentialId);
|
|
200
|
+
const bodyId = toStringOrNull(apiReq.req.body?.credentialId);
|
|
201
|
+
const credentialId = paramId ?? bodyId;
|
|
202
|
+
if (!credentialId) {
|
|
203
|
+
throw new ApiError({ code: 400, message: 'credentialId is required' });
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
isoBase64URL.toBuffer(credentialId);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
throw new ApiError({ code: 400, message: 'Invalid credentialId' });
|
|
210
|
+
}
|
|
211
|
+
return credentialId;
|
|
212
|
+
}
|
|
213
|
+
normalizeCredentialId(value) {
|
|
214
|
+
if (Buffer.isBuffer(value)) {
|
|
215
|
+
return value;
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
return Buffer.from(isoBase64URL.toBuffer(value));
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
try {
|
|
222
|
+
return Buffer.from(value, 'base64');
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return Buffer.from(value);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
toIsoDate(value) {
|
|
230
|
+
if (!value) {
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
234
|
+
return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
|
|
235
|
+
}
|
|
184
236
|
cookieOptions(apiReq) {
|
|
185
237
|
const conf = this.server.config;
|
|
186
238
|
const forwarded = apiReq.req.headers['x-forwarded-proto'];
|
|
@@ -511,7 +563,28 @@ class AuthModule extends BaseAuthModule {
|
|
|
511
563
|
: cookiePrefs.refreshTtlSeconds;
|
|
512
564
|
this.setJwtCookies(apiReq, { accessToken: access.token, refreshToken }, { sessionCookie: cookiePrefs.sessionCookie ?? false, refreshTtlSeconds: refreshTtlForCookie });
|
|
513
565
|
}
|
|
514
|
-
|
|
566
|
+
const tokenClaims = verify.data;
|
|
567
|
+
const effectiveUserId = this.storage.getUserId(user);
|
|
568
|
+
const effectiveId = String(effectiveUserId);
|
|
569
|
+
const rawRealId = stored.ruid ?? tokenClaims.ruid;
|
|
570
|
+
const normalizedRealId = rawRealId === undefined || rawRealId === null ? null : String(rawRealId).trim() || null;
|
|
571
|
+
const isImpersonating = normalizedRealId !== null && normalizedRealId !== effectiveId;
|
|
572
|
+
let realUser;
|
|
573
|
+
let realUserId;
|
|
574
|
+
if (isImpersonating && normalizedRealId !== null) {
|
|
575
|
+
const realUserEntity = await this.getUserOrThrow(normalizedRealId, 'Real user not found');
|
|
576
|
+
realUser = this.storage.filterUser(realUserEntity);
|
|
577
|
+
realUserId = this.storage.getUserId(realUserEntity);
|
|
578
|
+
}
|
|
579
|
+
return [
|
|
580
|
+
200,
|
|
581
|
+
{
|
|
582
|
+
user: this.storage.filterUser(user),
|
|
583
|
+
isImpersonating,
|
|
584
|
+
realUser,
|
|
585
|
+
realUserId
|
|
586
|
+
}
|
|
587
|
+
];
|
|
515
588
|
}
|
|
516
589
|
async postPasskeyChallenge(apiReq) {
|
|
517
590
|
if (typeof this.storage.createPasskeyChallenge !== 'function') {
|
|
@@ -596,6 +669,44 @@ class AuthModule extends BaseAuthModule {
|
|
|
596
669
|
const publicUser = this.storage.filterUser(user);
|
|
597
670
|
return [200, { ...tokens, user: publicUser }];
|
|
598
671
|
}
|
|
672
|
+
async getPasskeys(apiReq) {
|
|
673
|
+
if (typeof this.storage.listUserCredentials !== 'function') {
|
|
674
|
+
throw new ApiError({ code: 501, message: 'Passkey credential listing is not configured' });
|
|
675
|
+
}
|
|
676
|
+
const { userId } = await this.resolveActorContext(apiReq);
|
|
677
|
+
const credentials = await this.storage.listUserCredentials(userId);
|
|
678
|
+
const safeCredentials = credentials.map((credential) => {
|
|
679
|
+
const bufferId = this.normalizeCredentialId(credential.credentialId);
|
|
680
|
+
return {
|
|
681
|
+
id: isoBase64URL.fromBuffer(new Uint8Array(bufferId)),
|
|
682
|
+
transports: credential.transports,
|
|
683
|
+
backedUp: credential.backedUp,
|
|
684
|
+
deviceType: credential.deviceType,
|
|
685
|
+
createdAt: this.toIsoDate(credential.createdAt),
|
|
686
|
+
updatedAt: this.toIsoDate(credential.updatedAt)
|
|
687
|
+
};
|
|
688
|
+
});
|
|
689
|
+
return [200, { credentials: safeCredentials }];
|
|
690
|
+
}
|
|
691
|
+
async deletePasskey(apiReq) {
|
|
692
|
+
if (typeof this.storage.listUserCredentials !== 'function' ||
|
|
693
|
+
typeof this.storage.deletePasskeyCredential !== 'function') {
|
|
694
|
+
throw new ApiError({ code: 501, message: 'Passkey credential management is not configured' });
|
|
695
|
+
}
|
|
696
|
+
const { userId } = await this.resolveActorContext(apiReq);
|
|
697
|
+
const credentialId = this.validateCredentialId(apiReq);
|
|
698
|
+
const bufferId = Buffer.from(isoBase64URL.toBuffer(credentialId));
|
|
699
|
+
const credentials = await this.storage.listUserCredentials(userId);
|
|
700
|
+
const owns = credentials.some((credential) => {
|
|
701
|
+
const candidateId = this.normalizeCredentialId(credential.credentialId);
|
|
702
|
+
return isoBase64URL.fromBuffer(new Uint8Array(candidateId)) === credentialId;
|
|
703
|
+
});
|
|
704
|
+
if (!owns) {
|
|
705
|
+
throw new ApiError({ code: 404, message: 'Passkey not found' });
|
|
706
|
+
}
|
|
707
|
+
const deleted = await this.storage.deletePasskeyCredential(bufferId);
|
|
708
|
+
return [200, { deleted }];
|
|
709
|
+
}
|
|
599
710
|
async postImpersonation(apiReq) {
|
|
600
711
|
this.assertAuthReady();
|
|
601
712
|
const { targetIdentifier, metadata } = this.parseImpersonationRequest(apiReq);
|
|
@@ -939,47 +1050,91 @@ class AuthModule extends BaseAuthModule {
|
|
|
939
1050
|
}
|
|
940
1051
|
throw new ApiError({ code: 401, message: 'Authorization requires user authentication' });
|
|
941
1052
|
}
|
|
1053
|
+
hasPasskeyService() {
|
|
1054
|
+
const storageAny = this.storage;
|
|
1055
|
+
if (storageAny.passkeyService || storageAny.passkeyStore) {
|
|
1056
|
+
return true;
|
|
1057
|
+
}
|
|
1058
|
+
if (storageAny.adapter?.passkeyService || storageAny.adapter?.passkeyStore) {
|
|
1059
|
+
return true;
|
|
1060
|
+
}
|
|
1061
|
+
const serverAny = this.server;
|
|
1062
|
+
return !!serverAny.passkeyServiceAdapter;
|
|
1063
|
+
}
|
|
1064
|
+
hasOAuthStore() {
|
|
1065
|
+
const storageAny = this.storage;
|
|
1066
|
+
if (storageAny.oauthStore) {
|
|
1067
|
+
return true;
|
|
1068
|
+
}
|
|
1069
|
+
if (storageAny.adapter?.oauthStore) {
|
|
1070
|
+
return true;
|
|
1071
|
+
}
|
|
1072
|
+
const serverAny = this.server;
|
|
1073
|
+
return !!serverAny.oauthStoreAdapter;
|
|
1074
|
+
}
|
|
1075
|
+
storageImplements(key) {
|
|
1076
|
+
const candidate = this.storage[key];
|
|
1077
|
+
if (typeof candidate !== 'function') {
|
|
1078
|
+
return false;
|
|
1079
|
+
}
|
|
1080
|
+
const baseImpl = BaseAuthAdapter.prototype[key];
|
|
1081
|
+
return candidate !== baseImpl;
|
|
1082
|
+
}
|
|
1083
|
+
storageImplementsAll(keys) {
|
|
1084
|
+
return keys.every((key) => this.storageImplements(key));
|
|
1085
|
+
}
|
|
942
1086
|
defineRoutes() {
|
|
943
|
-
const routes = [
|
|
944
|
-
|
|
945
|
-
|
|
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
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1087
|
+
const routes = [];
|
|
1088
|
+
const coreAuthSupported = this.storageImplementsAll([
|
|
1089
|
+
'getUser',
|
|
1090
|
+
'getUserPasswordHash',
|
|
1091
|
+
'getUserId',
|
|
1092
|
+
'verifyPassword',
|
|
1093
|
+
'filterUser',
|
|
1094
|
+
'storeToken',
|
|
1095
|
+
'getToken',
|
|
1096
|
+
'deleteToken'
|
|
1097
|
+
]);
|
|
1098
|
+
if (!coreAuthSupported) {
|
|
1099
|
+
return routes;
|
|
1100
|
+
}
|
|
1101
|
+
routes.push({
|
|
1102
|
+
method: 'post',
|
|
1103
|
+
path: '/v1/login',
|
|
1104
|
+
handler: (req) => this.postLogin(req),
|
|
1105
|
+
auth: { type: 'none', req: 'any' }
|
|
1106
|
+
}, {
|
|
1107
|
+
method: 'post',
|
|
1108
|
+
path: '/v1/refresh',
|
|
1109
|
+
handler: (req) => this.postRefresh(req),
|
|
1110
|
+
auth: { type: 'none', req: 'any' }
|
|
1111
|
+
}, {
|
|
1112
|
+
method: 'post',
|
|
1113
|
+
path: '/v1/logout',
|
|
1114
|
+
handler: (req) => this.postLogout(req),
|
|
1115
|
+
auth: { type: 'maybe', req: 'any' }
|
|
1116
|
+
}, {
|
|
1117
|
+
method: 'post',
|
|
1118
|
+
path: '/v1/whoami',
|
|
1119
|
+
handler: (req) => this.postWhoAmI(req),
|
|
1120
|
+
auth: { type: 'maybe', req: 'any' }
|
|
1121
|
+
}, {
|
|
1122
|
+
method: 'post',
|
|
1123
|
+
path: '/v1/impersonations',
|
|
1124
|
+
handler: (req) => this.postImpersonation(req),
|
|
1125
|
+
auth: { type: 'strict', req: 'any' }
|
|
1126
|
+
}, {
|
|
1127
|
+
method: 'delete',
|
|
1128
|
+
path: '/v1/impersonations',
|
|
1129
|
+
handler: (req) => this.deleteImpersonation(req),
|
|
1130
|
+
auth: { type: 'strict', req: 'any' }
|
|
1131
|
+
});
|
|
1132
|
+
const passkeysSupported = this.hasPasskeyService() &&
|
|
1133
|
+
this.storageImplements('createPasskeyChallenge') &&
|
|
1134
|
+
this.storageImplements('verifyPasskeyResponse');
|
|
1135
|
+
const passkeyCredentialsSupported = passkeysSupported &&
|
|
1136
|
+
this.storageImplements('listUserCredentials') &&
|
|
1137
|
+
this.storageImplements('deletePasskeyCredential');
|
|
983
1138
|
if (passkeysSupported) {
|
|
984
1139
|
routes.push({
|
|
985
1140
|
method: 'post',
|
|
@@ -992,6 +1147,19 @@ class AuthModule extends BaseAuthModule {
|
|
|
992
1147
|
handler: (req) => this.postPasskeyVerify(req),
|
|
993
1148
|
auth: { type: 'none', req: 'any' }
|
|
994
1149
|
});
|
|
1150
|
+
if (passkeyCredentialsSupported) {
|
|
1151
|
+
routes.push({
|
|
1152
|
+
method: 'get',
|
|
1153
|
+
path: '/v1/passkeys',
|
|
1154
|
+
handler: (req) => this.getPasskeys(req),
|
|
1155
|
+
auth: { type: 'strict', req: 'any' }
|
|
1156
|
+
}, {
|
|
1157
|
+
method: 'delete',
|
|
1158
|
+
path: '/v1/passkeys/:credentialId?',
|
|
1159
|
+
handler: (req) => this.deletePasskey(req),
|
|
1160
|
+
auth: { type: 'strict', req: 'any' }
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
995
1163
|
}
|
|
996
1164
|
const externalOAuthSupported = typeof this.server.initiateOAuth === 'function' && typeof this.server.completeOAuth === 'function';
|
|
997
1165
|
if (externalOAuthSupported) {
|
|
@@ -1007,9 +1175,10 @@ class AuthModule extends BaseAuthModule {
|
|
|
1007
1175
|
auth: { type: 'none', req: 'any' }
|
|
1008
1176
|
});
|
|
1009
1177
|
}
|
|
1010
|
-
const oauthStorageSupported =
|
|
1011
|
-
|
|
1012
|
-
|
|
1178
|
+
const oauthStorageSupported = this.hasOAuthStore() &&
|
|
1179
|
+
this.storageImplements('getClient') &&
|
|
1180
|
+
this.storageImplements('createAuthCode') &&
|
|
1181
|
+
this.storageImplements('consumeAuthCode');
|
|
1013
1182
|
if (oauthStorageSupported) {
|
|
1014
1183
|
routes.push({
|
|
1015
1184
|
method: 'post',
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { PasskeyService } from '../passkey/service.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { AuthAdapter, AuthIdentifier } from './types.js';
|
|
3
3
|
import type { OAuthStore } from '../oauth/base.js';
|
|
4
4
|
import type { AuthCodeData, AuthCodeRequest, OAuthClient } from '../oauth/types.js';
|
|
5
5
|
import type { PasskeyStore } from '../passkey/base.js';
|
|
6
|
-
import type { PasskeyChallenge, PasskeyChallengeParams, PasskeyServiceConfig, PasskeyVerificationParams, PasskeyVerificationResult } from '../passkey/types.js';
|
|
6
|
+
import type { PasskeyChallenge, PasskeyChallengeParams, PasskeyServiceConfig, StoredPasskeyCredential, PasskeyVerificationParams, PasskeyVerificationResult } from '../passkey/types.js';
|
|
7
7
|
import type { TokenStore } from '../token/base.js';
|
|
8
8
|
import type { Token } from '../token/types.js';
|
|
9
9
|
import type { UserStore } from '../user/base.js';
|
|
@@ -11,7 +11,7 @@ interface PasskeyAdapterOptions {
|
|
|
11
11
|
store: PasskeyStore;
|
|
12
12
|
config: PasskeyServiceConfig;
|
|
13
13
|
}
|
|
14
|
-
export interface
|
|
14
|
+
export interface AuthAdapterOptions<UserRow, PublicUser> {
|
|
15
15
|
userStore: UserStore<UserRow, PublicUser>;
|
|
16
16
|
tokenStore: TokenStore;
|
|
17
17
|
passkeys?: PasskeyAdapterOptions | PasskeyService;
|
|
@@ -21,13 +21,13 @@ export interface AuthStorageAdapterOptions<UserRow, PublicUser> {
|
|
|
21
21
|
effectiveUserId: AuthIdentifier;
|
|
22
22
|
}) => boolean | Promise<boolean>;
|
|
23
23
|
}
|
|
24
|
-
export declare class
|
|
24
|
+
export declare class CompositeAuthAdapter<UserRow, PublicUser> implements AuthAdapter<UserRow, PublicUser> {
|
|
25
25
|
private readonly userStore;
|
|
26
26
|
private readonly tokenStore;
|
|
27
27
|
private readonly oauthStore?;
|
|
28
28
|
private readonly passkeyService?;
|
|
29
29
|
private readonly canImpersonateFn?;
|
|
30
|
-
constructor(options:
|
|
30
|
+
constructor(options: AuthAdapterOptions<UserRow, PublicUser>);
|
|
31
31
|
getUser(identifier: AuthIdentifier): Promise<UserRow | null>;
|
|
32
32
|
getUserPasswordHash(user: UserRow): string;
|
|
33
33
|
getUserId(user: UserRow): AuthIdentifier;
|
|
@@ -43,6 +43,8 @@ export declare class AuthStorageAdapter<UserRow, PublicUser> implements AuthStor
|
|
|
43
43
|
}): Promise<boolean>;
|
|
44
44
|
createPasskeyChallenge(params: PasskeyChallengeParams): Promise<PasskeyChallenge>;
|
|
45
45
|
verifyPasskeyResponse(params: PasskeyVerificationParams): Promise<PasskeyVerificationResult>;
|
|
46
|
+
listUserCredentials(userId: AuthIdentifier): Promise<StoredPasskeyCredential[]>;
|
|
47
|
+
deletePasskeyCredential(credentialId: Buffer | string): Promise<boolean>;
|
|
46
48
|
getClient(clientId: string): Promise<OAuthClient | null>;
|
|
47
49
|
verifyClientSecret(client: OAuthClient, clientSecret: string | null): Promise<boolean>;
|
|
48
50
|
createAuthCode(request: AuthCodeRequest): Promise<AuthCodeData>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { PasskeyService } from '../passkey/service.js';
|
|
3
|
-
export class
|
|
3
|
+
export class CompositeAuthAdapter {
|
|
4
4
|
constructor(options) {
|
|
5
5
|
this.userStore = options.userStore;
|
|
6
6
|
this.tokenStore = options.tokenStore;
|
|
@@ -52,6 +52,18 @@ export class AuthStorageAdapter {
|
|
|
52
52
|
}
|
|
53
53
|
return this.passkeyService.verifyResponse(params);
|
|
54
54
|
}
|
|
55
|
+
async listUserCredentials(userId) {
|
|
56
|
+
if (!this.passkeyService) {
|
|
57
|
+
throw new Error('Passkey storage is not configured');
|
|
58
|
+
}
|
|
59
|
+
return this.passkeyService.listUserCredentials(userId);
|
|
60
|
+
}
|
|
61
|
+
async deletePasskeyCredential(credentialId) {
|
|
62
|
+
if (!this.passkeyService) {
|
|
63
|
+
throw new Error('Passkey storage is not configured');
|
|
64
|
+
}
|
|
65
|
+
return this.passkeyService.deleteCredential(credentialId);
|
|
66
|
+
}
|
|
55
67
|
async getClient(clientId) {
|
|
56
68
|
if (!this.oauthStore) {
|
|
57
69
|
return null;
|
|
@@ -2,11 +2,11 @@ import { MemoryOAuthStore } from '../oauth/memory.js';
|
|
|
2
2
|
import { MemoryPasskeyStore } from '../passkey/memory.js';
|
|
3
3
|
import { TokenStore } from '../token/base.js';
|
|
4
4
|
import { MemoryUserStore } from '../user/memory.js';
|
|
5
|
-
import type {
|
|
5
|
+
import type { AuthAdapter, AuthIdentifier } from './types.js';
|
|
6
6
|
import type { MemoryOAuthStoreOptions } from '../oauth/memory.js';
|
|
7
7
|
import type { AuthCodeData, AuthCodeRequest, OAuthClient } from '../oauth/types.js';
|
|
8
8
|
import type { MemoryPasskeyStoreOptions } from '../passkey/memory.js';
|
|
9
|
-
import type { PasskeyChallenge, PasskeyChallengeParams, PasskeyServiceConfig, PasskeyUserDescriptor, PasskeyVerificationParams, PasskeyVerificationResult } from '../passkey/types.js';
|
|
9
|
+
import type { PasskeyChallenge, PasskeyChallengeParams, PasskeyServiceConfig, PasskeyUserDescriptor, StoredPasskeyCredential, PasskeyVerificationParams, PasskeyVerificationResult } from '../passkey/types.js';
|
|
10
10
|
import type { Token } from '../token/types.js';
|
|
11
11
|
import type { MemoryUserAttributes, MemoryPublicUser, MemoryUserStoreOptions } from '../user/memory.js';
|
|
12
12
|
interface PasskeyOptions extends Partial<PasskeyServiceConfig> {
|
|
@@ -24,7 +24,7 @@ export interface MemAuthStoreParams<UserAttributes extends MemoryUserAttributes
|
|
|
24
24
|
}) => boolean | Promise<boolean>;
|
|
25
25
|
tokenStore?: TokenStore;
|
|
26
26
|
}
|
|
27
|
-
export declare class MemAuthStore<UserAttributes extends MemoryUserAttributes = MemoryUserAttributes, PublicUserShape extends MemoryPublicUser<UserAttributes> = MemoryPublicUser<UserAttributes>> implements
|
|
27
|
+
export declare class MemAuthStore<UserAttributes extends MemoryUserAttributes = MemoryUserAttributes, PublicUserShape extends MemoryPublicUser<UserAttributes> = MemoryPublicUser<UserAttributes>> implements AuthAdapter<UserAttributes, PublicUserShape> {
|
|
28
28
|
readonly userStore: MemoryUserStore<UserAttributes, PublicUserShape>;
|
|
29
29
|
readonly tokenStore: TokenStore;
|
|
30
30
|
readonly passkeyStore?: MemoryPasskeyStore;
|
|
@@ -54,6 +54,8 @@ export declare class MemAuthStore<UserAttributes extends MemoryUserAttributes =
|
|
|
54
54
|
}): Promise<boolean>;
|
|
55
55
|
createPasskeyChallenge(params: PasskeyChallengeParams): Promise<PasskeyChallenge>;
|
|
56
56
|
verifyPasskeyResponse(params: PasskeyVerificationParams): Promise<PasskeyVerificationResult>;
|
|
57
|
+
listUserCredentials(userId: AuthIdentifier): Promise<StoredPasskeyCredential[]>;
|
|
58
|
+
deletePasskeyCredential(credentialId: Buffer | string): Promise<boolean>;
|
|
57
59
|
getClient(clientId: string): Promise<OAuthClient | null>;
|
|
58
60
|
verifyClientSecret(client: OAuthClient, clientSecret: string | null): Promise<boolean>;
|
|
59
61
|
createAuthCode(request: AuthCodeRequest): Promise<AuthCodeData>;
|
|
@@ -2,7 +2,7 @@ import { MemoryOAuthStore } from '../oauth/memory.js';
|
|
|
2
2
|
import { MemoryPasskeyStore } from '../passkey/memory.js';
|
|
3
3
|
import { MemoryTokenStore } from '../token/memory.js';
|
|
4
4
|
import { MemoryUserStore } from '../user/memory.js';
|
|
5
|
-
import {
|
|
5
|
+
import { CompositeAuthAdapter } from './compat-auth-storage.js';
|
|
6
6
|
const DEFAULT_PASSKEY_CONFIG = {
|
|
7
7
|
rpId: 'localhost',
|
|
8
8
|
rpName: 'API Server',
|
|
@@ -56,7 +56,7 @@ export class MemAuthStore {
|
|
|
56
56
|
passkeyStore = new MemoryPasskeyStore({ resolveUser });
|
|
57
57
|
this.passkeyStore = passkeyStore;
|
|
58
58
|
}
|
|
59
|
-
this.adapter = new
|
|
59
|
+
this.adapter = new CompositeAuthAdapter({
|
|
60
60
|
userStore: this.userStore,
|
|
61
61
|
tokenStore: this.tokenStore,
|
|
62
62
|
passkeys: passkeyStore && passkeyConfig ? { store: passkeyStore, config: passkeyConfig } : undefined,
|
|
@@ -113,6 +113,12 @@ export class MemAuthStore {
|
|
|
113
113
|
async verifyPasskeyResponse(params) {
|
|
114
114
|
return this.adapter.verifyPasskeyResponse(params);
|
|
115
115
|
}
|
|
116
|
+
async listUserCredentials(userId) {
|
|
117
|
+
return this.adapter.listUserCredentials(userId);
|
|
118
|
+
}
|
|
119
|
+
async deletePasskeyCredential(credentialId) {
|
|
120
|
+
return this.adapter.deletePasskeyCredential(credentialId);
|
|
121
|
+
}
|
|
116
122
|
async getClient(clientId) {
|
|
117
123
|
return this.adapter.getClient(clientId);
|
|
118
124
|
}
|
|
@@ -3,9 +3,9 @@ import { SequelizeOAuthStore, type SequelizeOAuthStoreOptions } from '../oauth/s
|
|
|
3
3
|
import { SequelizePasskeyStore } from '../passkey/sequelize.js';
|
|
4
4
|
import { type SequelizeTokenStoreOptions } from '../token/sequelize.js';
|
|
5
5
|
import { SequelizeUserStore, type AuthUserAttributes, GenericUserModel, GenericUserModelStatic } from '../user/sequelize.js';
|
|
6
|
-
import type {
|
|
6
|
+
import type { AuthAdapter, AuthIdentifier } from './types.js';
|
|
7
7
|
import type { AuthCodeData, AuthCodeRequest, OAuthClient } from '../oauth/types.js';
|
|
8
|
-
import type { PasskeyChallenge, PasskeyChallengeParams, PasskeyServiceConfig, PasskeyUserDescriptor, PasskeyVerificationParams, PasskeyVerificationResult } from '../passkey/types.js';
|
|
8
|
+
import type { PasskeyChallenge, PasskeyChallengeParams, PasskeyServiceConfig, PasskeyUserDescriptor, StoredPasskeyCredential, PasskeyVerificationParams, PasskeyVerificationResult } from '../passkey/types.js';
|
|
9
9
|
import type { TokenStore } from '../token/base.js';
|
|
10
10
|
import type { Token } from '../token/types.js';
|
|
11
11
|
interface PasskeyOptions extends Partial<PasskeyServiceConfig> {
|
|
@@ -30,7 +30,7 @@ export interface SqlAuthStoreParams<UserAttributes extends AuthUserAttributes =
|
|
|
30
30
|
tokenStoreOptions?: Omit<SequelizeTokenStoreOptions, 'sequelize'>;
|
|
31
31
|
oauthStoreOptions?: Omit<SequelizeOAuthStoreOptions, 'sequelize'>;
|
|
32
32
|
}
|
|
33
|
-
export declare class SqlAuthStore<UserAttributes extends AuthUserAttributes = AuthUserAttributes, PublicUserShape extends Omit<UserAttributes, 'password'> = Omit<UserAttributes, 'password'>> implements
|
|
33
|
+
export declare class SqlAuthStore<UserAttributes extends AuthUserAttributes = AuthUserAttributes, PublicUserShape extends Omit<UserAttributes, 'password'> = Omit<UserAttributes, 'password'>> implements AuthAdapter<UserAttributes, PublicUserShape> {
|
|
34
34
|
readonly userStore: SequelizeUserStore<UserAttributes, PublicUserShape>;
|
|
35
35
|
readonly tokenStore: TokenStore;
|
|
36
36
|
readonly passkeyStore?: SequelizePasskeyStore;
|
|
@@ -63,6 +63,8 @@ export declare class SqlAuthStore<UserAttributes extends AuthUserAttributes = Au
|
|
|
63
63
|
}): Promise<boolean>;
|
|
64
64
|
createPasskeyChallenge(params: PasskeyChallengeParams): Promise<PasskeyChallenge>;
|
|
65
65
|
verifyPasskeyResponse(params: PasskeyVerificationParams): Promise<PasskeyVerificationResult>;
|
|
66
|
+
listUserCredentials(userId: AuthIdentifier): Promise<StoredPasskeyCredential[]>;
|
|
67
|
+
deletePasskeyCredential(credentialId: Buffer | string): Promise<boolean>;
|
|
66
68
|
getClient(clientId: string): Promise<OAuthClient | null>;
|
|
67
69
|
verifyClientSecret(client: OAuthClient, clientSecret: string | null): Promise<boolean>;
|
|
68
70
|
createAuthCode(request: AuthCodeRequest): Promise<AuthCodeData>;
|
|
@@ -2,7 +2,7 @@ import { SequelizeOAuthStore } from '../oauth/sequelize.js';
|
|
|
2
2
|
import { SequelizePasskeyStore } from '../passkey/sequelize.js';
|
|
3
3
|
import { SequelizeTokenStore } from '../token/sequelize.js';
|
|
4
4
|
import { SequelizeUserStore } from '../user/sequelize.js';
|
|
5
|
-
import {
|
|
5
|
+
import { CompositeAuthAdapter } from './compat-auth-storage.js';
|
|
6
6
|
const DEFAULT_PASSKEY_CONFIG = {
|
|
7
7
|
rpId: 'localhost',
|
|
8
8
|
rpName: 'API Server',
|
|
@@ -69,7 +69,7 @@ export class SqlAuthStore {
|
|
|
69
69
|
passkeyStore = new SequelizePasskeyStore({ sequelize: this.sequelize, resolveUser });
|
|
70
70
|
this.passkeyStore = passkeyStore;
|
|
71
71
|
}
|
|
72
|
-
this.adapter = new
|
|
72
|
+
this.adapter = new CompositeAuthAdapter({
|
|
73
73
|
userStore: this.userStore,
|
|
74
74
|
tokenStore: this.tokenStore,
|
|
75
75
|
passkeys: passkeyStore && passkeyConfig ? { store: passkeyStore, config: passkeyConfig } : undefined,
|
|
@@ -144,6 +144,12 @@ export class SqlAuthStore {
|
|
|
144
144
|
async verifyPasskeyResponse(params) {
|
|
145
145
|
return this.adapter.verifyPasskeyResponse(params);
|
|
146
146
|
}
|
|
147
|
+
async listUserCredentials(userId) {
|
|
148
|
+
return this.adapter.listUserCredentials(userId);
|
|
149
|
+
}
|
|
150
|
+
async deletePasskeyCredential(credentialId) {
|
|
151
|
+
return this.adapter.deletePasskeyCredential(credentialId);
|
|
152
|
+
}
|
|
147
153
|
async getClient(clientId) {
|
|
148
154
|
return this.adapter.getClient(clientId);
|
|
149
155
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AuthAdapter, AuthIdentifier } from './types.js';
|
|
2
2
|
import type { AuthCodeData, AuthCodeRequest, OAuthClient } from '../oauth/types.js';
|
|
3
|
-
import type { PasskeyChallenge, PasskeyChallengeParams, PasskeyVerificationParams, PasskeyVerificationResult } from '../passkey/types.js';
|
|
3
|
+
import type { PasskeyChallenge, PasskeyChallengeParams, PasskeyVerificationParams, PasskeyVerificationResult, StoredPasskeyCredential } from '../passkey/types.js';
|
|
4
4
|
import type { Token } from '../token/types.js';
|
|
5
|
-
export declare class
|
|
5
|
+
export declare class BaseAuthAdapter<UserRow = unknown, SafeUser = unknown> implements AuthAdapter<UserRow, SafeUser> {
|
|
6
6
|
getUser(identifier: AuthIdentifier): Promise<UserRow | null>;
|
|
7
7
|
getUserPasswordHash(user: UserRow): string;
|
|
8
8
|
getUserId(user: UserRow): AuthIdentifier;
|
|
@@ -24,6 +24,8 @@ export declare class BaseAuthStorage<UserRow = unknown, SafeUser = unknown> impl
|
|
|
24
24
|
}): Promise<boolean>;
|
|
25
25
|
createPasskeyChallenge(params: PasskeyChallengeParams): Promise<PasskeyChallenge>;
|
|
26
26
|
verifyPasskeyResponse(params: PasskeyVerificationParams): Promise<PasskeyVerificationResult>;
|
|
27
|
+
listUserCredentials(userId: AuthIdentifier): Promise<StoredPasskeyCredential[]>;
|
|
28
|
+
deletePasskeyCredential(credentialId: Buffer | string): Promise<boolean>;
|
|
27
29
|
getClient(clientId: string): Promise<OAuthClient | null>;
|
|
28
30
|
verifyClientSecret(client: OAuthClient, clientSecret: string | null): Promise<boolean>;
|
|
29
31
|
createAuthCode(request: AuthCodeRequest): Promise<AuthCodeData>;
|
|
@@ -33,4 +35,4 @@ export declare class BaseAuthStorage<UserRow = unknown, SafeUser = unknown> impl
|
|
|
33
35
|
effectiveUserId: AuthIdentifier;
|
|
34
36
|
}): Promise<boolean>;
|
|
35
37
|
}
|
|
36
|
-
export declare const
|
|
38
|
+
export declare const nullAuthAdapter: AuthAdapter<unknown, unknown>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// Handy base you can extend when wiring a real
|
|
1
|
+
// Handy base you can extend when wiring a real auth adapter. Every method
|
|
2
2
|
// throws by default so unimplemented hooks fail loudly.
|
|
3
|
-
export class
|
|
3
|
+
export class BaseAuthAdapter {
|
|
4
4
|
// Override to load a user record by identifier
|
|
5
5
|
async getUser(identifier) {
|
|
6
6
|
void identifier;
|
|
@@ -57,6 +57,16 @@ export class BaseAuthStorage {
|
|
|
57
57
|
void params;
|
|
58
58
|
throw new Error('Auth storage not configured');
|
|
59
59
|
}
|
|
60
|
+
// Override to list passkey credentials for a user
|
|
61
|
+
async listUserCredentials(userId) {
|
|
62
|
+
void userId;
|
|
63
|
+
throw new Error('Auth storage not configured');
|
|
64
|
+
}
|
|
65
|
+
// Override to delete a passkey credential
|
|
66
|
+
async deletePasskeyCredential(credentialId) {
|
|
67
|
+
void credentialId;
|
|
68
|
+
throw new Error('Auth storage not configured');
|
|
69
|
+
}
|
|
60
70
|
// Override to fetch an OAuth client by identifier
|
|
61
71
|
async getClient(clientId) {
|
|
62
72
|
void clientId;
|
|
@@ -85,4 +95,4 @@ export class BaseAuthStorage {
|
|
|
85
95
|
return false;
|
|
86
96
|
}
|
|
87
97
|
}
|
|
88
|
-
export const
|
|
98
|
+
export const nullAuthAdapter = new BaseAuthAdapter();
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { AuthCodeData, AuthCodeRequest, OAuthClient } from '../oauth/types.js';
|
|
2
|
-
import type { PasskeyChallenge, PasskeyChallengeParams, PasskeyVerificationParams, PasskeyVerificationResult } from '../passkey/types.js';
|
|
2
|
+
import type { PasskeyChallenge, PasskeyChallengeParams, PasskeyVerificationParams, PasskeyVerificationResult, StoredPasskeyCredential } from '../passkey/types.js';
|
|
3
3
|
import type { Token } from '../token/types.js';
|
|
4
4
|
export type AuthIdentifier = string | number;
|
|
5
|
-
|
|
5
|
+
/** @internal */
|
|
6
|
+
export interface AuthAdapter<UserRow, SafeUser> {
|
|
6
7
|
getUser(identifier: AuthIdentifier): Promise<UserRow | null>;
|
|
7
8
|
getUserPasswordHash(user: UserRow): string;
|
|
8
9
|
getUserId(user: UserRow): AuthIdentifier;
|
|
@@ -18,6 +19,8 @@ export interface AuthStorage<UserRow, SafeUser> {
|
|
|
18
19
|
}): Promise<boolean>;
|
|
19
20
|
createPasskeyChallenge?(params: PasskeyChallengeParams): Promise<PasskeyChallenge>;
|
|
20
21
|
verifyPasskeyResponse?(params: PasskeyVerificationParams): Promise<PasskeyVerificationResult>;
|
|
22
|
+
listUserCredentials?(userId: AuthIdentifier): Promise<StoredPasskeyCredential[]>;
|
|
23
|
+
deletePasskeyCredential?(credentialId: Buffer | string): Promise<boolean>;
|
|
21
24
|
getClient?(clientId: string): Promise<OAuthClient | null>;
|
|
22
25
|
verifyClientSecret?(client: OAuthClient, clientSecret: string | null): Promise<boolean>;
|
|
23
26
|
createAuthCode?(request: AuthCodeRequest): Promise<AuthCodeData>;
|
|
@@ -27,3 +30,5 @@ export interface AuthStorage<UserRow, SafeUser> {
|
|
|
27
30
|
effectiveUserId: AuthIdentifier;
|
|
28
31
|
}): Promise<boolean>;
|
|
29
32
|
}
|
|
33
|
+
/** @internal */
|
|
34
|
+
export type AuthStorage<UserRow, SafeUser> = AuthAdapter<UserRow, SafeUser>;
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
export { default as ApiServer } from './api-server-base.js';
|
|
2
2
|
export { ApiError } from './api-server-base.js';
|
|
3
3
|
export { ApiModule } from './api-module.js';
|
|
4
|
-
export type { ApiErrorParams, ApiHandler, ApiKey, ApiServerConf, ApiRequest, ApiRoute, ApiAuthType, ApiAuthClass, ApiTokenData, ExtendedReq } from './api-server-base.js';
|
|
5
|
-
export type { AuthIdentifier
|
|
4
|
+
export type { ApiErrorParams, ApiHandler, ApiKey, ApiServerConf, ApiRequest, ApiRoute, ApiAuthType, ApiAuthClass, ApiTokenData, ExtendedReq, ExpressApiRequest, ExpressApiLocals } from './api-server-base.js';
|
|
5
|
+
export type { AuthIdentifier } from './auth-api/types.js';
|
|
6
6
|
export type { Token, TokenPair, TokenStatus } from './token/types.js';
|
|
7
7
|
export type { JwtSignResult, JwtVerifyResult, JwtDecodeResult } from './token/base.js';
|
|
8
8
|
export type { OAuthClient, AuthCodeData, AuthCodeRequest } from './oauth/types.js';
|
|
9
9
|
export type { AuthProviderModule } from './auth-api/module.js';
|
|
10
|
-
export {
|
|
10
|
+
export { nullAuthAdapter, BaseAuthAdapter } from './auth-api/storage.js';
|
|
11
11
|
export { nullAuthModule, BaseAuthModule } from './auth-api/module.js';
|
|
12
|
-
export {
|
|
12
|
+
export { CompositeAuthAdapter } from './auth-api/compat-auth-storage.js';
|
|
13
13
|
export { MemAuthStore } from './auth-api/mem-auth-store.js';
|
|
14
14
|
export { SqlAuthStore } from './auth-api/sql-auth-store.js';
|
|
15
15
|
export { default as AuthModule } from './auth-api/auth-module.js';
|
package/dist/esm/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export { default as ApiServer } from './api-server-base.js';
|
|
2
2
|
export { ApiError } from './api-server-base.js';
|
|
3
3
|
export { ApiModule } from './api-module.js';
|
|
4
|
-
export {
|
|
4
|
+
export { nullAuthAdapter, BaseAuthAdapter } from './auth-api/storage.js';
|
|
5
5
|
export { nullAuthModule, BaseAuthModule } from './auth-api/module.js';
|
|
6
|
-
export {
|
|
6
|
+
export { CompositeAuthAdapter } from './auth-api/compat-auth-storage.js';
|
|
7
7
|
export { MemAuthStore } from './auth-api/mem-auth-store.js';
|
|
8
8
|
export { SqlAuthStore } from './auth-api/sql-auth-store.js';
|
|
9
9
|
export { default as AuthModule } from './auth-api/auth-module.js';
|
|
@@ -116,7 +116,7 @@ export class SequelizeOAuthStore extends OAuthStore {
|
|
|
116
116
|
const existing = await this.clients.findByPk(input.clientId);
|
|
117
117
|
const hashedSecret = input.clientSecret !== undefined && input.clientSecret !== null
|
|
118
118
|
? await bcrypt.hash(input.clientSecret, this.bcryptRounds)
|
|
119
|
-
: existing?.client_secret ?? '';
|
|
119
|
+
: (existing?.client_secret ?? '');
|
|
120
120
|
const redirectUris = input.redirectUris ?? (existing ? decodeStringArray(existing.redirect_uris) : undefined);
|
|
121
121
|
const scope = input.scope ?? (existing ? decodeStringArray(existing.scope) : undefined);
|
|
122
122
|
const metadata = input.metadata ?? (existing ? parseMetadata(existing.metadata) : undefined);
|