@technomoron/api-server-base 2.0.0-beta.11 → 2.0.0-beta.12
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/auth-api/auth-module.js +14 -15
- package/dist/cjs/passkey/memory.js +5 -3
- package/dist/cjs/passkey/models.d.ts +7 -2
- package/dist/cjs/passkey/models.js +34 -4
- package/dist/cjs/passkey/sequelize.d.ts +7 -1
- package/dist/cjs/passkey/sequelize.js +57 -8
- package/dist/cjs/passkey/service.d.ts +1 -1
- package/dist/cjs/passkey/service.js +21 -15
- package/dist/cjs/passkey/types.d.ts +9 -9
- package/dist/esm/auth-api/auth-module.js +14 -15
- package/dist/esm/passkey/memory.js +5 -3
- package/dist/esm/passkey/models.d.ts +7 -2
- package/dist/esm/passkey/models.js +34 -4
- package/dist/esm/passkey/sequelize.d.ts +7 -1
- package/dist/esm/passkey/sequelize.js +57 -8
- package/dist/esm/passkey/service.d.ts +1 -1
- package/dist/esm/passkey/service.js +21 -15
- package/dist/esm/passkey/types.d.ts +9 -9
- package/package.json +1 -1
|
@@ -600,15 +600,7 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
600
600
|
const params = {
|
|
601
601
|
action,
|
|
602
602
|
login: toStringOrNull(body.login) ?? undefined,
|
|
603
|
-
userId: isAuthIdentifier(body.userId) ? body.userId : undefined
|
|
604
|
-
userAgent: toStringOrNull(body.userAgent) ?? undefined,
|
|
605
|
-
domain: toStringOrNull(body.domain) ?? undefined,
|
|
606
|
-
fingerprint: toStringOrNull(body.fingerprint) ?? undefined,
|
|
607
|
-
label: toStringOrNull(body.label) ?? undefined,
|
|
608
|
-
browser: toStringOrNull(body.browser) ?? undefined,
|
|
609
|
-
device: toStringOrNull(body.device) ?? undefined,
|
|
610
|
-
ip: toStringOrNull(body.ip) ?? undefined,
|
|
611
|
-
os: toStringOrNull(body.os) ?? undefined
|
|
603
|
+
userId: isAuthIdentifier(body.userId) ? body.userId : undefined
|
|
612
604
|
};
|
|
613
605
|
const challenge = await this.storage.createPasskeyChallenge(params);
|
|
614
606
|
return [200, challenge];
|
|
@@ -624,18 +616,25 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
624
616
|
if (!expectedChallenge || typeof response !== 'object' || response === null) {
|
|
625
617
|
throw new api_server_base_js_1.ApiError({ code: 400, message: 'Malformed passkey verification payload' });
|
|
626
618
|
}
|
|
627
|
-
const
|
|
628
|
-
expectedChallenge,
|
|
629
|
-
response: response,
|
|
630
|
-
login: toStringOrNull(body.login) ?? undefined,
|
|
631
|
-
userId: isAuthIdentifier(body.userId) ? body.userId : undefined,
|
|
619
|
+
const rawMetadata = {
|
|
632
620
|
domain: toStringOrNull(body.domain) ?? undefined,
|
|
633
621
|
fingerprint: toStringOrNull(body.fingerprint) ?? undefined,
|
|
634
622
|
label: toStringOrNull(body.label) ?? undefined,
|
|
635
623
|
browser: toStringOrNull(body.browser) ?? undefined,
|
|
636
624
|
device: toStringOrNull(body.device) ?? undefined,
|
|
637
625
|
ip: toStringOrNull(body.ip) ?? undefined,
|
|
638
|
-
os: toStringOrNull(body.os) ?? undefined
|
|
626
|
+
os: toStringOrNull(body.os) ?? undefined
|
|
627
|
+
};
|
|
628
|
+
const clientInfo = apiReq.getClientInfo();
|
|
629
|
+
const userAgent = toStringOrNull(body.userAgent) ?? (clientInfo.ua ? clientInfo.ua : null);
|
|
630
|
+
const requestMetadata = this.enrichTokenMetadata(apiReq, rawMetadata);
|
|
631
|
+
const params = {
|
|
632
|
+
expectedChallenge,
|
|
633
|
+
response: response,
|
|
634
|
+
login: toStringOrNull(body.login) ?? undefined,
|
|
635
|
+
userId: isAuthIdentifier(body.userId) ? body.userId : undefined,
|
|
636
|
+
userAgent: userAgent ?? undefined,
|
|
637
|
+
...requestMetadata,
|
|
639
638
|
...sessionPrefs
|
|
640
639
|
};
|
|
641
640
|
const result = await this.storage.verifyPasskeyResponse(params);
|
|
@@ -62,9 +62,11 @@ class MemoryPasskeyStore extends base_js_1.PasskeyStore {
|
|
|
62
62
|
}
|
|
63
63
|
async saveChallenge(record) {
|
|
64
64
|
this.challenges.set(record.challenge, {
|
|
65
|
-
|
|
65
|
+
challenge: record.challenge,
|
|
66
|
+
action: record.action,
|
|
66
67
|
userId: record.userId !== undefined ? normalizeUserId(record.userId) : undefined,
|
|
67
|
-
|
|
68
|
+
login: record.login ?? undefined,
|
|
69
|
+
expiresAt: record.expiresAt
|
|
68
70
|
});
|
|
69
71
|
}
|
|
70
72
|
async consumeChallenge(challenge) {
|
|
@@ -73,7 +75,7 @@ class MemoryPasskeyStore extends base_js_1.PasskeyStore {
|
|
|
73
75
|
return null;
|
|
74
76
|
}
|
|
75
77
|
this.challenges.delete(challenge);
|
|
76
|
-
return { ...record
|
|
78
|
+
return { ...record };
|
|
77
79
|
}
|
|
78
80
|
async cleanupChallenges(now) {
|
|
79
81
|
for (const [challenge, record] of this.challenges.entries()) {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Model, type InferAttributes, type InferCreationAttributes, type ModelStatic, type Sequelize } from 'sequelize';
|
|
2
|
-
import type { PasskeyChallengeMetadata } from './service.js';
|
|
3
2
|
export declare class PasskeyCredentialModel extends Model<InferAttributes<PasskeyCredentialModel>, InferCreationAttributes<PasskeyCredentialModel>> {
|
|
4
3
|
credentialId: Buffer;
|
|
5
4
|
userId: number;
|
|
@@ -8,6 +7,13 @@ export declare class PasskeyCredentialModel extends Model<InferAttributes<Passke
|
|
|
8
7
|
transports: string[] | null;
|
|
9
8
|
backedUp: boolean;
|
|
10
9
|
deviceType: string;
|
|
10
|
+
label: string | null;
|
|
11
|
+
createdDomain: string | null;
|
|
12
|
+
createdUserAgent: string | null;
|
|
13
|
+
createdBrowser: string | null;
|
|
14
|
+
createdOs: string | null;
|
|
15
|
+
createdDevice: string | null;
|
|
16
|
+
createdIp: string | null;
|
|
11
17
|
createdAt?: Date;
|
|
12
18
|
updatedAt?: Date;
|
|
13
19
|
}
|
|
@@ -16,7 +22,6 @@ export declare class PasskeyChallengeModel extends Model<InferAttributes<Passkey
|
|
|
16
22
|
action: 'register' | 'authenticate';
|
|
17
23
|
userId: number | null;
|
|
18
24
|
login: string | null;
|
|
19
|
-
metadata: PasskeyChallengeMetadata | null;
|
|
20
25
|
expiresAt: Date;
|
|
21
26
|
createdAt?: Date;
|
|
22
27
|
updatedAt?: Date;
|
|
@@ -67,6 +67,40 @@ function initPasskeyCredentialModel(sequelize) {
|
|
|
67
67
|
type: sequelize_1.DataTypes.STRING(32),
|
|
68
68
|
allowNull: false,
|
|
69
69
|
defaultValue: 'multiDevice'
|
|
70
|
+
},
|
|
71
|
+
label: {
|
|
72
|
+
type: sequelize_1.DataTypes.STRING(120),
|
|
73
|
+
allowNull: true
|
|
74
|
+
},
|
|
75
|
+
createdDomain: {
|
|
76
|
+
field: 'created_domain',
|
|
77
|
+
type: sequelize_1.DataTypes.STRING(255),
|
|
78
|
+
allowNull: true
|
|
79
|
+
},
|
|
80
|
+
createdUserAgent: {
|
|
81
|
+
field: 'created_user_agent',
|
|
82
|
+
type: sequelize_1.DataTypes.TEXT,
|
|
83
|
+
allowNull: true
|
|
84
|
+
},
|
|
85
|
+
createdBrowser: {
|
|
86
|
+
field: 'created_browser',
|
|
87
|
+
type: sequelize_1.DataTypes.STRING(120),
|
|
88
|
+
allowNull: true
|
|
89
|
+
},
|
|
90
|
+
createdOs: {
|
|
91
|
+
field: 'created_os',
|
|
92
|
+
type: sequelize_1.DataTypes.STRING(120),
|
|
93
|
+
allowNull: true
|
|
94
|
+
},
|
|
95
|
+
createdDevice: {
|
|
96
|
+
field: 'created_device',
|
|
97
|
+
type: sequelize_1.DataTypes.STRING(120),
|
|
98
|
+
allowNull: true
|
|
99
|
+
},
|
|
100
|
+
createdIp: {
|
|
101
|
+
field: 'created_ip',
|
|
102
|
+
type: sequelize_1.DataTypes.STRING(45),
|
|
103
|
+
allowNull: true
|
|
70
104
|
}
|
|
71
105
|
}, {
|
|
72
106
|
sequelize,
|
|
@@ -96,10 +130,6 @@ function initPasskeyChallengeModel(sequelize) {
|
|
|
96
130
|
type: sequelize_1.DataTypes.STRING(128),
|
|
97
131
|
allowNull: true
|
|
98
132
|
},
|
|
99
|
-
metadata: {
|
|
100
|
-
type: sequelize_1.DataTypes.JSON,
|
|
101
|
-
allowNull: true
|
|
102
|
-
},
|
|
103
133
|
expiresAt: {
|
|
104
134
|
field: 'expires_at',
|
|
105
135
|
type: sequelize_1.DataTypes.DATE,
|
|
@@ -10,6 +10,13 @@ declare class PasskeyCredentialModel extends Model<InferAttributes<PasskeyCreden
|
|
|
10
10
|
transports: string[] | null;
|
|
11
11
|
backedUp: boolean;
|
|
12
12
|
deviceType: string;
|
|
13
|
+
label: string | null;
|
|
14
|
+
createdDomain: string | null;
|
|
15
|
+
createdUserAgent: string | null;
|
|
16
|
+
createdBrowser: string | null;
|
|
17
|
+
createdOs: string | null;
|
|
18
|
+
createdDevice: string | null;
|
|
19
|
+
createdIp: string | null;
|
|
13
20
|
createdAt?: Date;
|
|
14
21
|
updatedAt?: Date;
|
|
15
22
|
}
|
|
@@ -18,7 +25,6 @@ declare class PasskeyChallengeModel extends Model<InferAttributes<PasskeyChallen
|
|
|
18
25
|
action: 'register' | 'authenticate';
|
|
19
26
|
userId: number | null;
|
|
20
27
|
login: string | null;
|
|
21
|
-
metadata: Record<string, unknown> | null;
|
|
22
28
|
expiresAt: Date;
|
|
23
29
|
createdAt?: Date;
|
|
24
30
|
updatedAt?: Date;
|
|
@@ -76,6 +76,40 @@ function initPasskeyCredentialModel(sequelize) {
|
|
|
76
76
|
type: sequelize_1.DataTypes.STRING(32),
|
|
77
77
|
allowNull: false,
|
|
78
78
|
defaultValue: 'multiDevice'
|
|
79
|
+
},
|
|
80
|
+
label: {
|
|
81
|
+
type: sequelize_1.DataTypes.STRING(120),
|
|
82
|
+
allowNull: true
|
|
83
|
+
},
|
|
84
|
+
createdDomain: {
|
|
85
|
+
field: 'created_domain',
|
|
86
|
+
type: sequelize_1.DataTypes.STRING(255),
|
|
87
|
+
allowNull: true
|
|
88
|
+
},
|
|
89
|
+
createdUserAgent: {
|
|
90
|
+
field: 'created_user_agent',
|
|
91
|
+
type: sequelize_1.DataTypes.TEXT,
|
|
92
|
+
allowNull: true
|
|
93
|
+
},
|
|
94
|
+
createdBrowser: {
|
|
95
|
+
field: 'created_browser',
|
|
96
|
+
type: sequelize_1.DataTypes.STRING(120),
|
|
97
|
+
allowNull: true
|
|
98
|
+
},
|
|
99
|
+
createdOs: {
|
|
100
|
+
field: 'created_os',
|
|
101
|
+
type: sequelize_1.DataTypes.STRING(120),
|
|
102
|
+
allowNull: true
|
|
103
|
+
},
|
|
104
|
+
createdDevice: {
|
|
105
|
+
field: 'created_device',
|
|
106
|
+
type: sequelize_1.DataTypes.STRING(120),
|
|
107
|
+
allowNull: true
|
|
108
|
+
},
|
|
109
|
+
createdIp: {
|
|
110
|
+
field: 'created_ip',
|
|
111
|
+
type: sequelize_1.DataTypes.STRING(45),
|
|
112
|
+
allowNull: true
|
|
79
113
|
}
|
|
80
114
|
}, {
|
|
81
115
|
sequelize,
|
|
@@ -105,10 +139,6 @@ function initPasskeyChallengeModel(sequelize) {
|
|
|
105
139
|
type: sequelize_1.DataTypes.STRING(128),
|
|
106
140
|
allowNull: true
|
|
107
141
|
},
|
|
108
|
-
metadata: {
|
|
109
|
-
type: sequelize_1.DataTypes.JSON,
|
|
110
|
-
allowNull: true
|
|
111
|
-
},
|
|
112
142
|
expiresAt: {
|
|
113
143
|
field: 'expires_at',
|
|
114
144
|
type: sequelize_1.DataTypes.DATE,
|
|
@@ -148,6 +178,13 @@ class SequelizePasskeyStore extends base_js_1.PasskeyStore {
|
|
|
148
178
|
transports: (model.transports ?? undefined),
|
|
149
179
|
backedUp: model.backedUp,
|
|
150
180
|
deviceType: model.deviceType,
|
|
181
|
+
label: model.label ?? undefined,
|
|
182
|
+
createdDomain: model.createdDomain ?? undefined,
|
|
183
|
+
createdUserAgent: model.createdUserAgent ?? undefined,
|
|
184
|
+
createdBrowser: model.createdBrowser ?? undefined,
|
|
185
|
+
createdOs: model.createdOs ?? undefined,
|
|
186
|
+
createdDevice: model.createdDevice ?? undefined,
|
|
187
|
+
createdIp: model.createdIp ?? undefined,
|
|
151
188
|
createdAt: model.createdAt ?? undefined,
|
|
152
189
|
updatedAt: model.updatedAt ?? undefined
|
|
153
190
|
}));
|
|
@@ -170,6 +207,13 @@ class SequelizePasskeyStore extends base_js_1.PasskeyStore {
|
|
|
170
207
|
transports: (model.transports ?? undefined),
|
|
171
208
|
backedUp: model.backedUp,
|
|
172
209
|
deviceType: model.deviceType,
|
|
210
|
+
label: model.label ?? undefined,
|
|
211
|
+
createdDomain: model.createdDomain ?? undefined,
|
|
212
|
+
createdUserAgent: model.createdUserAgent ?? undefined,
|
|
213
|
+
createdBrowser: model.createdBrowser ?? undefined,
|
|
214
|
+
createdOs: model.createdOs ?? undefined,
|
|
215
|
+
createdDevice: model.createdDevice ?? undefined,
|
|
216
|
+
createdIp: model.createdIp ?? undefined,
|
|
173
217
|
createdAt: model.createdAt ?? undefined,
|
|
174
218
|
updatedAt: model.updatedAt ?? undefined
|
|
175
219
|
};
|
|
@@ -182,7 +226,14 @@ class SequelizePasskeyStore extends base_js_1.PasskeyStore {
|
|
|
182
226
|
counter: record.counter,
|
|
183
227
|
transports: record.transports ?? null,
|
|
184
228
|
backedUp: record.backedUp,
|
|
185
|
-
deviceType: record.deviceType
|
|
229
|
+
deviceType: record.deviceType,
|
|
230
|
+
label: record.label ?? null,
|
|
231
|
+
createdDomain: record.createdDomain ?? null,
|
|
232
|
+
createdUserAgent: record.createdUserAgent ?? null,
|
|
233
|
+
createdBrowser: record.createdBrowser ?? null,
|
|
234
|
+
createdOs: record.createdOs ?? null,
|
|
235
|
+
createdDevice: record.createdDevice ?? null,
|
|
236
|
+
createdIp: record.createdIp ?? null
|
|
186
237
|
});
|
|
187
238
|
}
|
|
188
239
|
async updateCredentialCounter(credentialId, counter) {
|
|
@@ -194,7 +245,6 @@ class SequelizePasskeyStore extends base_js_1.PasskeyStore {
|
|
|
194
245
|
action: record.action,
|
|
195
246
|
userId: record.userId !== undefined ? normalizeUserId(record.userId) : null,
|
|
196
247
|
login: record.login ?? null,
|
|
197
|
-
metadata: (record.metadata ?? {}),
|
|
198
248
|
expiresAt: record.expiresAt
|
|
199
249
|
});
|
|
200
250
|
}
|
|
@@ -209,8 +259,7 @@ class SequelizePasskeyStore extends base_js_1.PasskeyStore {
|
|
|
209
259
|
action: model.action,
|
|
210
260
|
userId: model.userId ?? undefined,
|
|
211
261
|
login: model.login ?? undefined,
|
|
212
|
-
expiresAt: model.expiresAt
|
|
213
|
-
metadata: model.metadata ?? {}
|
|
262
|
+
expiresAt: model.expiresAt
|
|
214
263
|
};
|
|
215
264
|
}
|
|
216
265
|
async cleanupChallenges(now) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { PasskeyChallenge, PasskeyChallengeParams, PasskeyStorageAdapter, PasskeyVerificationParams, PasskeyVerificationResult, PasskeyServiceConfig, StoredPasskeyCredential } from './types.js';
|
|
2
2
|
import type { AuthIdentifier } from '../auth-api/types.js';
|
|
3
|
-
export type { CredentialDeviceType, PasskeyChallenge, PasskeyChallengeParams, PasskeyChallengeRecord,
|
|
3
|
+
export type { CredentialDeviceType, PasskeyChallenge, PasskeyChallengeParams, PasskeyChallengeRecord, PasskeyUserDescriptor, PasskeyVerificationParams, PasskeyVerificationResult, PasskeyServiceConfig, PasskeyStorageAdapter, StoredPasskeyCredential } from './types.js';
|
|
4
4
|
type Logger = Pick<typeof console, 'error' | 'warn'>;
|
|
5
5
|
export declare class PasskeyService {
|
|
6
6
|
private readonly config;
|
|
@@ -54,6 +54,13 @@ function sanitizeTransports(input) {
|
|
|
54
54
|
.filter((value) => ALLOWED_TRANSPORTS.includes(value));
|
|
55
55
|
return filtered.length > 0 ? filtered : undefined;
|
|
56
56
|
}
|
|
57
|
+
function toOptionalString(value) {
|
|
58
|
+
if (typeof value !== 'string') {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
const trimmed = value.trim();
|
|
62
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
63
|
+
}
|
|
57
64
|
function toBase64Url(buffer) {
|
|
58
65
|
return helpers_1.isoBase64URL.fromBuffer(new Uint8Array(buffer));
|
|
59
66
|
}
|
|
@@ -127,17 +134,11 @@ class PasskeyService {
|
|
|
127
134
|
}
|
|
128
135
|
async createChallenge(params) {
|
|
129
136
|
await this.adapter.cleanupChallenges?.(new Date());
|
|
130
|
-
const metadata = {
|
|
131
|
-
domain: typeof params.domain === 'string' ? params.domain : undefined,
|
|
132
|
-
fingerprint: typeof params.fingerprint === 'string' ? params.fingerprint : undefined,
|
|
133
|
-
label: typeof params.label === 'string' ? params.label : undefined,
|
|
134
|
-
userAgent: typeof params.userAgent === 'string' ? params.userAgent : undefined
|
|
135
|
-
};
|
|
136
137
|
if (params.action === 'register') {
|
|
137
|
-
return this.createRegistrationChallenge(params
|
|
138
|
+
return this.createRegistrationChallenge(params);
|
|
138
139
|
}
|
|
139
140
|
if (params.action === 'authenticate') {
|
|
140
|
-
return this.createAuthenticationChallenge(params
|
|
141
|
+
return this.createAuthenticationChallenge(params);
|
|
141
142
|
}
|
|
142
143
|
throw new Error(`Unsupported passkey action: ${String(params.action)}`);
|
|
143
144
|
}
|
|
@@ -163,7 +164,7 @@ class PasskeyService {
|
|
|
163
164
|
}
|
|
164
165
|
return { verified: false };
|
|
165
166
|
}
|
|
166
|
-
async createRegistrationChallenge(params
|
|
167
|
+
async createRegistrationChallenge(params) {
|
|
167
168
|
const user = await this.requireUser({ userId: params.userId, login: params.login });
|
|
168
169
|
const existing = await this.adapter.listUserCredentials(user.id);
|
|
169
170
|
const excludeCredentials = existing.map((credential) => {
|
|
@@ -186,8 +187,7 @@ class PasskeyService {
|
|
|
186
187
|
action: 'register',
|
|
187
188
|
userId: user.id,
|
|
188
189
|
login: user.login,
|
|
189
|
-
expiresAt
|
|
190
|
-
metadata
|
|
190
|
+
expiresAt
|
|
191
191
|
});
|
|
192
192
|
return {
|
|
193
193
|
challenge: options.challenge,
|
|
@@ -196,7 +196,7 @@ class PasskeyService {
|
|
|
196
196
|
publicKey: options
|
|
197
197
|
};
|
|
198
198
|
}
|
|
199
|
-
async createAuthenticationChallenge(params
|
|
199
|
+
async createAuthenticationChallenge(params) {
|
|
200
200
|
const user = await this.requireUser({ userId: params.userId, login: params.login });
|
|
201
201
|
const credentials = await this.adapter.listUserCredentials(user.id);
|
|
202
202
|
const allowCredentials = credentials.map((credential) => {
|
|
@@ -216,8 +216,7 @@ class PasskeyService {
|
|
|
216
216
|
action: 'authenticate',
|
|
217
217
|
userId: user.id,
|
|
218
218
|
login: user.login,
|
|
219
|
-
expiresAt
|
|
220
|
-
metadata
|
|
219
|
+
expiresAt
|
|
221
220
|
});
|
|
222
221
|
return {
|
|
223
222
|
challenge: options.challenge,
|
|
@@ -295,7 +294,14 @@ class PasskeyService {
|
|
|
295
294
|
counter: registrationInfo.counter ?? 0,
|
|
296
295
|
transports: sanitizeTransports(params.response.transports),
|
|
297
296
|
backedUp: registrationInfo.credentialDeviceType === 'multiDevice',
|
|
298
|
-
deviceType: registrationInfo.credentialDeviceType
|
|
297
|
+
deviceType: registrationInfo.credentialDeviceType,
|
|
298
|
+
label: toOptionalString(params.label),
|
|
299
|
+
createdDomain: toOptionalString(params.domain),
|
|
300
|
+
createdUserAgent: toOptionalString(params.userAgent),
|
|
301
|
+
createdBrowser: toOptionalString(params.browser),
|
|
302
|
+
createdOs: toOptionalString(params.os),
|
|
303
|
+
createdDevice: toOptionalString(params.device),
|
|
304
|
+
createdIp: toOptionalString(params.ip)
|
|
299
305
|
});
|
|
300
306
|
return { verified: true, userId: user.id, login: user.login };
|
|
301
307
|
}
|
|
@@ -9,19 +9,12 @@ export interface PasskeyServiceConfig {
|
|
|
9
9
|
timeoutMs: number;
|
|
10
10
|
userVerification?: 'preferred' | 'required' | 'discouraged';
|
|
11
11
|
}
|
|
12
|
-
export interface PasskeyChallengeMetadata {
|
|
13
|
-
domain?: string;
|
|
14
|
-
fingerprint?: string;
|
|
15
|
-
label?: string;
|
|
16
|
-
userAgent?: string;
|
|
17
|
-
}
|
|
18
12
|
export interface PasskeyChallengeRecord {
|
|
19
13
|
challenge: string;
|
|
20
14
|
action: 'register' | 'authenticate';
|
|
21
15
|
userId?: AuthIdentifier;
|
|
22
16
|
login?: string;
|
|
23
17
|
expiresAt: Date;
|
|
24
|
-
metadata: PasskeyChallengeMetadata;
|
|
25
18
|
}
|
|
26
19
|
export interface PasskeyUserDescriptor {
|
|
27
20
|
id: AuthIdentifier;
|
|
@@ -36,6 +29,13 @@ export interface StoredPasskeyCredential {
|
|
|
36
29
|
transports?: AuthenticatorTransportFuture[];
|
|
37
30
|
backedUp: boolean;
|
|
38
31
|
deviceType: CredentialDeviceType;
|
|
32
|
+
label?: string;
|
|
33
|
+
createdDomain?: string;
|
|
34
|
+
createdUserAgent?: string;
|
|
35
|
+
createdBrowser?: string;
|
|
36
|
+
createdOs?: string;
|
|
37
|
+
createdDevice?: string;
|
|
38
|
+
createdIp?: string;
|
|
39
39
|
createdAt?: Date;
|
|
40
40
|
updatedAt?: Date;
|
|
41
41
|
}
|
|
@@ -53,10 +53,9 @@ export interface PasskeyStorageAdapter {
|
|
|
53
53
|
consumeChallenge(challenge: string): Promise<PasskeyChallengeRecord | null>;
|
|
54
54
|
cleanupChallenges?(now: Date): Promise<void>;
|
|
55
55
|
}
|
|
56
|
-
export interface PasskeyChallengeParams
|
|
56
|
+
export interface PasskeyChallengeParams {
|
|
57
57
|
action: 'register' | 'authenticate';
|
|
58
58
|
login?: string;
|
|
59
|
-
userAgent?: string;
|
|
60
59
|
userId?: AuthIdentifier;
|
|
61
60
|
}
|
|
62
61
|
export interface PasskeyChallenge extends Record<string, unknown> {
|
|
@@ -69,6 +68,7 @@ export interface PasskeyVerificationParams extends Partial<Omit<Token, 'userId'>
|
|
|
69
68
|
login?: string;
|
|
70
69
|
response: Record<string, unknown>;
|
|
71
70
|
userId?: AuthIdentifier;
|
|
71
|
+
userAgent?: string;
|
|
72
72
|
}
|
|
73
73
|
export interface PasskeyVerificationResult extends Record<string, unknown> {
|
|
74
74
|
login?: string;
|
|
@@ -598,15 +598,7 @@ class AuthModule extends BaseAuthModule {
|
|
|
598
598
|
const params = {
|
|
599
599
|
action,
|
|
600
600
|
login: toStringOrNull(body.login) ?? undefined,
|
|
601
|
-
userId: isAuthIdentifier(body.userId) ? body.userId : undefined
|
|
602
|
-
userAgent: toStringOrNull(body.userAgent) ?? undefined,
|
|
603
|
-
domain: toStringOrNull(body.domain) ?? undefined,
|
|
604
|
-
fingerprint: toStringOrNull(body.fingerprint) ?? undefined,
|
|
605
|
-
label: toStringOrNull(body.label) ?? undefined,
|
|
606
|
-
browser: toStringOrNull(body.browser) ?? undefined,
|
|
607
|
-
device: toStringOrNull(body.device) ?? undefined,
|
|
608
|
-
ip: toStringOrNull(body.ip) ?? undefined,
|
|
609
|
-
os: toStringOrNull(body.os) ?? undefined
|
|
601
|
+
userId: isAuthIdentifier(body.userId) ? body.userId : undefined
|
|
610
602
|
};
|
|
611
603
|
const challenge = await this.storage.createPasskeyChallenge(params);
|
|
612
604
|
return [200, challenge];
|
|
@@ -622,18 +614,25 @@ class AuthModule extends BaseAuthModule {
|
|
|
622
614
|
if (!expectedChallenge || typeof response !== 'object' || response === null) {
|
|
623
615
|
throw new ApiError({ code: 400, message: 'Malformed passkey verification payload' });
|
|
624
616
|
}
|
|
625
|
-
const
|
|
626
|
-
expectedChallenge,
|
|
627
|
-
response: response,
|
|
628
|
-
login: toStringOrNull(body.login) ?? undefined,
|
|
629
|
-
userId: isAuthIdentifier(body.userId) ? body.userId : undefined,
|
|
617
|
+
const rawMetadata = {
|
|
630
618
|
domain: toStringOrNull(body.domain) ?? undefined,
|
|
631
619
|
fingerprint: toStringOrNull(body.fingerprint) ?? undefined,
|
|
632
620
|
label: toStringOrNull(body.label) ?? undefined,
|
|
633
621
|
browser: toStringOrNull(body.browser) ?? undefined,
|
|
634
622
|
device: toStringOrNull(body.device) ?? undefined,
|
|
635
623
|
ip: toStringOrNull(body.ip) ?? undefined,
|
|
636
|
-
os: toStringOrNull(body.os) ?? undefined
|
|
624
|
+
os: toStringOrNull(body.os) ?? undefined
|
|
625
|
+
};
|
|
626
|
+
const clientInfo = apiReq.getClientInfo();
|
|
627
|
+
const userAgent = toStringOrNull(body.userAgent) ?? (clientInfo.ua ? clientInfo.ua : null);
|
|
628
|
+
const requestMetadata = this.enrichTokenMetadata(apiReq, rawMetadata);
|
|
629
|
+
const params = {
|
|
630
|
+
expectedChallenge,
|
|
631
|
+
response: response,
|
|
632
|
+
login: toStringOrNull(body.login) ?? undefined,
|
|
633
|
+
userId: isAuthIdentifier(body.userId) ? body.userId : undefined,
|
|
634
|
+
userAgent: userAgent ?? undefined,
|
|
635
|
+
...requestMetadata,
|
|
637
636
|
...sessionPrefs
|
|
638
637
|
};
|
|
639
638
|
const result = await this.storage.verifyPasskeyResponse(params);
|
|
@@ -59,9 +59,11 @@ export class MemoryPasskeyStore extends PasskeyStore {
|
|
|
59
59
|
}
|
|
60
60
|
async saveChallenge(record) {
|
|
61
61
|
this.challenges.set(record.challenge, {
|
|
62
|
-
|
|
62
|
+
challenge: record.challenge,
|
|
63
|
+
action: record.action,
|
|
63
64
|
userId: record.userId !== undefined ? normalizeUserId(record.userId) : undefined,
|
|
64
|
-
|
|
65
|
+
login: record.login ?? undefined,
|
|
66
|
+
expiresAt: record.expiresAt
|
|
65
67
|
});
|
|
66
68
|
}
|
|
67
69
|
async consumeChallenge(challenge) {
|
|
@@ -70,7 +72,7 @@ export class MemoryPasskeyStore extends PasskeyStore {
|
|
|
70
72
|
return null;
|
|
71
73
|
}
|
|
72
74
|
this.challenges.delete(challenge);
|
|
73
|
-
return { ...record
|
|
75
|
+
return { ...record };
|
|
74
76
|
}
|
|
75
77
|
async cleanupChallenges(now) {
|
|
76
78
|
for (const [challenge, record] of this.challenges.entries()) {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Model, type InferAttributes, type InferCreationAttributes, type ModelStatic, type Sequelize } from 'sequelize';
|
|
2
|
-
import type { PasskeyChallengeMetadata } from './service.js';
|
|
3
2
|
export declare class PasskeyCredentialModel extends Model<InferAttributes<PasskeyCredentialModel>, InferCreationAttributes<PasskeyCredentialModel>> {
|
|
4
3
|
credentialId: Buffer;
|
|
5
4
|
userId: number;
|
|
@@ -8,6 +7,13 @@ export declare class PasskeyCredentialModel extends Model<InferAttributes<Passke
|
|
|
8
7
|
transports: string[] | null;
|
|
9
8
|
backedUp: boolean;
|
|
10
9
|
deviceType: string;
|
|
10
|
+
label: string | null;
|
|
11
|
+
createdDomain: string | null;
|
|
12
|
+
createdUserAgent: string | null;
|
|
13
|
+
createdBrowser: string | null;
|
|
14
|
+
createdOs: string | null;
|
|
15
|
+
createdDevice: string | null;
|
|
16
|
+
createdIp: string | null;
|
|
11
17
|
createdAt?: Date;
|
|
12
18
|
updatedAt?: Date;
|
|
13
19
|
}
|
|
@@ -16,7 +22,6 @@ export declare class PasskeyChallengeModel extends Model<InferAttributes<Passkey
|
|
|
16
22
|
action: 'register' | 'authenticate';
|
|
17
23
|
userId: number | null;
|
|
18
24
|
login: string | null;
|
|
19
|
-
metadata: PasskeyChallengeMetadata | null;
|
|
20
25
|
expiresAt: Date;
|
|
21
26
|
createdAt?: Date;
|
|
22
27
|
updatedAt?: Date;
|
|
@@ -60,6 +60,40 @@ export function initPasskeyCredentialModel(sequelize) {
|
|
|
60
60
|
type: DataTypes.STRING(32),
|
|
61
61
|
allowNull: false,
|
|
62
62
|
defaultValue: 'multiDevice'
|
|
63
|
+
},
|
|
64
|
+
label: {
|
|
65
|
+
type: DataTypes.STRING(120),
|
|
66
|
+
allowNull: true
|
|
67
|
+
},
|
|
68
|
+
createdDomain: {
|
|
69
|
+
field: 'created_domain',
|
|
70
|
+
type: DataTypes.STRING(255),
|
|
71
|
+
allowNull: true
|
|
72
|
+
},
|
|
73
|
+
createdUserAgent: {
|
|
74
|
+
field: 'created_user_agent',
|
|
75
|
+
type: DataTypes.TEXT,
|
|
76
|
+
allowNull: true
|
|
77
|
+
},
|
|
78
|
+
createdBrowser: {
|
|
79
|
+
field: 'created_browser',
|
|
80
|
+
type: DataTypes.STRING(120),
|
|
81
|
+
allowNull: true
|
|
82
|
+
},
|
|
83
|
+
createdOs: {
|
|
84
|
+
field: 'created_os',
|
|
85
|
+
type: DataTypes.STRING(120),
|
|
86
|
+
allowNull: true
|
|
87
|
+
},
|
|
88
|
+
createdDevice: {
|
|
89
|
+
field: 'created_device',
|
|
90
|
+
type: DataTypes.STRING(120),
|
|
91
|
+
allowNull: true
|
|
92
|
+
},
|
|
93
|
+
createdIp: {
|
|
94
|
+
field: 'created_ip',
|
|
95
|
+
type: DataTypes.STRING(45),
|
|
96
|
+
allowNull: true
|
|
63
97
|
}
|
|
64
98
|
}, {
|
|
65
99
|
sequelize,
|
|
@@ -89,10 +123,6 @@ export function initPasskeyChallengeModel(sequelize) {
|
|
|
89
123
|
type: DataTypes.STRING(128),
|
|
90
124
|
allowNull: true
|
|
91
125
|
},
|
|
92
|
-
metadata: {
|
|
93
|
-
type: DataTypes.JSON,
|
|
94
|
-
allowNull: true
|
|
95
|
-
},
|
|
96
126
|
expiresAt: {
|
|
97
127
|
field: 'expires_at',
|
|
98
128
|
type: DataTypes.DATE,
|
|
@@ -10,6 +10,13 @@ declare class PasskeyCredentialModel extends Model<InferAttributes<PasskeyCreden
|
|
|
10
10
|
transports: string[] | null;
|
|
11
11
|
backedUp: boolean;
|
|
12
12
|
deviceType: string;
|
|
13
|
+
label: string | null;
|
|
14
|
+
createdDomain: string | null;
|
|
15
|
+
createdUserAgent: string | null;
|
|
16
|
+
createdBrowser: string | null;
|
|
17
|
+
createdOs: string | null;
|
|
18
|
+
createdDevice: string | null;
|
|
19
|
+
createdIp: string | null;
|
|
13
20
|
createdAt?: Date;
|
|
14
21
|
updatedAt?: Date;
|
|
15
22
|
}
|
|
@@ -18,7 +25,6 @@ declare class PasskeyChallengeModel extends Model<InferAttributes<PasskeyChallen
|
|
|
18
25
|
action: 'register' | 'authenticate';
|
|
19
26
|
userId: number | null;
|
|
20
27
|
login: string | null;
|
|
21
|
-
metadata: Record<string, unknown> | null;
|
|
22
28
|
expiresAt: Date;
|
|
23
29
|
createdAt?: Date;
|
|
24
30
|
updatedAt?: Date;
|
|
@@ -73,6 +73,40 @@ function initPasskeyCredentialModel(sequelize) {
|
|
|
73
73
|
type: DataTypes.STRING(32),
|
|
74
74
|
allowNull: false,
|
|
75
75
|
defaultValue: 'multiDevice'
|
|
76
|
+
},
|
|
77
|
+
label: {
|
|
78
|
+
type: DataTypes.STRING(120),
|
|
79
|
+
allowNull: true
|
|
80
|
+
},
|
|
81
|
+
createdDomain: {
|
|
82
|
+
field: 'created_domain',
|
|
83
|
+
type: DataTypes.STRING(255),
|
|
84
|
+
allowNull: true
|
|
85
|
+
},
|
|
86
|
+
createdUserAgent: {
|
|
87
|
+
field: 'created_user_agent',
|
|
88
|
+
type: DataTypes.TEXT,
|
|
89
|
+
allowNull: true
|
|
90
|
+
},
|
|
91
|
+
createdBrowser: {
|
|
92
|
+
field: 'created_browser',
|
|
93
|
+
type: DataTypes.STRING(120),
|
|
94
|
+
allowNull: true
|
|
95
|
+
},
|
|
96
|
+
createdOs: {
|
|
97
|
+
field: 'created_os',
|
|
98
|
+
type: DataTypes.STRING(120),
|
|
99
|
+
allowNull: true
|
|
100
|
+
},
|
|
101
|
+
createdDevice: {
|
|
102
|
+
field: 'created_device',
|
|
103
|
+
type: DataTypes.STRING(120),
|
|
104
|
+
allowNull: true
|
|
105
|
+
},
|
|
106
|
+
createdIp: {
|
|
107
|
+
field: 'created_ip',
|
|
108
|
+
type: DataTypes.STRING(45),
|
|
109
|
+
allowNull: true
|
|
76
110
|
}
|
|
77
111
|
}, {
|
|
78
112
|
sequelize,
|
|
@@ -102,10 +136,6 @@ function initPasskeyChallengeModel(sequelize) {
|
|
|
102
136
|
type: DataTypes.STRING(128),
|
|
103
137
|
allowNull: true
|
|
104
138
|
},
|
|
105
|
-
metadata: {
|
|
106
|
-
type: DataTypes.JSON,
|
|
107
|
-
allowNull: true
|
|
108
|
-
},
|
|
109
139
|
expiresAt: {
|
|
110
140
|
field: 'expires_at',
|
|
111
141
|
type: DataTypes.DATE,
|
|
@@ -145,6 +175,13 @@ export class SequelizePasskeyStore extends PasskeyStore {
|
|
|
145
175
|
transports: (model.transports ?? undefined),
|
|
146
176
|
backedUp: model.backedUp,
|
|
147
177
|
deviceType: model.deviceType,
|
|
178
|
+
label: model.label ?? undefined,
|
|
179
|
+
createdDomain: model.createdDomain ?? undefined,
|
|
180
|
+
createdUserAgent: model.createdUserAgent ?? undefined,
|
|
181
|
+
createdBrowser: model.createdBrowser ?? undefined,
|
|
182
|
+
createdOs: model.createdOs ?? undefined,
|
|
183
|
+
createdDevice: model.createdDevice ?? undefined,
|
|
184
|
+
createdIp: model.createdIp ?? undefined,
|
|
148
185
|
createdAt: model.createdAt ?? undefined,
|
|
149
186
|
updatedAt: model.updatedAt ?? undefined
|
|
150
187
|
}));
|
|
@@ -167,6 +204,13 @@ export class SequelizePasskeyStore extends PasskeyStore {
|
|
|
167
204
|
transports: (model.transports ?? undefined),
|
|
168
205
|
backedUp: model.backedUp,
|
|
169
206
|
deviceType: model.deviceType,
|
|
207
|
+
label: model.label ?? undefined,
|
|
208
|
+
createdDomain: model.createdDomain ?? undefined,
|
|
209
|
+
createdUserAgent: model.createdUserAgent ?? undefined,
|
|
210
|
+
createdBrowser: model.createdBrowser ?? undefined,
|
|
211
|
+
createdOs: model.createdOs ?? undefined,
|
|
212
|
+
createdDevice: model.createdDevice ?? undefined,
|
|
213
|
+
createdIp: model.createdIp ?? undefined,
|
|
170
214
|
createdAt: model.createdAt ?? undefined,
|
|
171
215
|
updatedAt: model.updatedAt ?? undefined
|
|
172
216
|
};
|
|
@@ -179,7 +223,14 @@ export class SequelizePasskeyStore extends PasskeyStore {
|
|
|
179
223
|
counter: record.counter,
|
|
180
224
|
transports: record.transports ?? null,
|
|
181
225
|
backedUp: record.backedUp,
|
|
182
|
-
deviceType: record.deviceType
|
|
226
|
+
deviceType: record.deviceType,
|
|
227
|
+
label: record.label ?? null,
|
|
228
|
+
createdDomain: record.createdDomain ?? null,
|
|
229
|
+
createdUserAgent: record.createdUserAgent ?? null,
|
|
230
|
+
createdBrowser: record.createdBrowser ?? null,
|
|
231
|
+
createdOs: record.createdOs ?? null,
|
|
232
|
+
createdDevice: record.createdDevice ?? null,
|
|
233
|
+
createdIp: record.createdIp ?? null
|
|
183
234
|
});
|
|
184
235
|
}
|
|
185
236
|
async updateCredentialCounter(credentialId, counter) {
|
|
@@ -191,7 +242,6 @@ export class SequelizePasskeyStore extends PasskeyStore {
|
|
|
191
242
|
action: record.action,
|
|
192
243
|
userId: record.userId !== undefined ? normalizeUserId(record.userId) : null,
|
|
193
244
|
login: record.login ?? null,
|
|
194
|
-
metadata: (record.metadata ?? {}),
|
|
195
245
|
expiresAt: record.expiresAt
|
|
196
246
|
});
|
|
197
247
|
}
|
|
@@ -206,8 +256,7 @@ export class SequelizePasskeyStore extends PasskeyStore {
|
|
|
206
256
|
action: model.action,
|
|
207
257
|
userId: model.userId ?? undefined,
|
|
208
258
|
login: model.login ?? undefined,
|
|
209
|
-
expiresAt: model.expiresAt
|
|
210
|
-
metadata: model.metadata ?? {}
|
|
259
|
+
expiresAt: model.expiresAt
|
|
211
260
|
};
|
|
212
261
|
}
|
|
213
262
|
async cleanupChallenges(now) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { PasskeyChallenge, PasskeyChallengeParams, PasskeyStorageAdapter, PasskeyVerificationParams, PasskeyVerificationResult, PasskeyServiceConfig, StoredPasskeyCredential } from './types.js';
|
|
2
2
|
import type { AuthIdentifier } from '../auth-api/types.js';
|
|
3
|
-
export type { CredentialDeviceType, PasskeyChallenge, PasskeyChallengeParams, PasskeyChallengeRecord,
|
|
3
|
+
export type { CredentialDeviceType, PasskeyChallenge, PasskeyChallengeParams, PasskeyChallengeRecord, PasskeyUserDescriptor, PasskeyVerificationParams, PasskeyVerificationResult, PasskeyServiceConfig, PasskeyStorageAdapter, StoredPasskeyCredential } from './types.js';
|
|
4
4
|
type Logger = Pick<typeof console, 'error' | 'warn'>;
|
|
5
5
|
export declare class PasskeyService {
|
|
6
6
|
private readonly config;
|
|
@@ -18,6 +18,13 @@ function sanitizeTransports(input) {
|
|
|
18
18
|
.filter((value) => ALLOWED_TRANSPORTS.includes(value));
|
|
19
19
|
return filtered.length > 0 ? filtered : undefined;
|
|
20
20
|
}
|
|
21
|
+
function toOptionalString(value) {
|
|
22
|
+
if (typeof value !== 'string') {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
const trimmed = value.trim();
|
|
26
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
27
|
+
}
|
|
21
28
|
function toBase64Url(buffer) {
|
|
22
29
|
return isoBase64URL.fromBuffer(new Uint8Array(buffer));
|
|
23
30
|
}
|
|
@@ -91,17 +98,11 @@ export class PasskeyService {
|
|
|
91
98
|
}
|
|
92
99
|
async createChallenge(params) {
|
|
93
100
|
await this.adapter.cleanupChallenges?.(new Date());
|
|
94
|
-
const metadata = {
|
|
95
|
-
domain: typeof params.domain === 'string' ? params.domain : undefined,
|
|
96
|
-
fingerprint: typeof params.fingerprint === 'string' ? params.fingerprint : undefined,
|
|
97
|
-
label: typeof params.label === 'string' ? params.label : undefined,
|
|
98
|
-
userAgent: typeof params.userAgent === 'string' ? params.userAgent : undefined
|
|
99
|
-
};
|
|
100
101
|
if (params.action === 'register') {
|
|
101
|
-
return this.createRegistrationChallenge(params
|
|
102
|
+
return this.createRegistrationChallenge(params);
|
|
102
103
|
}
|
|
103
104
|
if (params.action === 'authenticate') {
|
|
104
|
-
return this.createAuthenticationChallenge(params
|
|
105
|
+
return this.createAuthenticationChallenge(params);
|
|
105
106
|
}
|
|
106
107
|
throw new Error(`Unsupported passkey action: ${String(params.action)}`);
|
|
107
108
|
}
|
|
@@ -127,7 +128,7 @@ export class PasskeyService {
|
|
|
127
128
|
}
|
|
128
129
|
return { verified: false };
|
|
129
130
|
}
|
|
130
|
-
async createRegistrationChallenge(params
|
|
131
|
+
async createRegistrationChallenge(params) {
|
|
131
132
|
const user = await this.requireUser({ userId: params.userId, login: params.login });
|
|
132
133
|
const existing = await this.adapter.listUserCredentials(user.id);
|
|
133
134
|
const excludeCredentials = existing.map((credential) => {
|
|
@@ -150,8 +151,7 @@ export class PasskeyService {
|
|
|
150
151
|
action: 'register',
|
|
151
152
|
userId: user.id,
|
|
152
153
|
login: user.login,
|
|
153
|
-
expiresAt
|
|
154
|
-
metadata
|
|
154
|
+
expiresAt
|
|
155
155
|
});
|
|
156
156
|
return {
|
|
157
157
|
challenge: options.challenge,
|
|
@@ -160,7 +160,7 @@ export class PasskeyService {
|
|
|
160
160
|
publicKey: options
|
|
161
161
|
};
|
|
162
162
|
}
|
|
163
|
-
async createAuthenticationChallenge(params
|
|
163
|
+
async createAuthenticationChallenge(params) {
|
|
164
164
|
const user = await this.requireUser({ userId: params.userId, login: params.login });
|
|
165
165
|
const credentials = await this.adapter.listUserCredentials(user.id);
|
|
166
166
|
const allowCredentials = credentials.map((credential) => {
|
|
@@ -180,8 +180,7 @@ export class PasskeyService {
|
|
|
180
180
|
action: 'authenticate',
|
|
181
181
|
userId: user.id,
|
|
182
182
|
login: user.login,
|
|
183
|
-
expiresAt
|
|
184
|
-
metadata
|
|
183
|
+
expiresAt
|
|
185
184
|
});
|
|
186
185
|
return {
|
|
187
186
|
challenge: options.challenge,
|
|
@@ -259,7 +258,14 @@ export class PasskeyService {
|
|
|
259
258
|
counter: registrationInfo.counter ?? 0,
|
|
260
259
|
transports: sanitizeTransports(params.response.transports),
|
|
261
260
|
backedUp: registrationInfo.credentialDeviceType === 'multiDevice',
|
|
262
|
-
deviceType: registrationInfo.credentialDeviceType
|
|
261
|
+
deviceType: registrationInfo.credentialDeviceType,
|
|
262
|
+
label: toOptionalString(params.label),
|
|
263
|
+
createdDomain: toOptionalString(params.domain),
|
|
264
|
+
createdUserAgent: toOptionalString(params.userAgent),
|
|
265
|
+
createdBrowser: toOptionalString(params.browser),
|
|
266
|
+
createdOs: toOptionalString(params.os),
|
|
267
|
+
createdDevice: toOptionalString(params.device),
|
|
268
|
+
createdIp: toOptionalString(params.ip)
|
|
263
269
|
});
|
|
264
270
|
return { verified: true, userId: user.id, login: user.login };
|
|
265
271
|
}
|
|
@@ -9,19 +9,12 @@ export interface PasskeyServiceConfig {
|
|
|
9
9
|
timeoutMs: number;
|
|
10
10
|
userVerification?: 'preferred' | 'required' | 'discouraged';
|
|
11
11
|
}
|
|
12
|
-
export interface PasskeyChallengeMetadata {
|
|
13
|
-
domain?: string;
|
|
14
|
-
fingerprint?: string;
|
|
15
|
-
label?: string;
|
|
16
|
-
userAgent?: string;
|
|
17
|
-
}
|
|
18
12
|
export interface PasskeyChallengeRecord {
|
|
19
13
|
challenge: string;
|
|
20
14
|
action: 'register' | 'authenticate';
|
|
21
15
|
userId?: AuthIdentifier;
|
|
22
16
|
login?: string;
|
|
23
17
|
expiresAt: Date;
|
|
24
|
-
metadata: PasskeyChallengeMetadata;
|
|
25
18
|
}
|
|
26
19
|
export interface PasskeyUserDescriptor {
|
|
27
20
|
id: AuthIdentifier;
|
|
@@ -36,6 +29,13 @@ export interface StoredPasskeyCredential {
|
|
|
36
29
|
transports?: AuthenticatorTransportFuture[];
|
|
37
30
|
backedUp: boolean;
|
|
38
31
|
deviceType: CredentialDeviceType;
|
|
32
|
+
label?: string;
|
|
33
|
+
createdDomain?: string;
|
|
34
|
+
createdUserAgent?: string;
|
|
35
|
+
createdBrowser?: string;
|
|
36
|
+
createdOs?: string;
|
|
37
|
+
createdDevice?: string;
|
|
38
|
+
createdIp?: string;
|
|
39
39
|
createdAt?: Date;
|
|
40
40
|
updatedAt?: Date;
|
|
41
41
|
}
|
|
@@ -53,10 +53,9 @@ export interface PasskeyStorageAdapter {
|
|
|
53
53
|
consumeChallenge(challenge: string): Promise<PasskeyChallengeRecord | null>;
|
|
54
54
|
cleanupChallenges?(now: Date): Promise<void>;
|
|
55
55
|
}
|
|
56
|
-
export interface PasskeyChallengeParams
|
|
56
|
+
export interface PasskeyChallengeParams {
|
|
57
57
|
action: 'register' | 'authenticate';
|
|
58
58
|
login?: string;
|
|
59
|
-
userAgent?: string;
|
|
60
59
|
userId?: AuthIdentifier;
|
|
61
60
|
}
|
|
62
61
|
export interface PasskeyChallenge extends Record<string, unknown> {
|
|
@@ -69,6 +68,7 @@ export interface PasskeyVerificationParams extends Partial<Omit<Token, 'userId'>
|
|
|
69
68
|
login?: string;
|
|
70
69
|
response: Record<string, unknown>;
|
|
71
70
|
userId?: AuthIdentifier;
|
|
71
|
+
userAgent?: string;
|
|
72
72
|
}
|
|
73
73
|
export interface PasskeyVerificationResult extends Record<string, unknown> {
|
|
74
74
|
login?: string;
|