@better-auth/passkey 1.7.0-beta.0 → 1.7.0-beta.10
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/client.d.mts +17 -17
- package/dist/client.mjs +19 -6
- package/dist/{index-DD5Lute1.d.mts → index-z0BvlclO.d.mts} +45 -20
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +112 -32
- package/dist/{version-Bpv4-Vmt.mjs → version-vleaxnrJ.mjs} +1 -1
- package/package.json +11 -10
package/dist/client.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as Passkey, c as PasskeyOptions, d as WebAuthnChallengeValue, l as PasskeyRegistrationOptions, n as PASSKEY_ERROR_CODES, o as PasskeyAuthenticationOptions, s as PasskeyExtensionsResolver, t as passkey, u as PasskeyRegistrationUser } from "./index-z0BvlclO.mjs";
|
|
2
2
|
import { AuthenticationExtensionsClientInputs, AuthenticationExtensionsClientOutputs, AuthenticationResponseJSON, RegistrationResponseJSON } from "@simplewebauthn/server";
|
|
3
3
|
import * as better_auth_client0 from "better-auth/client";
|
|
4
4
|
import * as nanostores from "nanostores";
|
|
@@ -39,6 +39,14 @@ declare const getPasskeyActions: ($fetch: BetterFetch, {
|
|
|
39
39
|
user: User;
|
|
40
40
|
};
|
|
41
41
|
error: null;
|
|
42
|
+
} | {
|
|
43
|
+
data: null;
|
|
44
|
+
error: {
|
|
45
|
+
code: string;
|
|
46
|
+
message: string;
|
|
47
|
+
status: number;
|
|
48
|
+
statusText: string;
|
|
49
|
+
};
|
|
42
50
|
} | {
|
|
43
51
|
webauthn: {
|
|
44
52
|
response: AuthenticationResponseJSON;
|
|
@@ -60,14 +68,6 @@ declare const getPasskeyActions: ($fetch: BetterFetch, {
|
|
|
60
68
|
user: User;
|
|
61
69
|
};
|
|
62
70
|
error: null;
|
|
63
|
-
} | {
|
|
64
|
-
data: null;
|
|
65
|
-
error: {
|
|
66
|
-
code: string;
|
|
67
|
-
message: string;
|
|
68
|
-
status: number;
|
|
69
|
-
statusText: string;
|
|
70
|
-
};
|
|
71
71
|
}>;
|
|
72
72
|
};
|
|
73
73
|
passkey: {
|
|
@@ -165,6 +165,14 @@ declare const passkeyClient: () => {
|
|
|
165
165
|
user: User;
|
|
166
166
|
};
|
|
167
167
|
error: null;
|
|
168
|
+
} | {
|
|
169
|
+
data: null;
|
|
170
|
+
error: {
|
|
171
|
+
code: string;
|
|
172
|
+
message: string;
|
|
173
|
+
status: number;
|
|
174
|
+
statusText: string;
|
|
175
|
+
};
|
|
168
176
|
} | {
|
|
169
177
|
webauthn: {
|
|
170
178
|
response: AuthenticationResponseJSON;
|
|
@@ -186,14 +194,6 @@ declare const passkeyClient: () => {
|
|
|
186
194
|
user: User;
|
|
187
195
|
};
|
|
188
196
|
error: null;
|
|
189
|
-
} | {
|
|
190
|
-
data: null;
|
|
191
|
-
error: {
|
|
192
|
-
code: string;
|
|
193
|
-
message: string;
|
|
194
|
-
status: number;
|
|
195
|
-
statusText: string;
|
|
196
|
-
};
|
|
197
197
|
}>;
|
|
198
198
|
};
|
|
199
199
|
passkey: {
|
package/dist/client.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as PASSKEY_ERROR_CODES, t as PACKAGE_VERSION } from "./version-
|
|
1
|
+
import { n as PASSKEY_ERROR_CODES, t as PACKAGE_VERSION } from "./version-vleaxnrJ.mjs";
|
|
2
2
|
import { WebAuthnError, startAuthentication, startRegistration } from "@simplewebauthn/browser";
|
|
3
3
|
import { useAuthQuery } from "better-auth/client";
|
|
4
4
|
import { atom } from "nanostores";
|
|
@@ -10,18 +10,31 @@ const getPasskeyActions = ($fetch, { $listPasskeys, $store }) => {
|
|
|
10
10
|
throw: false
|
|
11
11
|
});
|
|
12
12
|
if (!response.data) return response;
|
|
13
|
+
const mergedExtensions = response.data.extensions || opts?.extensions ? {
|
|
14
|
+
...response.data.extensions || {},
|
|
15
|
+
...opts?.extensions || {}
|
|
16
|
+
} : void 0;
|
|
17
|
+
let res;
|
|
13
18
|
try {
|
|
14
|
-
|
|
15
|
-
...response.data.extensions || {},
|
|
16
|
-
...opts?.extensions || {}
|
|
17
|
-
} : void 0;
|
|
18
|
-
const res = await startAuthentication({
|
|
19
|
+
res = await startAuthentication({
|
|
19
20
|
optionsJSON: {
|
|
20
21
|
...response.data,
|
|
21
22
|
extensions: mergedExtensions
|
|
22
23
|
},
|
|
23
24
|
useBrowserAutofill: opts?.autoFill
|
|
24
25
|
});
|
|
26
|
+
} catch (err) {
|
|
27
|
+
return {
|
|
28
|
+
data: null,
|
|
29
|
+
error: {
|
|
30
|
+
code: err instanceof WebAuthnError ? err.code : "AUTH_CANCELLED",
|
|
31
|
+
message: PASSKEY_ERROR_CODES.AUTH_CANCELLED.message,
|
|
32
|
+
status: 400,
|
|
33
|
+
statusText: "BAD_REQUEST"
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
25
38
|
const { clientExtensionResults, ...responseBody } = res;
|
|
26
39
|
const verified = await $fetch("/passkey/verify-authentication", {
|
|
27
40
|
body: { response: responseBody },
|
|
@@ -100,7 +100,13 @@ interface PasskeyRegistrationOptions {
|
|
|
100
100
|
}) => Awaitable<PasskeyRegistrationUser>) | undefined;
|
|
101
101
|
/**
|
|
102
102
|
* Callback after a successful registration verification.
|
|
103
|
-
* Useful for user linking or
|
|
103
|
+
* Useful for user linking, auditing, or labeling the passkey.
|
|
104
|
+
*
|
|
105
|
+
* Return `userId` to attribute the passkey to a different user. Return `name`
|
|
106
|
+
* to set the stored label when the client did not provide one; the AAGUID is
|
|
107
|
+
* available via `verification.registrationInfo?.aaguid`. A non-empty
|
|
108
|
+
* client-supplied name always takes precedence over the returned `name`;
|
|
109
|
+
* whitespace-only input is treated as absent.
|
|
104
110
|
*/
|
|
105
111
|
afterVerification?: ((args: {
|
|
106
112
|
ctx: GenericEndpointContext;
|
|
@@ -110,6 +116,7 @@ interface PasskeyRegistrationOptions {
|
|
|
110
116
|
context?: string | null | undefined;
|
|
111
117
|
}) => Awaitable<{
|
|
112
118
|
userId?: string;
|
|
119
|
+
name?: string;
|
|
113
120
|
} | void>) | undefined;
|
|
114
121
|
/**
|
|
115
122
|
* Optional WebAuthn extensions to include in registration options.
|
|
@@ -196,6 +203,30 @@ type Passkey = {
|
|
|
196
203
|
aaguid?: string | undefined;
|
|
197
204
|
};
|
|
198
205
|
//#endregion
|
|
206
|
+
//#region src/authenticator-metadata.d.ts
|
|
207
|
+
/**
|
|
208
|
+
* Best-effort map of common authenticator AAGUIDs to a human-readable provider
|
|
209
|
+
* name, for labeling passkeys in management UIs.
|
|
210
|
+
*
|
|
211
|
+
* An AAGUID identifies an authenticator *model* (not a device or a user) and is
|
|
212
|
+
* present only in the registration response. Better Auth stores it on every
|
|
213
|
+
* passkey row and returns it from `listPasskeys`, so a display label can be
|
|
214
|
+
* resolved wherever passkeys are rendered.
|
|
215
|
+
*
|
|
216
|
+
* This list is intentionally small and not authoritative. Many authenticators
|
|
217
|
+
* are missing, and privacy-preserving platforms report an all-zero AAGUID
|
|
218
|
+
* (`00000000-0000-0000-0000-000000000000`) that matches nothing here. Notably,
|
|
219
|
+
* Apple devices zero the AAGUID under the default `attestation: "none"` flow, so
|
|
220
|
+
* the Apple entries below only appear in attested or managed contexts. For full
|
|
221
|
+
* coverage, resolve against the community-maintained source instead:
|
|
222
|
+
*
|
|
223
|
+
* - https://github.com/passkeydeveloper/passkey-authenticator-aaguids
|
|
224
|
+
*
|
|
225
|
+
* Names mirror that source verbatim.
|
|
226
|
+
*/
|
|
227
|
+
declare const commonAuthenticatorNames: Record<string, string>;
|
|
228
|
+
declare const getAuthenticatorName: (aaguid: string | null | undefined) => string | undefined;
|
|
229
|
+
//#endregion
|
|
199
230
|
//#region src/error-codes.d.ts
|
|
200
231
|
declare const PASSKEY_ERROR_CODES: {
|
|
201
232
|
CHALLENGE_NOT_FOUND: better_auth0.RawError<"CHALLENGE_NOT_FOUND">;
|
|
@@ -250,7 +281,7 @@ declare const passkey: (options?: PasskeyOptions | undefined) => {
|
|
|
250
281
|
image?: string | null | undefined;
|
|
251
282
|
};
|
|
252
283
|
};
|
|
253
|
-
}>)[]
|
|
284
|
+
}>)[];
|
|
254
285
|
query: zod.ZodOptional<zod.ZodObject<{
|
|
255
286
|
authenticatorAttachment: zod.ZodOptional<zod.ZodEnum<{
|
|
256
287
|
platform: "platform";
|
|
@@ -263,25 +294,10 @@ declare const passkey: (options?: PasskeyOptions | undefined) => {
|
|
|
263
294
|
openapi: {
|
|
264
295
|
operationId: string;
|
|
265
296
|
description: string;
|
|
297
|
+
parameters: better_call0.OpenAPIParameter[];
|
|
266
298
|
responses: {
|
|
267
299
|
200: {
|
|
268
300
|
description: string;
|
|
269
|
-
parameters: {
|
|
270
|
-
query: {
|
|
271
|
-
authenticatorAttachment: {
|
|
272
|
-
description: string;
|
|
273
|
-
required: boolean;
|
|
274
|
-
};
|
|
275
|
-
name: {
|
|
276
|
-
description: string;
|
|
277
|
-
required: boolean;
|
|
278
|
-
};
|
|
279
|
-
context: {
|
|
280
|
-
description: string;
|
|
281
|
-
required: boolean;
|
|
282
|
-
};
|
|
283
|
-
};
|
|
284
|
-
};
|
|
285
301
|
content: {
|
|
286
302
|
"application/json": {
|
|
287
303
|
schema: {
|
|
@@ -503,7 +519,7 @@ declare const passkey: (options?: PasskeyOptions | undefined) => {
|
|
|
503
519
|
image?: string | null | undefined;
|
|
504
520
|
};
|
|
505
521
|
};
|
|
506
|
-
}>)[]
|
|
522
|
+
}>)[];
|
|
507
523
|
metadata: {
|
|
508
524
|
openapi: {
|
|
509
525
|
operationId: string;
|
|
@@ -573,6 +589,15 @@ declare const passkey: (options?: PasskeyOptions | undefined) => {
|
|
|
573
589
|
ipAddress?: string | null | undefined;
|
|
574
590
|
userAgent?: string | null | undefined;
|
|
575
591
|
};
|
|
592
|
+
user: {
|
|
593
|
+
id: string;
|
|
594
|
+
createdAt: Date;
|
|
595
|
+
updatedAt: Date;
|
|
596
|
+
email: string;
|
|
597
|
+
emailVerified: boolean;
|
|
598
|
+
name: string;
|
|
599
|
+
image?: string | null | undefined;
|
|
600
|
+
};
|
|
576
601
|
}>;
|
|
577
602
|
listPasskeys: better_call0.StrictEndpoint<"/passkey/list-user-passkeys", {
|
|
578
603
|
method: "GET";
|
|
@@ -808,4 +833,4 @@ declare const passkey: (options?: PasskeyOptions | undefined) => {
|
|
|
808
833
|
options: PasskeyOptions | undefined;
|
|
809
834
|
};
|
|
810
835
|
//#endregion
|
|
811
|
-
export {
|
|
836
|
+
export { Passkey as a, PasskeyOptions as c, WebAuthnChallengeValue as d, getAuthenticatorName as i, PasskeyRegistrationOptions as l, PASSKEY_ERROR_CODES as n, PasskeyAuthenticationOptions as o, commonAuthenticatorNames as r, PasskeyExtensionsResolver as s, passkey as t, PasskeyRegistrationUser as u };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { PASSKEY_ERROR_CODES, Passkey, PasskeyOptions, passkey };
|
|
1
|
+
import { a as Passkey, c as PasskeyOptions, i as getAuthenticatorName, n as PASSKEY_ERROR_CODES, r as commonAuthenticatorNames, t as passkey } from "./index-z0BvlclO.mjs";
|
|
2
|
+
export { PASSKEY_ERROR_CODES, Passkey, PasskeyOptions, commonAuthenticatorNames, getAuthenticatorName, passkey };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as PASSKEY_ERROR_CODES, t as PACKAGE_VERSION } from "./version-
|
|
1
|
+
import { n as PASSKEY_ERROR_CODES, t as PACKAGE_VERSION } from "./version-vleaxnrJ.mjs";
|
|
2
2
|
import { mergeSchema } from "better-auth/db";
|
|
3
3
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
4
4
|
import { APIError } from "@better-auth/core/error";
|
|
@@ -52,33 +52,46 @@ const generatePasskeyQuerySchema = z.object({
|
|
|
52
52
|
name: z.string().optional(),
|
|
53
53
|
context: z.string().optional()
|
|
54
54
|
}).optional();
|
|
55
|
+
const generatePasskeyRegistrationOptionsOpenAPIParameters = [
|
|
56
|
+
{
|
|
57
|
+
name: "authenticatorAttachment",
|
|
58
|
+
in: "query",
|
|
59
|
+
required: false,
|
|
60
|
+
description: `Type of authenticator to use for registration.
|
|
61
|
+
"platform" for device-specific authenticators,
|
|
62
|
+
"cross-platform" for authenticators that can be used across devices.`,
|
|
63
|
+
schema: {
|
|
64
|
+
type: "string",
|
|
65
|
+
enum: ["platform", "cross-platform"]
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: "name",
|
|
70
|
+
in: "query",
|
|
71
|
+
required: false,
|
|
72
|
+
description: `Optional custom name for the passkey.
|
|
73
|
+
This can help identify the passkey when managing multiple credentials.`,
|
|
74
|
+
schema: { type: "string" }
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "context",
|
|
78
|
+
in: "query",
|
|
79
|
+
required: false,
|
|
80
|
+
description: "Optional context for passkey-first registration flows.",
|
|
81
|
+
schema: { type: "string" }
|
|
82
|
+
}
|
|
83
|
+
];
|
|
55
84
|
const generatePasskeyRegistrationOptions = (opts, { maxAgeInSeconds }) => {
|
|
56
85
|
return createAuthEndpoint("/passkey/generate-register-options", {
|
|
57
86
|
method: "GET",
|
|
58
|
-
use: opts.registration?.requireSession ?? true ? [freshSessionMiddleware] :
|
|
87
|
+
use: opts.registration?.requireSession ?? true ? [freshSessionMiddleware] : [],
|
|
59
88
|
query: generatePasskeyQuerySchema,
|
|
60
89
|
metadata: { openapi: {
|
|
61
90
|
operationId: "generatePasskeyRegistrationOptions",
|
|
62
91
|
description: "Generate registration options for a new passkey",
|
|
92
|
+
parameters: generatePasskeyRegistrationOptionsOpenAPIParameters,
|
|
63
93
|
responses: { 200: {
|
|
64
94
|
description: "Success",
|
|
65
|
-
parameters: { query: {
|
|
66
|
-
authenticatorAttachment: {
|
|
67
|
-
description: `Type of authenticator to use for registration.
|
|
68
|
-
"platform" for device-specific authenticators,
|
|
69
|
-
"cross-platform" for authenticators that can be used across devices.`,
|
|
70
|
-
required: false
|
|
71
|
-
},
|
|
72
|
-
name: {
|
|
73
|
-
description: `Optional custom name for the passkey.
|
|
74
|
-
This can help identify the passkey when managing multiple credentials.`,
|
|
75
|
-
required: false
|
|
76
|
-
},
|
|
77
|
-
context: {
|
|
78
|
-
description: "Optional context for passkey-first registration flows.",
|
|
79
|
-
required: false
|
|
80
|
-
}
|
|
81
|
-
} },
|
|
82
95
|
content: { "application/json": { schema: {
|
|
83
96
|
type: "object",
|
|
84
97
|
properties: {
|
|
@@ -178,6 +191,7 @@ const generatePasskeyRegistrationOptions = (opts, { maxAgeInSeconds }) => {
|
|
|
178
191
|
await ctx.context.internalAdapter.createVerificationValue({
|
|
179
192
|
identifier: verificationToken,
|
|
180
193
|
value: JSON.stringify({
|
|
194
|
+
type: "registration",
|
|
181
195
|
expectedChallenge: options.challenge,
|
|
182
196
|
userData: {
|
|
183
197
|
id: user.id,
|
|
@@ -268,6 +282,7 @@ const generatePasskeyAuthenticationOptions = (opts, { maxAgeInSeconds }) => crea
|
|
|
268
282
|
})) } : {}
|
|
269
283
|
});
|
|
270
284
|
const data = {
|
|
285
|
+
type: "authentication",
|
|
271
286
|
expectedChallenge: options.challenge,
|
|
272
287
|
userData: { id: session?.user.id || "" }
|
|
273
288
|
};
|
|
@@ -287,14 +302,14 @@ const generatePasskeyAuthenticationOptions = (opts, { maxAgeInSeconds }) => crea
|
|
|
287
302
|
});
|
|
288
303
|
const verifyPasskeyRegistrationBodySchema = z.object({
|
|
289
304
|
response: z.any(),
|
|
290
|
-
name: z.string().meta({ description: "Name of the passkey" }).optional()
|
|
305
|
+
name: z.string().trim().meta({ description: "Name of the passkey" }).optional()
|
|
291
306
|
});
|
|
292
307
|
const verifyPasskeyRegistration = (options) => {
|
|
293
308
|
const requireSession = options.registration?.requireSession ?? true;
|
|
294
309
|
return createAuthEndpoint("/passkey/verify-registration", {
|
|
295
310
|
method: "POST",
|
|
296
311
|
body: verifyPasskeyRegistrationBodySchema,
|
|
297
|
-
use: requireSession ? [freshSessionMiddleware] :
|
|
312
|
+
use: requireSession ? [freshSessionMiddleware] : [],
|
|
298
313
|
metadata: { openapi: {
|
|
299
314
|
operationId: "passkeyVerifyRegistration",
|
|
300
315
|
description: "Verify registration of a new passkey",
|
|
@@ -313,9 +328,10 @@ const verifyPasskeyRegistration = (options) => {
|
|
|
313
328
|
const webAuthnCookie = ctx.context.createAuthCookie(options.advanced.webAuthnChallengeCookie);
|
|
314
329
|
const verificationToken = await ctx.getSignedCookie(webAuthnCookie.name, ctx.context.secret);
|
|
315
330
|
if (!verificationToken) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND);
|
|
316
|
-
const data = await ctx.context.internalAdapter.
|
|
331
|
+
const data = await ctx.context.internalAdapter.consumeVerificationValue(verificationToken);
|
|
317
332
|
if (!data) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND);
|
|
318
|
-
const { expectedChallenge, userData, context } = JSON.parse(data.value);
|
|
333
|
+
const { type: ceremony, expectedChallenge, userData, context } = JSON.parse(data.value);
|
|
334
|
+
if (ceremony !== "registration") throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND);
|
|
319
335
|
const session = requireSession ? ctx.context.session : await getSessionFromCtx(ctx);
|
|
320
336
|
if (session?.user?.id && userData.id !== session.user.id) throw APIError.from("UNAUTHORIZED", PASSKEY_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY);
|
|
321
337
|
try {
|
|
@@ -335,6 +351,7 @@ const verifyPasskeyRegistration = (options) => {
|
|
|
335
351
|
displayName: userData.displayName
|
|
336
352
|
};
|
|
337
353
|
let targetUserId = resolvedUser.id;
|
|
354
|
+
let resolvedName = ctx.body.name || void 0;
|
|
338
355
|
if (options.registration?.afterVerification) {
|
|
339
356
|
const result = await options.registration.afterVerification({
|
|
340
357
|
ctx,
|
|
@@ -348,16 +365,18 @@ const verifyPasskeyRegistration = (options) => {
|
|
|
348
365
|
if (session?.user?.id && result.userId !== session.user.id) throw APIError.from("UNAUTHORIZED", PASSKEY_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY);
|
|
349
366
|
targetUserId = result.userId;
|
|
350
367
|
}
|
|
368
|
+
if (!resolvedName) resolvedName = result?.name?.trim() || void 0;
|
|
351
369
|
}
|
|
370
|
+
if (!targetUserId) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.RESOLVED_USER_INVALID);
|
|
352
371
|
const pubKey = base64.encode(credential.publicKey);
|
|
353
372
|
const newPasskey = {
|
|
354
|
-
name:
|
|
373
|
+
name: resolvedName,
|
|
355
374
|
userId: targetUserId,
|
|
356
375
|
credentialID: credential.id,
|
|
357
376
|
publicKey: pubKey,
|
|
358
377
|
counter: credential.counter,
|
|
359
378
|
deviceType: credentialDeviceType,
|
|
360
|
-
transports: resp.response.transports
|
|
379
|
+
transports: resp.response.transports?.join(",") ?? "",
|
|
361
380
|
backedUp: credentialBackedUp,
|
|
362
381
|
createdAt: /* @__PURE__ */ new Date(),
|
|
363
382
|
aaguid
|
|
@@ -366,9 +385,9 @@ const verifyPasskeyRegistration = (options) => {
|
|
|
366
385
|
model: "passkey",
|
|
367
386
|
data: newPasskey
|
|
368
387
|
});
|
|
369
|
-
await ctx.context.internalAdapter.deleteVerificationByIdentifier(verificationToken);
|
|
370
388
|
return ctx.json(newPasskeyRes, { status: 200 });
|
|
371
389
|
} catch (e) {
|
|
390
|
+
if (e instanceof APIError) throw e;
|
|
372
391
|
ctx.context.logger.error("Failed to verify registration", e);
|
|
373
392
|
throw APIError.from("INTERNAL_SERVER_ERROR", PASSKEY_ERROR_CODES.FAILED_TO_VERIFY_REGISTRATION);
|
|
374
393
|
}
|
|
@@ -402,9 +421,10 @@ const verifyPasskeyAuthentication = (options) => createAuthEndpoint("/passkey/ve
|
|
|
402
421
|
const webAuthnCookie = ctx.context.createAuthCookie(options.advanced.webAuthnChallengeCookie);
|
|
403
422
|
const verificationToken = await ctx.getSignedCookie(webAuthnCookie.name, ctx.context.secret);
|
|
404
423
|
if (!verificationToken) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND);
|
|
405
|
-
const data = await ctx.context.internalAdapter.
|
|
424
|
+
const data = await ctx.context.internalAdapter.consumeVerificationValue(verificationToken);
|
|
406
425
|
if (!data) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND);
|
|
407
|
-
const { expectedChallenge } = JSON.parse(data.value);
|
|
426
|
+
const { type: ceremony, expectedChallenge } = JSON.parse(data.value);
|
|
427
|
+
if (ceremony !== "authentication") throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND);
|
|
408
428
|
const passkey = await ctx.context.adapter.findOne({
|
|
409
429
|
model: "passkey",
|
|
410
430
|
where: [{
|
|
@@ -450,9 +470,12 @@ const verifyPasskeyAuthentication = (options) => createAuthEndpoint("/passkey/ve
|
|
|
450
470
|
session: s,
|
|
451
471
|
user
|
|
452
472
|
});
|
|
453
|
-
|
|
454
|
-
|
|
473
|
+
return ctx.json({
|
|
474
|
+
session: s,
|
|
475
|
+
user
|
|
476
|
+
}, { status: 200 });
|
|
455
477
|
} catch (e) {
|
|
478
|
+
if (e instanceof APIError) throw e;
|
|
456
479
|
ctx.context.logger.error("Failed to verify authentication", e);
|
|
457
480
|
throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.AUTHENTICATION_FAILED);
|
|
458
481
|
}
|
|
@@ -573,7 +596,7 @@ const updatePasskey = createAuthEndpoint("/passkey/update-passkey", {
|
|
|
573
596
|
method: "POST",
|
|
574
597
|
body: z.object({
|
|
575
598
|
id: z.string().meta({ description: `The ID of the passkey which will be updated. Eg: \"passkey-id\"` }),
|
|
576
|
-
name: z.string().meta({ description: `The new name which the passkey will be updated to. Eg: \"my-new-passkey-name\"` })
|
|
599
|
+
name: z.string().trim().min(1).meta({ description: `The new name which the passkey will be updated to. Eg: \"my-new-passkey-name\"` })
|
|
577
600
|
}),
|
|
578
601
|
use: [sessionMiddleware, requireResourceOwnership({
|
|
579
602
|
model: "passkey",
|
|
@@ -657,6 +680,63 @@ const schema = { passkey: { fields: {
|
|
|
657
680
|
}
|
|
658
681
|
} } };
|
|
659
682
|
//#endregion
|
|
683
|
+
//#region src/authenticator-metadata.ts
|
|
684
|
+
/**
|
|
685
|
+
* Best-effort map of common authenticator AAGUIDs to a human-readable provider
|
|
686
|
+
* name, for labeling passkeys in management UIs.
|
|
687
|
+
*
|
|
688
|
+
* An AAGUID identifies an authenticator *model* (not a device or a user) and is
|
|
689
|
+
* present only in the registration response. Better Auth stores it on every
|
|
690
|
+
* passkey row and returns it from `listPasskeys`, so a display label can be
|
|
691
|
+
* resolved wherever passkeys are rendered.
|
|
692
|
+
*
|
|
693
|
+
* This list is intentionally small and not authoritative. Many authenticators
|
|
694
|
+
* are missing, and privacy-preserving platforms report an all-zero AAGUID
|
|
695
|
+
* (`00000000-0000-0000-0000-000000000000`) that matches nothing here. Notably,
|
|
696
|
+
* Apple devices zero the AAGUID under the default `attestation: "none"` flow, so
|
|
697
|
+
* the Apple entries below only appear in attested or managed contexts. For full
|
|
698
|
+
* coverage, resolve against the community-maintained source instead:
|
|
699
|
+
*
|
|
700
|
+
* - https://github.com/passkeydeveloper/passkey-authenticator-aaguids
|
|
701
|
+
*
|
|
702
|
+
* Names mirror that source verbatim.
|
|
703
|
+
*/
|
|
704
|
+
const commonAuthenticatorNames = {
|
|
705
|
+
"ea9b8d66-4d01-1d21-3ce4-b6b48cb575d4": "Google Password Manager",
|
|
706
|
+
"fbfc3007-154e-4ecc-8c0b-6e020557d7bd": "Apple Passwords",
|
|
707
|
+
"dd4ec289-e01d-41c9-bb89-70fa845d4bf2": "iCloud Keychain (Managed)",
|
|
708
|
+
"08987058-cadc-4b81-b6e1-30de50dcbe96": "Windows Hello",
|
|
709
|
+
"9ddd1817-af5a-4672-a2b9-3e3dd95000a9": "Windows Hello",
|
|
710
|
+
"6028b017-b1d4-4c02-b4b3-afcdafc96bb2": "Windows Hello",
|
|
711
|
+
"bada5566-a7aa-401f-bd96-45619a55120d": "1Password",
|
|
712
|
+
"d548826e-79b4-db40-a3d8-11116f7e8349": "Bitwarden",
|
|
713
|
+
"531126d6-e717-415c-9320-3d9aa6981239": "Dashlane",
|
|
714
|
+
"b78a0a55-6ef8-d246-a042-ba0f6d55050c": "LastPass",
|
|
715
|
+
"b84e4048-15dc-4dd0-8640-f4f60813c8af": "NordPass",
|
|
716
|
+
"50726f74-6f6e-5061-7373-50726f746f6e": "Proton Pass",
|
|
717
|
+
"0ea242b4-43c4-4a1b-8b17-dd6d0b6baec6": "Keeper",
|
|
718
|
+
"53414d53-554e-4700-0000-000000000000": "Samsung Pass"
|
|
719
|
+
};
|
|
720
|
+
/**
|
|
721
|
+
* Resolve a best-effort provider name for an authenticator AAGUID.
|
|
722
|
+
*
|
|
723
|
+
* Returns `undefined` when the AAGUID is unknown, empty, or the all-zero value
|
|
724
|
+
* reported by privacy-preserving platforms. Casing and surrounding whitespace
|
|
725
|
+
* are normalized before lookup.
|
|
726
|
+
*
|
|
727
|
+
* @example
|
|
728
|
+
* ```ts
|
|
729
|
+
* const label = passkey.name || getAuthenticatorName(passkey.aaguid) || "Passkey";
|
|
730
|
+
* ```
|
|
731
|
+
*/
|
|
732
|
+
const ANONYMOUS_AAGUID = "00000000-0000-0000-0000-000000000000";
|
|
733
|
+
const getAuthenticatorName = (aaguid) => {
|
|
734
|
+
const normalized = aaguid?.trim().toLowerCase();
|
|
735
|
+
if (!normalized || normalized === ANONYMOUS_AAGUID) return void 0;
|
|
736
|
+
const name = commonAuthenticatorNames[normalized];
|
|
737
|
+
return typeof name === "string" ? name : void 0;
|
|
738
|
+
};
|
|
739
|
+
//#endregion
|
|
660
740
|
//#region src/index.ts
|
|
661
741
|
const MAX_AGE_IN_SECONDS = 300;
|
|
662
742
|
const passkey = (options) => {
|
|
@@ -686,4 +766,4 @@ const passkey = (options) => {
|
|
|
686
766
|
};
|
|
687
767
|
};
|
|
688
768
|
//#endregion
|
|
689
|
-
export { PASSKEY_ERROR_CODES, passkey };
|
|
769
|
+
export { PASSKEY_ERROR_CODES, commonAuthenticatorNames, getAuthenticatorName, passkey };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-auth/passkey",
|
|
3
|
-
"version": "1.7.0-beta.
|
|
3
|
+
"version": "1.7.0-beta.10",
|
|
4
4
|
"description": "Passkey plugin for Better Auth",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -49,22 +49,23 @@
|
|
|
49
49
|
}
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@simplewebauthn/browser": "^13.
|
|
53
|
-
"@simplewebauthn/server": "^13.
|
|
52
|
+
"@simplewebauthn/browser": "^13.3.0",
|
|
53
|
+
"@simplewebauthn/server": "^13.3.1",
|
|
54
54
|
"zod": "^4.3.6"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
|
+
"nanostores": "^1.3.0",
|
|
57
58
|
"tsdown": "0.21.1",
|
|
58
|
-
"@better-auth/core": "1.7.0-beta.
|
|
59
|
-
"better-auth": "1.7.0-beta.
|
|
59
|
+
"@better-auth/core": "1.7.0-beta.10",
|
|
60
|
+
"better-auth": "1.7.0-beta.10"
|
|
60
61
|
},
|
|
61
62
|
"peerDependencies": {
|
|
62
|
-
"@better-auth/utils": "0.4.
|
|
63
|
-
"@better-fetch/fetch": "1.1
|
|
64
|
-
"better-call": "1.3.
|
|
63
|
+
"@better-auth/utils": "0.4.2",
|
|
64
|
+
"@better-fetch/fetch": "1.3.1",
|
|
65
|
+
"better-call": "1.3.7",
|
|
65
66
|
"nanostores": "^1.0.1",
|
|
66
|
-
"@better-auth/core": "^1.7.0-beta.
|
|
67
|
-
"better-auth": "^1.7.0-beta.
|
|
67
|
+
"@better-auth/core": "^1.7.0-beta.10",
|
|
68
|
+
"better-auth": "^1.7.0-beta.10"
|
|
68
69
|
},
|
|
69
70
|
"scripts": {
|
|
70
71
|
"build": "tsdown",
|