@technomoron/api-server-base 2.0.0-beta.2 → 2.0.0-beta.4
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/dist/cjs/api-server-base.cjs +2 -2
- package/dist/cjs/api-server-base.d.ts +4 -4
- package/dist/cjs/auth-api/auth-module.d.ts +11 -2
- package/dist/cjs/auth-api/auth-module.js +179 -43
- 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 +3 -3
- 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 +6 -0
- package/dist/cjs/passkey/types.d.ts +3 -0
- package/dist/esm/api-server-base.d.ts +4 -4
- package/dist/esm/api-server-base.js +3 -3
- package/dist/esm/auth-api/auth-module.d.ts +11 -2
- package/dist/esm/auth-api/auth-module.js +179 -43
- 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 +3 -3
- package/dist/esm/index.js +2 -2
- 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 +6 -0
- package/dist/esm/passkey/types.d.ts +3 -0
- package/package.json +1 -1
package/dist/cjs/index.cjs
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.SequelizeOAuthStore = exports.MemoryOAuthStore = exports.OAuthStore = exports.SequelizePasskeyStore = exports.MemoryPasskeyStore = exports.PasskeyStore = exports.PasskeyService = exports.SequelizeTokenStore = exports.MemoryTokenStore = exports.TokenStore = exports.SequelizeUserStore = exports.MemoryUserStore = exports.UserStore = exports.AuthModule = exports.SqlAuthStore = exports.MemAuthStore = exports.
|
|
6
|
+
exports.SequelizeOAuthStore = exports.MemoryOAuthStore = exports.OAuthStore = exports.SequelizePasskeyStore = exports.MemoryPasskeyStore = exports.PasskeyStore = exports.PasskeyService = exports.SequelizeTokenStore = exports.MemoryTokenStore = exports.TokenStore = exports.SequelizeUserStore = exports.MemoryUserStore = exports.UserStore = exports.AuthModule = exports.SqlAuthStore = exports.MemAuthStore = exports.CompositeAuthAdapter = exports.BaseAuthModule = exports.nullAuthModule = exports.BaseAuthAdapter = exports.nullAuthAdapter = exports.ApiModule = exports.ApiError = exports.ApiServer = void 0;
|
|
7
7
|
var api_server_base_js_1 = require("./api-server-base.cjs");
|
|
8
8
|
Object.defineProperty(exports, "ApiServer", { enumerable: true, get: function () { return __importDefault(api_server_base_js_1).default; } });
|
|
9
9
|
var api_server_base_js_2 = require("./api-server-base.cjs");
|
|
@@ -11,13 +11,13 @@ Object.defineProperty(exports, "ApiError", { enumerable: true, get: function ()
|
|
|
11
11
|
var api_module_js_1 = require("./api-module.cjs");
|
|
12
12
|
Object.defineProperty(exports, "ApiModule", { enumerable: true, get: function () { return api_module_js_1.ApiModule; } });
|
|
13
13
|
var storage_js_1 = require("./auth-api/storage.js");
|
|
14
|
-
Object.defineProperty(exports, "
|
|
15
|
-
Object.defineProperty(exports, "
|
|
14
|
+
Object.defineProperty(exports, "nullAuthAdapter", { enumerable: true, get: function () { return storage_js_1.nullAuthAdapter; } });
|
|
15
|
+
Object.defineProperty(exports, "BaseAuthAdapter", { enumerable: true, get: function () { return storage_js_1.BaseAuthAdapter; } });
|
|
16
16
|
var module_js_1 = require("./auth-api/module.js");
|
|
17
17
|
Object.defineProperty(exports, "nullAuthModule", { enumerable: true, get: function () { return module_js_1.nullAuthModule; } });
|
|
18
18
|
Object.defineProperty(exports, "BaseAuthModule", { enumerable: true, get: function () { return module_js_1.BaseAuthModule; } });
|
|
19
19
|
var compat_auth_storage_js_1 = require("./auth-api/compat-auth-storage.js");
|
|
20
|
-
Object.defineProperty(exports, "
|
|
20
|
+
Object.defineProperty(exports, "CompositeAuthAdapter", { enumerable: true, get: function () { return compat_auth_storage_js_1.CompositeAuthAdapter; } });
|
|
21
21
|
var mem_auth_store_js_1 = require("./auth-api/mem-auth-store.js");
|
|
22
22
|
Object.defineProperty(exports, "MemAuthStore", { enumerable: true, get: function () { return mem_auth_store_js_1.MemAuthStore; } });
|
|
23
23
|
var sql_auth_store_js_1 = require("./auth-api/sql-auth-store.js");
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -2,14 +2,14 @@ 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
4
|
export type { ApiErrorParams, ApiHandler, ApiKey, ApiServerConf, ApiRequest, ApiRoute, ApiAuthType, ApiAuthClass, ApiTokenData, ExtendedReq } from './api-server-base.js';
|
|
5
|
-
export type { AuthIdentifier
|
|
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';
|
|
@@ -6,6 +6,7 @@ export declare abstract class PasskeyStore implements PasskeyStorageAdapter {
|
|
|
6
6
|
login?: string;
|
|
7
7
|
}): Promise<PasskeyUserDescriptor | null>;
|
|
8
8
|
abstract listUserCredentials(userId: AuthIdentifier): Promise<StoredPasskeyCredential[]>;
|
|
9
|
+
abstract deleteCredential(credentialId: Buffer | string): Promise<boolean>;
|
|
9
10
|
abstract findCredentialById(credentialId: Buffer): Promise<StoredPasskeyCredential | null>;
|
|
10
11
|
abstract saveCredential(record: StoredPasskeyCredential): Promise<void>;
|
|
11
12
|
abstract updateCredentialCounter(credentialId: Buffer, counter: number): Promise<void>;
|
|
@@ -17,6 +17,7 @@ export declare class MemoryPasskeyStore extends PasskeyStore {
|
|
|
17
17
|
login?: string;
|
|
18
18
|
}): Promise<PasskeyUserDescriptor | null>;
|
|
19
19
|
listUserCredentials(userId: AuthIdentifier): Promise<StoredPasskeyCredential[]>;
|
|
20
|
+
deleteCredential(credentialId: Buffer | string): Promise<boolean>;
|
|
20
21
|
findCredentialById(credentialId: Buffer): Promise<StoredPasskeyCredential | null>;
|
|
21
22
|
saveCredential(record: StoredPasskeyCredential): Promise<void>;
|
|
22
23
|
updateCredentialCounter(credentialId: Buffer, counter: number): Promise<void>;
|
|
@@ -37,6 +37,10 @@ class MemoryPasskeyStore extends base_js_1.PasskeyStore {
|
|
|
37
37
|
.filter((record) => normalizeUserId(record.userId) === normalizedUserId)
|
|
38
38
|
.map((record) => cloneCredential(record));
|
|
39
39
|
}
|
|
40
|
+
async deleteCredential(credentialId) {
|
|
41
|
+
const key = encodeCredentialId(credentialId);
|
|
42
|
+
return this.credentials.delete(key);
|
|
43
|
+
}
|
|
40
44
|
async findCredentialById(credentialId) {
|
|
41
45
|
const record = this.credentials.get(encodeCredentialId(credentialId));
|
|
42
46
|
return record ? cloneCredential(record) : null;
|
|
@@ -44,6 +44,7 @@ export declare class SequelizePasskeyStore extends PasskeyStore {
|
|
|
44
44
|
login?: string;
|
|
45
45
|
}): Promise<PasskeyUserDescriptor | null>;
|
|
46
46
|
listUserCredentials(userId: AuthIdentifier): Promise<StoredPasskeyCredential[]>;
|
|
47
|
+
deleteCredential(credentialId: Buffer | string): Promise<boolean>;
|
|
47
48
|
findCredentialById(credentialId: Buffer): Promise<StoredPasskeyCredential | null>;
|
|
48
49
|
saveCredential(record: StoredPasskeyCredential): Promise<void>;
|
|
49
50
|
updateCredentialCounter(credentialId: Buffer, counter: number): Promise<void>;
|
|
@@ -147,9 +147,16 @@ class SequelizePasskeyStore extends base_js_1.PasskeyStore {
|
|
|
147
147
|
counter: model.counter,
|
|
148
148
|
transports: (model.transports ?? undefined),
|
|
149
149
|
backedUp: model.backedUp,
|
|
150
|
-
deviceType: model.deviceType
|
|
150
|
+
deviceType: model.deviceType,
|
|
151
|
+
createdAt: model.createdAt ?? undefined,
|
|
152
|
+
updatedAt: model.updatedAt ?? undefined
|
|
151
153
|
}));
|
|
152
154
|
}
|
|
155
|
+
async deleteCredential(credentialId) {
|
|
156
|
+
const encoded = Buffer.isBuffer(credentialId) ? credentialId.toString('base64') : credentialId;
|
|
157
|
+
const deleted = await this.credentials.destroy({ where: { credentialId: encoded } });
|
|
158
|
+
return deleted > 0;
|
|
159
|
+
}
|
|
153
160
|
async findCredentialById(credentialId) {
|
|
154
161
|
const model = await this.credentials.findByPk(encodeCredentialId(credentialId));
|
|
155
162
|
if (!model) {
|
|
@@ -162,7 +169,9 @@ class SequelizePasskeyStore extends base_js_1.PasskeyStore {
|
|
|
162
169
|
counter: model.counter,
|
|
163
170
|
transports: (model.transports ?? undefined),
|
|
164
171
|
backedUp: model.backedUp,
|
|
165
|
-
deviceType: model.deviceType
|
|
172
|
+
deviceType: model.deviceType,
|
|
173
|
+
createdAt: model.createdAt ?? undefined,
|
|
174
|
+
updatedAt: model.updatedAt ?? undefined
|
|
166
175
|
};
|
|
167
176
|
}
|
|
168
177
|
async saveCredential(record) {
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import type { PasskeyChallenge, PasskeyChallengeParams, PasskeyStorageAdapter, PasskeyVerificationParams, PasskeyVerificationResult, PasskeyServiceConfig } from './types.js';
|
|
2
|
-
|
|
1
|
+
import type { PasskeyChallenge, PasskeyChallengeParams, PasskeyStorageAdapter, PasskeyVerificationParams, PasskeyVerificationResult, PasskeyServiceConfig, StoredPasskeyCredential } from './types.js';
|
|
2
|
+
import type { AuthIdentifier } from '../auth-api/types.js';
|
|
3
|
+
export type { CredentialDeviceType, PasskeyChallenge, PasskeyChallengeParams, PasskeyChallengeRecord, PasskeyChallengeMetadata, PasskeyUserDescriptor, PasskeyVerificationParams, PasskeyVerificationResult, PasskeyServiceConfig, PasskeyStorageAdapter, StoredPasskeyCredential } from './types.js';
|
|
3
4
|
type Logger = Pick<typeof console, 'error' | 'warn'>;
|
|
4
5
|
export declare class PasskeyService {
|
|
5
6
|
private readonly config;
|
|
6
7
|
private readonly adapter;
|
|
7
8
|
private readonly logger;
|
|
8
9
|
constructor(config: PasskeyServiceConfig, adapter: PasskeyStorageAdapter, logger?: Logger);
|
|
10
|
+
listUserCredentials(userId: AuthIdentifier): Promise<StoredPasskeyCredential[]>;
|
|
11
|
+
deleteCredential(credentialId: Buffer | string): Promise<boolean>;
|
|
9
12
|
createChallenge(params: PasskeyChallengeParams): Promise<PasskeyChallenge>;
|
|
10
13
|
verifyResponse(params: PasskeyVerificationParams): Promise<PasskeyVerificationResult>;
|
|
11
14
|
private createRegistrationChallenge;
|
|
@@ -40,6 +40,12 @@ class PasskeyService {
|
|
|
40
40
|
this.adapter = adapter;
|
|
41
41
|
this.logger = logger;
|
|
42
42
|
}
|
|
43
|
+
async listUserCredentials(userId) {
|
|
44
|
+
return this.adapter.listUserCredentials(userId);
|
|
45
|
+
}
|
|
46
|
+
async deleteCredential(credentialId) {
|
|
47
|
+
return this.adapter.deleteCredential(credentialId);
|
|
48
|
+
}
|
|
43
49
|
async createChallenge(params) {
|
|
44
50
|
await this.adapter.cleanupChallenges?.(new Date());
|
|
45
51
|
const metadata = {
|
|
@@ -36,6 +36,8 @@ export interface StoredPasskeyCredential {
|
|
|
36
36
|
transports?: AuthenticatorTransportFuture[];
|
|
37
37
|
backedUp: boolean;
|
|
38
38
|
deviceType: CredentialDeviceType;
|
|
39
|
+
createdAt?: Date;
|
|
40
|
+
updatedAt?: Date;
|
|
39
41
|
}
|
|
40
42
|
export interface PasskeyStorageAdapter {
|
|
41
43
|
resolveUser(params: {
|
|
@@ -43,6 +45,7 @@ export interface PasskeyStorageAdapter {
|
|
|
43
45
|
login?: string;
|
|
44
46
|
}): Promise<PasskeyUserDescriptor | null>;
|
|
45
47
|
listUserCredentials(userId: AuthIdentifier): Promise<StoredPasskeyCredential[]>;
|
|
48
|
+
deleteCredential(credentialId: Buffer | string): Promise<boolean>;
|
|
46
49
|
findCredentialById(credentialId: Buffer): Promise<StoredPasskeyCredential | null>;
|
|
47
50
|
saveCredential(record: StoredPasskeyCredential): Promise<void>;
|
|
48
51
|
updateCredentialCounter(credentialId: Buffer, counter: number): Promise<void>;
|
|
@@ -9,7 +9,7 @@ import { ApiModule } from './api-module.js';
|
|
|
9
9
|
import { TokenStore, type JwtDecodeResult, type JwtSignResult, type JwtVerifyResult } from './token/base.js';
|
|
10
10
|
import type { ApiAuthClass, ApiKey } from './api-module.js';
|
|
11
11
|
import type { AuthProviderModule } from './auth-api/module.js';
|
|
12
|
-
import type {
|
|
12
|
+
import type { AuthAdapter, AuthIdentifier } from './auth-api/types.js';
|
|
13
13
|
import type { OAuthStore } from './oauth/base.js';
|
|
14
14
|
import type { AuthCodeData, AuthCodeRequest, OAuthClient } from './oauth/types.js';
|
|
15
15
|
import type { PasskeyService } from './passkey/service.js';
|
|
@@ -122,17 +122,17 @@ export declare class ApiServer {
|
|
|
122
122
|
private canImpersonateAdapter;
|
|
123
123
|
private readonly jwtHelper;
|
|
124
124
|
constructor(config?: Partial<ApiServerConf>);
|
|
125
|
-
authStorage<UserRow, SafeUser>(storage:
|
|
125
|
+
authStorage<UserRow, SafeUser>(storage: AuthAdapter<UserRow, SafeUser>): this;
|
|
126
126
|
/**
|
|
127
127
|
* @deprecated Use {@link ApiServer.authStorage} instead.
|
|
128
128
|
*/
|
|
129
|
-
useAuthStorage<UserRow, SafeUser>(storage:
|
|
129
|
+
useAuthStorage<UserRow, SafeUser>(storage: AuthAdapter<UserRow, SafeUser>): this;
|
|
130
130
|
authModule<UserRow>(module: AuthProviderModule<UserRow>): this;
|
|
131
131
|
/**
|
|
132
132
|
* @deprecated Use {@link ApiServer.authModule} instead.
|
|
133
133
|
*/
|
|
134
134
|
useAuthModule<UserRow>(module: AuthProviderModule<UserRow>): this;
|
|
135
|
-
getAuthStorage():
|
|
135
|
+
getAuthStorage(): AuthAdapter<any, any>;
|
|
136
136
|
getAuthModule(): AuthProviderModule<any>;
|
|
137
137
|
setTokenStore(store: TokenStore): this;
|
|
138
138
|
getTokenStore(): TokenStore | null;
|
|
@@ -10,7 +10,7 @@ import cors from 'cors';
|
|
|
10
10
|
import express from 'express';
|
|
11
11
|
import multer from 'multer';
|
|
12
12
|
import { nullAuthModule } from './auth-api/module.js';
|
|
13
|
-
import {
|
|
13
|
+
import { nullAuthAdapter } from './auth-api/storage.js';
|
|
14
14
|
import { TokenStore } from './token/base.js';
|
|
15
15
|
class JwtHelperStore extends TokenStore {
|
|
16
16
|
async save() {
|
|
@@ -362,7 +362,7 @@ export class ApiServer {
|
|
|
362
362
|
this.config = fillConfig(config);
|
|
363
363
|
this.apiBasePath = this.normalizeApiBasePath(this.config.apiBasePath);
|
|
364
364
|
this.startedAt = Date.now();
|
|
365
|
-
this.storageAdapter =
|
|
365
|
+
this.storageAdapter = nullAuthAdapter;
|
|
366
366
|
this.moduleAdapter = nullAuthModule;
|
|
367
367
|
this.jwtHelper = new JwtHelperStore();
|
|
368
368
|
this.tokenStoreAdapter = this.config.tokenStore ?? null;
|
|
@@ -446,7 +446,7 @@ export class ApiServer {
|
|
|
446
446
|
}
|
|
447
447
|
return this.oauthStoreAdapter;
|
|
448
448
|
}
|
|
449
|
-
//
|
|
449
|
+
// AuthAdapter-compatible helpers (used by AuthModule)
|
|
450
450
|
async getUser(identifier) {
|
|
451
451
|
return this.userStoreAdapter ? this.userStoreAdapter.findUser(identifier) : null;
|
|
452
452
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ApiRequest, type ApiRoute, type ApiServer } from '../api-server-base.js';
|
|
2
2
|
import { BaseAuthModule, type AuthProviderModule } from './module.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type { AuthAdapter, AuthIdentifier } from './types.js';
|
|
4
4
|
import type { OAuthCallbackParams, OAuthCallbackResult, OAuthStartParams, OAuthStartResult } from '../oauth/types.js';
|
|
5
5
|
import type { TokenPair, Token } from '../token/types.js';
|
|
6
6
|
interface CanImpersonateContext<UserEntity> {
|
|
@@ -46,7 +46,7 @@ export default class AuthModule<UserEntity, PublicUser> extends BaseAuthModule<U
|
|
|
46
46
|
private readonly defaultDomain?;
|
|
47
47
|
private readonly canImpersonateHook?;
|
|
48
48
|
constructor(options?: AuthModuleOptions<UserEntity>);
|
|
49
|
-
protected get storage():
|
|
49
|
+
protected get storage(): AuthAdapter<UserEntity, PublicUser>;
|
|
50
50
|
protected canImpersonate(apiReq: ApiRequest, realUser: UserEntity, targetUser: UserEntity): Promise<boolean>;
|
|
51
51
|
protected ensureImpersonationAllowed(apiReq: ApiRequest, realUser: UserEntity, targetUser: UserEntity): Promise<void>;
|
|
52
52
|
protected buildTokenPayload(user: UserEntity, metadata?: TokenMetadata): TokenClaims;
|
|
@@ -57,6 +57,9 @@ export default class AuthModule<UserEntity, PublicUser> extends BaseAuthModule<U
|
|
|
57
57
|
private resolveSessionPreferences;
|
|
58
58
|
private mergeSessionPreferences;
|
|
59
59
|
private sessionPrefsFromRecord;
|
|
60
|
+
private validateCredentialId;
|
|
61
|
+
private normalizeCredentialId;
|
|
62
|
+
private toIsoDate;
|
|
60
63
|
private cookieOptions;
|
|
61
64
|
private setJwtCookies;
|
|
62
65
|
issueTokens(apiReq: ApiRequest, user: UserEntity, metadata?: TokenIssueOptions): Promise<TokenPair>;
|
|
@@ -76,6 +79,8 @@ export default class AuthModule<UserEntity, PublicUser> extends BaseAuthModule<U
|
|
|
76
79
|
private postWhoAmI;
|
|
77
80
|
private postPasskeyChallenge;
|
|
78
81
|
private postPasskeyVerify;
|
|
82
|
+
private getPasskeys;
|
|
83
|
+
private deletePasskey;
|
|
79
84
|
private postImpersonation;
|
|
80
85
|
private deleteImpersonation;
|
|
81
86
|
private getUserFromPasskey;
|
|
@@ -91,6 +96,10 @@ export default class AuthModule<UserEntity, PublicUser> extends BaseAuthModule<U
|
|
|
91
96
|
private resolveClientAuthentication;
|
|
92
97
|
private assertRedirectUriAllowed;
|
|
93
98
|
private resolveUserForOAuth;
|
|
99
|
+
private hasPasskeyService;
|
|
100
|
+
private hasOAuthStore;
|
|
101
|
+
private storageImplements;
|
|
102
|
+
private storageImplementsAll;
|
|
94
103
|
defineRoutes(): ApiRoute[];
|
|
95
104
|
}
|
|
96
105
|
export {};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createHash } 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
|
}
|
|
@@ -181,6 +183,44 @@ class AuthModule extends BaseAuthModule {
|
|
|
181
183
|
}
|
|
182
184
|
return prefs;
|
|
183
185
|
}
|
|
186
|
+
validateCredentialId(apiReq) {
|
|
187
|
+
const paramId = toStringOrNull(apiReq.req.params?.credentialId);
|
|
188
|
+
const bodyId = toStringOrNull(apiReq.req.body?.credentialId);
|
|
189
|
+
const credentialId = paramId ?? bodyId;
|
|
190
|
+
if (!credentialId) {
|
|
191
|
+
throw new ApiError({ code: 400, message: 'credentialId is required' });
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
isoBase64URL.toBuffer(credentialId);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
throw new ApiError({ code: 400, message: 'Invalid credentialId' });
|
|
198
|
+
}
|
|
199
|
+
return credentialId;
|
|
200
|
+
}
|
|
201
|
+
normalizeCredentialId(value) {
|
|
202
|
+
if (Buffer.isBuffer(value)) {
|
|
203
|
+
return value;
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
return Buffer.from(isoBase64URL.toBuffer(value));
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
try {
|
|
210
|
+
return Buffer.from(value, 'base64');
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return Buffer.from(value);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
toIsoDate(value) {
|
|
218
|
+
if (!value) {
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
222
|
+
return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
|
|
223
|
+
}
|
|
184
224
|
cookieOptions(apiReq) {
|
|
185
225
|
const conf = this.server.config;
|
|
186
226
|
const forwarded = apiReq.req.headers['x-forwarded-proto'];
|
|
@@ -596,6 +636,44 @@ class AuthModule extends BaseAuthModule {
|
|
|
596
636
|
const publicUser = this.storage.filterUser(user);
|
|
597
637
|
return [200, { ...tokens, user: publicUser }];
|
|
598
638
|
}
|
|
639
|
+
async getPasskeys(apiReq) {
|
|
640
|
+
if (typeof this.storage.listUserCredentials !== 'function') {
|
|
641
|
+
throw new ApiError({ code: 501, message: 'Passkey credential listing is not configured' });
|
|
642
|
+
}
|
|
643
|
+
const { userId } = await this.resolveActorContext(apiReq);
|
|
644
|
+
const credentials = await this.storage.listUserCredentials(userId);
|
|
645
|
+
const safeCredentials = credentials.map((credential) => {
|
|
646
|
+
const bufferId = this.normalizeCredentialId(credential.credentialId);
|
|
647
|
+
return {
|
|
648
|
+
id: isoBase64URL.fromBuffer(new Uint8Array(bufferId)),
|
|
649
|
+
transports: credential.transports,
|
|
650
|
+
backedUp: credential.backedUp,
|
|
651
|
+
deviceType: credential.deviceType,
|
|
652
|
+
createdAt: this.toIsoDate(credential.createdAt),
|
|
653
|
+
updatedAt: this.toIsoDate(credential.updatedAt)
|
|
654
|
+
};
|
|
655
|
+
});
|
|
656
|
+
return [200, { credentials: safeCredentials }];
|
|
657
|
+
}
|
|
658
|
+
async deletePasskey(apiReq) {
|
|
659
|
+
if (typeof this.storage.listUserCredentials !== 'function' ||
|
|
660
|
+
typeof this.storage.deletePasskeyCredential !== 'function') {
|
|
661
|
+
throw new ApiError({ code: 501, message: 'Passkey credential management is not configured' });
|
|
662
|
+
}
|
|
663
|
+
const { userId } = await this.resolveActorContext(apiReq);
|
|
664
|
+
const credentialId = this.validateCredentialId(apiReq);
|
|
665
|
+
const bufferId = Buffer.from(isoBase64URL.toBuffer(credentialId));
|
|
666
|
+
const credentials = await this.storage.listUserCredentials(userId);
|
|
667
|
+
const owns = credentials.some((credential) => {
|
|
668
|
+
const candidateId = this.normalizeCredentialId(credential.credentialId);
|
|
669
|
+
return isoBase64URL.fromBuffer(new Uint8Array(candidateId)) === credentialId;
|
|
670
|
+
});
|
|
671
|
+
if (!owns) {
|
|
672
|
+
throw new ApiError({ code: 404, message: 'Passkey not found' });
|
|
673
|
+
}
|
|
674
|
+
const deleted = await this.storage.deletePasskeyCredential(bufferId);
|
|
675
|
+
return [200, { deleted }];
|
|
676
|
+
}
|
|
599
677
|
async postImpersonation(apiReq) {
|
|
600
678
|
this.assertAuthReady();
|
|
601
679
|
const { targetIdentifier, metadata } = this.parseImpersonationRequest(apiReq);
|
|
@@ -939,47 +1017,91 @@ class AuthModule extends BaseAuthModule {
|
|
|
939
1017
|
}
|
|
940
1018
|
throw new ApiError({ code: 401, message: 'Authorization requires user authentication' });
|
|
941
1019
|
}
|
|
1020
|
+
hasPasskeyService() {
|
|
1021
|
+
const storageAny = this.storage;
|
|
1022
|
+
if (storageAny.passkeyService || storageAny.passkeyStore) {
|
|
1023
|
+
return true;
|
|
1024
|
+
}
|
|
1025
|
+
if (storageAny.adapter?.passkeyService || storageAny.adapter?.passkeyStore) {
|
|
1026
|
+
return true;
|
|
1027
|
+
}
|
|
1028
|
+
const serverAny = this.server;
|
|
1029
|
+
return !!serverAny.passkeyServiceAdapter;
|
|
1030
|
+
}
|
|
1031
|
+
hasOAuthStore() {
|
|
1032
|
+
const storageAny = this.storage;
|
|
1033
|
+
if (storageAny.oauthStore) {
|
|
1034
|
+
return true;
|
|
1035
|
+
}
|
|
1036
|
+
if (storageAny.adapter?.oauthStore) {
|
|
1037
|
+
return true;
|
|
1038
|
+
}
|
|
1039
|
+
const serverAny = this.server;
|
|
1040
|
+
return !!serverAny.oauthStoreAdapter;
|
|
1041
|
+
}
|
|
1042
|
+
storageImplements(key) {
|
|
1043
|
+
const candidate = this.storage[key];
|
|
1044
|
+
if (typeof candidate !== 'function') {
|
|
1045
|
+
return false;
|
|
1046
|
+
}
|
|
1047
|
+
const baseImpl = BaseAuthAdapter.prototype[key];
|
|
1048
|
+
return candidate !== baseImpl;
|
|
1049
|
+
}
|
|
1050
|
+
storageImplementsAll(keys) {
|
|
1051
|
+
return keys.every((key) => this.storageImplements(key));
|
|
1052
|
+
}
|
|
942
1053
|
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
|
-
|
|
1054
|
+
const routes = [];
|
|
1055
|
+
const coreAuthSupported = this.storageImplementsAll([
|
|
1056
|
+
'getUser',
|
|
1057
|
+
'getUserPasswordHash',
|
|
1058
|
+
'getUserId',
|
|
1059
|
+
'verifyPassword',
|
|
1060
|
+
'filterUser',
|
|
1061
|
+
'storeToken',
|
|
1062
|
+
'getToken',
|
|
1063
|
+
'deleteToken'
|
|
1064
|
+
]);
|
|
1065
|
+
if (!coreAuthSupported) {
|
|
1066
|
+
return routes;
|
|
1067
|
+
}
|
|
1068
|
+
routes.push({
|
|
1069
|
+
method: 'post',
|
|
1070
|
+
path: '/v1/login',
|
|
1071
|
+
handler: (req) => this.postLogin(req),
|
|
1072
|
+
auth: { type: 'none', req: 'any' }
|
|
1073
|
+
}, {
|
|
1074
|
+
method: 'post',
|
|
1075
|
+
path: '/v1/refresh',
|
|
1076
|
+
handler: (req) => this.postRefresh(req),
|
|
1077
|
+
auth: { type: 'none', req: 'any' }
|
|
1078
|
+
}, {
|
|
1079
|
+
method: 'post',
|
|
1080
|
+
path: '/v1/logout',
|
|
1081
|
+
handler: (req) => this.postLogout(req),
|
|
1082
|
+
auth: { type: 'maybe', req: 'any' }
|
|
1083
|
+
}, {
|
|
1084
|
+
method: 'post',
|
|
1085
|
+
path: '/v1/whoami',
|
|
1086
|
+
handler: (req) => this.postWhoAmI(req),
|
|
1087
|
+
auth: { type: 'maybe', req: 'any' }
|
|
1088
|
+
}, {
|
|
1089
|
+
method: 'post',
|
|
1090
|
+
path: '/v1/impersonations',
|
|
1091
|
+
handler: (req) => this.postImpersonation(req),
|
|
1092
|
+
auth: { type: 'strict', req: 'any' }
|
|
1093
|
+
}, {
|
|
1094
|
+
method: 'delete',
|
|
1095
|
+
path: '/v1/impersonations',
|
|
1096
|
+
handler: (req) => this.deleteImpersonation(req),
|
|
1097
|
+
auth: { type: 'strict', req: 'any' }
|
|
1098
|
+
});
|
|
1099
|
+
const passkeysSupported = this.hasPasskeyService() &&
|
|
1100
|
+
this.storageImplements('createPasskeyChallenge') &&
|
|
1101
|
+
this.storageImplements('verifyPasskeyResponse');
|
|
1102
|
+
const passkeyCredentialsSupported = passkeysSupported &&
|
|
1103
|
+
this.storageImplements('listUserCredentials') &&
|
|
1104
|
+
this.storageImplements('deletePasskeyCredential');
|
|
983
1105
|
if (passkeysSupported) {
|
|
984
1106
|
routes.push({
|
|
985
1107
|
method: 'post',
|
|
@@ -992,6 +1114,19 @@ class AuthModule extends BaseAuthModule {
|
|
|
992
1114
|
handler: (req) => this.postPasskeyVerify(req),
|
|
993
1115
|
auth: { type: 'none', req: 'any' }
|
|
994
1116
|
});
|
|
1117
|
+
if (passkeyCredentialsSupported) {
|
|
1118
|
+
routes.push({
|
|
1119
|
+
method: 'get',
|
|
1120
|
+
path: '/v1/passkeys',
|
|
1121
|
+
handler: (req) => this.getPasskeys(req),
|
|
1122
|
+
auth: { type: 'strict', req: 'any' }
|
|
1123
|
+
}, {
|
|
1124
|
+
method: 'delete',
|
|
1125
|
+
path: '/v1/passkeys/:credentialId?',
|
|
1126
|
+
handler: (req) => this.deletePasskey(req),
|
|
1127
|
+
auth: { type: 'strict', req: 'any' }
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
995
1130
|
}
|
|
996
1131
|
const externalOAuthSupported = typeof this.server.initiateOAuth === 'function' && typeof this.server.completeOAuth === 'function';
|
|
997
1132
|
if (externalOAuthSupported) {
|
|
@@ -1007,9 +1142,10 @@ class AuthModule extends BaseAuthModule {
|
|
|
1007
1142
|
auth: { type: 'none', req: 'any' }
|
|
1008
1143
|
});
|
|
1009
1144
|
}
|
|
1010
|
-
const oauthStorageSupported =
|
|
1011
|
-
|
|
1012
|
-
|
|
1145
|
+
const oauthStorageSupported = this.hasOAuthStore() &&
|
|
1146
|
+
this.storageImplements('getClient') &&
|
|
1147
|
+
this.storageImplements('createAuthCode') &&
|
|
1148
|
+
this.storageImplements('consumeAuthCode');
|
|
1013
1149
|
if (oauthStorageSupported) {
|
|
1014
1150
|
routes.push({
|
|
1015
1151
|
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>;
|