@better-auth/passkey 1.5.7-beta.1 → 1.6.0
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 +91 -35
- package/dist/client.mjs +45 -14
- package/dist/index-DD5Lute1.d.mts +811 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +272 -198
- package/dist/{error-codes-BwAsYefH.mjs → version-DSSbBvqx.mjs} +8 -4
- package/package.json +9 -8
- package/dist/client.mjs.map +0 -1
- package/dist/error-codes-BwAsYefH.mjs.map +0 -1
- package/dist/index-BfRDyiNp.d.mts +0 -692
- package/dist/index.mjs.map +0 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as PASSKEY_ERROR_CODES, o as PasskeyOptions, r as Passkey, t as passkey } from "./index-DD5Lute1.mjs";
|
|
2
2
|
export { PASSKEY_ERROR_CODES, Passkey, PasskeyOptions, passkey };
|
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as PASSKEY_ERROR_CODES, t as PACKAGE_VERSION } from "./version-DSSbBvqx.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";
|
|
5
5
|
import { base64 } from "@better-auth/utils/base64";
|
|
6
6
|
import { generateAuthenticationOptions, generateRegistrationOptions, verifyAuthenticationResponse, verifyRegistrationResponse } from "@simplewebauthn/server";
|
|
7
|
-
import { freshSessionMiddleware, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
|
|
7
|
+
import { freshSessionMiddleware, getSessionFromCtx, requireResourceOwnership, sessionMiddleware } from "better-auth/api";
|
|
8
8
|
import { setSessionCookie } from "better-auth/cookies";
|
|
9
9
|
import { generateRandomString } from "better-auth/crypto";
|
|
10
10
|
import * as z from "zod";
|
|
@@ -14,136 +14,183 @@ function getRpID(options, baseURL) {
|
|
|
14
14
|
}
|
|
15
15
|
//#endregion
|
|
16
16
|
//#region src/routes.ts
|
|
17
|
+
const resolveExtensions = async (extensions, ctx) => {
|
|
18
|
+
if (!extensions) return;
|
|
19
|
+
if (typeof extensions === "function") return await extensions({ ctx });
|
|
20
|
+
return extensions;
|
|
21
|
+
};
|
|
22
|
+
const resolveRegistrationUser = async (opts, ctx) => {
|
|
23
|
+
if (opts.registration?.requireSession ?? true) {
|
|
24
|
+
const session = ctx.context?.session;
|
|
25
|
+
if (!session?.user?.id) throw APIError.from("UNAUTHORIZED", PASSKEY_ERROR_CODES.SESSION_REQUIRED);
|
|
26
|
+
const sessionName = session.user.email || session.user.id;
|
|
27
|
+
return {
|
|
28
|
+
id: session.user.id,
|
|
29
|
+
name: sessionName,
|
|
30
|
+
displayName: sessionName
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const session = await getSessionFromCtx(ctx);
|
|
34
|
+
if (session?.user?.id) {
|
|
35
|
+
const sessionName = session.user.email || session.user.id;
|
|
36
|
+
return {
|
|
37
|
+
id: session.user.id,
|
|
38
|
+
name: sessionName,
|
|
39
|
+
displayName: sessionName
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (!opts.registration?.resolveUser) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.RESOLVE_USER_REQUIRED);
|
|
43
|
+
const resolvedUser = await opts.registration.resolveUser({
|
|
44
|
+
ctx,
|
|
45
|
+
context: ctx.query?.context ?? null
|
|
46
|
+
});
|
|
47
|
+
if (!resolvedUser?.id || !resolvedUser?.name) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.RESOLVED_USER_INVALID);
|
|
48
|
+
return resolvedUser;
|
|
49
|
+
};
|
|
17
50
|
const generatePasskeyQuerySchema = z.object({
|
|
18
51
|
authenticatorAttachment: z.enum(["platform", "cross-platform"]).optional(),
|
|
19
|
-
name: z.string().optional()
|
|
52
|
+
name: z.string().optional(),
|
|
53
|
+
context: z.string().optional()
|
|
20
54
|
}).optional();
|
|
21
|
-
const generatePasskeyRegistrationOptions = (opts, { maxAgeInSeconds }) =>
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
55
|
+
const generatePasskeyRegistrationOptions = (opts, { maxAgeInSeconds }) => {
|
|
56
|
+
return createAuthEndpoint("/passkey/generate-register-options", {
|
|
57
|
+
method: "GET",
|
|
58
|
+
use: opts.registration?.requireSession ?? true ? [freshSessionMiddleware] : void 0,
|
|
59
|
+
query: generatePasskeyQuerySchema,
|
|
60
|
+
metadata: { openapi: {
|
|
61
|
+
operationId: "generatePasskeyRegistrationOptions",
|
|
62
|
+
description: "Generate registration options for a new passkey",
|
|
63
|
+
responses: { 200: {
|
|
64
|
+
description: "Success",
|
|
65
|
+
parameters: { query: {
|
|
66
|
+
authenticatorAttachment: {
|
|
67
|
+
description: `Type of authenticator to use for registration.
|
|
33
68
|
"platform" for device-specific authenticators,
|
|
34
69
|
"cross-platform" for authenticators that can be used across devices.`,
|
|
35
|
-
|
|
36
|
-
},
|
|
37
|
-
name: {
|
|
38
|
-
description: `Optional custom name for the passkey.
|
|
39
|
-
This can help identify the passkey when managing multiple credentials.`,
|
|
40
|
-
required: false
|
|
41
|
-
}
|
|
42
|
-
} },
|
|
43
|
-
content: { "application/json": { schema: {
|
|
44
|
-
type: "object",
|
|
45
|
-
properties: {
|
|
46
|
-
challenge: { type: "string" },
|
|
47
|
-
rp: {
|
|
48
|
-
type: "object",
|
|
49
|
-
properties: {
|
|
50
|
-
name: { type: "string" },
|
|
51
|
-
id: { type: "string" }
|
|
52
|
-
}
|
|
70
|
+
required: false
|
|
53
71
|
},
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
name: { type: "string" },
|
|
59
|
-
displayName: { type: "string" }
|
|
60
|
-
}
|
|
72
|
+
name: {
|
|
73
|
+
description: `Optional custom name for the passkey.
|
|
74
|
+
This can help identify the passkey when managing multiple credentials.`,
|
|
75
|
+
required: false
|
|
61
76
|
},
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
77
|
+
context: {
|
|
78
|
+
description: "Optional context for passkey-first registration flows.",
|
|
79
|
+
required: false
|
|
80
|
+
}
|
|
81
|
+
} },
|
|
82
|
+
content: { "application/json": { schema: {
|
|
83
|
+
type: "object",
|
|
84
|
+
properties: {
|
|
85
|
+
challenge: { type: "string" },
|
|
86
|
+
rp: {
|
|
65
87
|
type: "object",
|
|
66
88
|
properties: {
|
|
67
|
-
|
|
68
|
-
|
|
89
|
+
name: { type: "string" },
|
|
90
|
+
id: { type: "string" }
|
|
69
91
|
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
timeout: { type: "number" },
|
|
73
|
-
excludeCredentials: {
|
|
74
|
-
type: "array",
|
|
75
|
-
items: {
|
|
92
|
+
},
|
|
93
|
+
user: {
|
|
76
94
|
type: "object",
|
|
77
95
|
properties: {
|
|
78
96
|
id: { type: "string" },
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
97
|
+
name: { type: "string" },
|
|
98
|
+
displayName: { type: "string" }
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
pubKeyCredParams: {
|
|
102
|
+
type: "array",
|
|
103
|
+
items: {
|
|
104
|
+
type: "object",
|
|
105
|
+
properties: {
|
|
106
|
+
type: { type: "string" },
|
|
107
|
+
alg: { type: "number" }
|
|
83
108
|
}
|
|
84
109
|
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
110
|
+
},
|
|
111
|
+
timeout: { type: "number" },
|
|
112
|
+
excludeCredentials: {
|
|
113
|
+
type: "array",
|
|
114
|
+
items: {
|
|
115
|
+
type: "object",
|
|
116
|
+
properties: {
|
|
117
|
+
id: { type: "string" },
|
|
118
|
+
type: { type: "string" },
|
|
119
|
+
transports: {
|
|
120
|
+
type: "array",
|
|
121
|
+
items: { type: "string" }
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
authenticatorSelection: {
|
|
127
|
+
type: "object",
|
|
128
|
+
properties: {
|
|
129
|
+
authenticatorAttachment: { type: "string" },
|
|
130
|
+
requireResidentKey: { type: "boolean" },
|
|
131
|
+
userVerification: { type: "string" }
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
attestation: { type: "string" },
|
|
135
|
+
extensions: { type: "object" }
|
|
136
|
+
}
|
|
137
|
+
} } }
|
|
138
|
+
} }
|
|
99
139
|
} }
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
140
|
+
}, async (ctx) => {
|
|
141
|
+
const user = await resolveRegistrationUser(opts, ctx);
|
|
142
|
+
const userPasskeys = await ctx.context.adapter.findMany({
|
|
143
|
+
model: "passkey",
|
|
144
|
+
where: [{
|
|
145
|
+
field: "userId",
|
|
146
|
+
value: user.id
|
|
147
|
+
}]
|
|
148
|
+
});
|
|
149
|
+
const registrationExtensions = await resolveExtensions(opts.registration?.extensions, ctx);
|
|
150
|
+
const userID = new TextEncoder().encode(generateRandomString(32, "a-z", "0-9"));
|
|
151
|
+
const baseURLString = typeof ctx.context.options.baseURL === "string" ? ctx.context.options.baseURL : void 0;
|
|
152
|
+
const options = await generateRegistrationOptions({
|
|
153
|
+
rpName: opts.rpName || ctx.context.appName,
|
|
154
|
+
rpID: getRpID(opts, baseURLString),
|
|
155
|
+
userID,
|
|
156
|
+
userName: ctx.query?.name || user.name || user.id,
|
|
157
|
+
userDisplayName: user.displayName || user.name || user.id,
|
|
158
|
+
attestationType: "none",
|
|
159
|
+
excludeCredentials: userPasskeys.map((passkey) => ({
|
|
160
|
+
id: passkey.credentialID,
|
|
161
|
+
transports: passkey.transports?.split(",")
|
|
162
|
+
})),
|
|
163
|
+
authenticatorSelection: {
|
|
164
|
+
residentKey: "preferred",
|
|
165
|
+
userVerification: "preferred",
|
|
166
|
+
...opts.authenticatorSelection || {},
|
|
167
|
+
...ctx.query?.authenticatorAttachment ? { authenticatorAttachment: ctx.query.authenticatorAttachment } : {}
|
|
168
|
+
},
|
|
169
|
+
extensions: registrationExtensions
|
|
170
|
+
});
|
|
171
|
+
const verificationToken = generateRandomString(32);
|
|
172
|
+
const webAuthnCookie = ctx.context.createAuthCookie(opts.advanced.webAuthnChallengeCookie);
|
|
173
|
+
await ctx.setSignedCookie(webAuthnCookie.name, verificationToken, ctx.context.secret, {
|
|
174
|
+
...webAuthnCookie.attributes,
|
|
175
|
+
maxAge: maxAgeInSeconds
|
|
176
|
+
});
|
|
177
|
+
const expirationTime = new Date(Date.now() + maxAgeInSeconds * 1e3);
|
|
178
|
+
await ctx.context.internalAdapter.createVerificationValue({
|
|
179
|
+
identifier: verificationToken,
|
|
180
|
+
value: JSON.stringify({
|
|
181
|
+
expectedChallenge: options.challenge,
|
|
182
|
+
userData: {
|
|
183
|
+
id: user.id,
|
|
184
|
+
name: user.name,
|
|
185
|
+
displayName: user.displayName
|
|
186
|
+
},
|
|
187
|
+
context: ctx.query?.context ?? null
|
|
188
|
+
}),
|
|
189
|
+
expiresAt: expirationTime
|
|
190
|
+
});
|
|
191
|
+
return ctx.json(options, { status: 200 });
|
|
144
192
|
});
|
|
145
|
-
|
|
146
|
-
});
|
|
193
|
+
};
|
|
147
194
|
const generatePasskeyAuthenticationOptions = (opts, { maxAgeInSeconds }) => createAuthEndpoint("/passkey/generate-authenticate-options", {
|
|
148
195
|
method: "GET",
|
|
149
196
|
metadata: { openapi: {
|
|
@@ -209,9 +256,12 @@ const generatePasskeyAuthenticationOptions = (opts, { maxAgeInSeconds }) => crea
|
|
|
209
256
|
value: session.user.id
|
|
210
257
|
}]
|
|
211
258
|
});
|
|
259
|
+
const baseURLString = typeof ctx.context.options.baseURL === "string" ? ctx.context.options.baseURL : void 0;
|
|
260
|
+
const authenticationExtensions = await resolveExtensions(opts.authentication?.extensions, ctx);
|
|
212
261
|
const options = await generateAuthenticationOptions({
|
|
213
|
-
rpID: getRpID(opts,
|
|
262
|
+
rpID: getRpID(opts, baseURLString),
|
|
214
263
|
userVerification: "preferred",
|
|
264
|
+
extensions: authenticationExtensions,
|
|
215
265
|
...userPasskeys.length ? { allowCredentials: userPasskeys.map((passkey) => ({
|
|
216
266
|
id: passkey.credentialID,
|
|
217
267
|
transports: passkey.transports?.split(",")
|
|
@@ -239,66 +289,91 @@ const verifyPasskeyRegistrationBodySchema = z.object({
|
|
|
239
289
|
response: z.any(),
|
|
240
290
|
name: z.string().meta({ description: "Name of the passkey" }).optional()
|
|
241
291
|
});
|
|
242
|
-
const verifyPasskeyRegistration = (options) =>
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
292
|
+
const verifyPasskeyRegistration = (options) => {
|
|
293
|
+
const requireSession = options.registration?.requireSession ?? true;
|
|
294
|
+
return createAuthEndpoint("/passkey/verify-registration", {
|
|
295
|
+
method: "POST",
|
|
296
|
+
body: verifyPasskeyRegistrationBodySchema,
|
|
297
|
+
use: requireSession ? [freshSessionMiddleware] : void 0,
|
|
298
|
+
metadata: { openapi: {
|
|
299
|
+
operationId: "passkeyVerifyRegistration",
|
|
300
|
+
description: "Verify registration of a new passkey",
|
|
301
|
+
responses: {
|
|
302
|
+
200: {
|
|
303
|
+
description: "Success",
|
|
304
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/Passkey" } } }
|
|
305
|
+
},
|
|
306
|
+
400: { description: "Bad request" }
|
|
307
|
+
}
|
|
308
|
+
} }
|
|
309
|
+
}, async (ctx) => {
|
|
310
|
+
const origin = options?.origin || ctx.headers?.get("origin") || "";
|
|
311
|
+
if (!origin) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.FAILED_TO_VERIFY_REGISTRATION);
|
|
312
|
+
const resp = ctx.body.response;
|
|
313
|
+
const webAuthnCookie = ctx.context.createAuthCookie(options.advanced.webAuthnChallengeCookie);
|
|
314
|
+
const verificationToken = await ctx.getSignedCookie(webAuthnCookie.name, ctx.context.secret);
|
|
315
|
+
if (!verificationToken) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND);
|
|
316
|
+
const data = await ctx.context.internalAdapter.findVerificationValue(verificationToken);
|
|
317
|
+
if (!data) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND);
|
|
318
|
+
const { expectedChallenge, userData, context } = JSON.parse(data.value);
|
|
319
|
+
const session = requireSession ? ctx.context.session : await getSessionFromCtx(ctx);
|
|
320
|
+
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
|
+
try {
|
|
322
|
+
const verification = await verifyRegistrationResponse({
|
|
323
|
+
response: resp,
|
|
324
|
+
expectedChallenge,
|
|
325
|
+
expectedOrigin: origin,
|
|
326
|
+
expectedRPID: getRpID(options, typeof ctx.context.options.baseURL === "string" ? ctx.context.options.baseURL : void 0),
|
|
327
|
+
requireUserVerification: false
|
|
328
|
+
});
|
|
329
|
+
const { verified, registrationInfo } = verification;
|
|
330
|
+
if (!verified || !registrationInfo) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.FAILED_TO_VERIFY_REGISTRATION);
|
|
331
|
+
const { aaguid, credentialDeviceType, credentialBackedUp, credential } = registrationInfo;
|
|
332
|
+
const resolvedUser = {
|
|
333
|
+
id: userData.id,
|
|
334
|
+
name: userData.name || userData.id,
|
|
335
|
+
displayName: userData.displayName
|
|
336
|
+
};
|
|
337
|
+
let targetUserId = resolvedUser.id;
|
|
338
|
+
if (options.registration?.afterVerification) {
|
|
339
|
+
const result = await options.registration.afterVerification({
|
|
340
|
+
ctx,
|
|
341
|
+
verification,
|
|
342
|
+
user: resolvedUser,
|
|
343
|
+
clientData: resp,
|
|
344
|
+
context
|
|
345
|
+
});
|
|
346
|
+
if (result?.userId) {
|
|
347
|
+
if (typeof result.userId !== "string" || !result.userId) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.RESOLVED_USER_INVALID);
|
|
348
|
+
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
|
+
targetUserId = result.userId;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const pubKey = base64.encode(credential.publicKey);
|
|
353
|
+
const newPasskey = {
|
|
354
|
+
name: ctx.body.name,
|
|
355
|
+
userId: targetUserId,
|
|
356
|
+
credentialID: credential.id,
|
|
357
|
+
publicKey: pubKey,
|
|
358
|
+
counter: credential.counter,
|
|
359
|
+
deviceType: credentialDeviceType,
|
|
360
|
+
transports: resp.response.transports.join(","),
|
|
361
|
+
backedUp: credentialBackedUp,
|
|
362
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
363
|
+
aaguid
|
|
364
|
+
};
|
|
365
|
+
const newPasskeyRes = await ctx.context.adapter.create({
|
|
366
|
+
model: "passkey",
|
|
367
|
+
data: newPasskey
|
|
368
|
+
});
|
|
369
|
+
await ctx.context.internalAdapter.deleteVerificationByIdentifier(verificationToken);
|
|
370
|
+
return ctx.json(newPasskeyRes, { status: 200 });
|
|
371
|
+
} catch (e) {
|
|
372
|
+
ctx.context.logger.error("Failed to verify registration", e);
|
|
373
|
+
throw APIError.from("INTERNAL_SERVER_ERROR", PASSKEY_ERROR_CODES.FAILED_TO_VERIFY_REGISTRATION);
|
|
255
374
|
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
const origin = options?.origin || ctx.headers?.get("origin") || "";
|
|
259
|
-
if (!origin) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.FAILED_TO_VERIFY_REGISTRATION);
|
|
260
|
-
const resp = ctx.body.response;
|
|
261
|
-
const webAuthnCookie = ctx.context.createAuthCookie(options.advanced.webAuthnChallengeCookie);
|
|
262
|
-
const verificationToken = await ctx.getSignedCookie(webAuthnCookie.name, ctx.context.secret);
|
|
263
|
-
if (!verificationToken) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND);
|
|
264
|
-
const data = await ctx.context.internalAdapter.findVerificationValue(verificationToken);
|
|
265
|
-
if (!data) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND);
|
|
266
|
-
const { expectedChallenge, userData } = JSON.parse(data.value);
|
|
267
|
-
if (userData.id !== ctx.context.session.user.id) throw APIError.from("UNAUTHORIZED", PASSKEY_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY);
|
|
268
|
-
try {
|
|
269
|
-
const { verified, registrationInfo } = await verifyRegistrationResponse({
|
|
270
|
-
response: resp,
|
|
271
|
-
expectedChallenge,
|
|
272
|
-
expectedOrigin: origin,
|
|
273
|
-
expectedRPID: getRpID(options, typeof ctx.context.options.baseURL === "string" ? ctx.context.options.baseURL : void 0),
|
|
274
|
-
requireUserVerification: false
|
|
275
|
-
});
|
|
276
|
-
if (!verified || !registrationInfo) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.FAILED_TO_VERIFY_REGISTRATION);
|
|
277
|
-
const { aaguid, credentialDeviceType, credentialBackedUp, credential } = registrationInfo;
|
|
278
|
-
const pubKey = base64.encode(credential.publicKey);
|
|
279
|
-
const newPasskey = {
|
|
280
|
-
name: ctx.body.name,
|
|
281
|
-
userId: userData.id,
|
|
282
|
-
credentialID: credential.id,
|
|
283
|
-
publicKey: pubKey,
|
|
284
|
-
counter: credential.counter,
|
|
285
|
-
deviceType: credentialDeviceType,
|
|
286
|
-
transports: resp.response.transports.join(","),
|
|
287
|
-
backedUp: credentialBackedUp,
|
|
288
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
289
|
-
aaguid
|
|
290
|
-
};
|
|
291
|
-
const newPasskeyRes = await ctx.context.adapter.create({
|
|
292
|
-
model: "passkey",
|
|
293
|
-
data: newPasskey
|
|
294
|
-
});
|
|
295
|
-
await ctx.context.internalAdapter.deleteVerificationByIdentifier(verificationToken);
|
|
296
|
-
return ctx.json(newPasskeyRes, { status: 200 });
|
|
297
|
-
} catch (e) {
|
|
298
|
-
ctx.context.logger.error("Failed to verify registration", e);
|
|
299
|
-
throw APIError.from("INTERNAL_SERVER_ERROR", PASSKEY_ERROR_CODES.FAILED_TO_VERIFY_REGISTRATION);
|
|
300
|
-
}
|
|
301
|
-
});
|
|
375
|
+
});
|
|
376
|
+
};
|
|
302
377
|
const verifyPasskeyAuthenticationBodySchema = z.object({ response: z.record(z.any(), z.any()) });
|
|
303
378
|
const verifyPasskeyAuthentication = (options) => createAuthEndpoint("/passkey/verify-authentication", {
|
|
304
379
|
method: "POST",
|
|
@@ -354,6 +429,11 @@ const verifyPasskeyAuthentication = (options) => createAuthEndpoint("/passkey/ve
|
|
|
354
429
|
});
|
|
355
430
|
const { verified } = verification;
|
|
356
431
|
if (!verified) throw APIError.from("UNAUTHORIZED", PASSKEY_ERROR_CODES.AUTHENTICATION_FAILED);
|
|
432
|
+
if (options.authentication?.afterVerification) await options.authentication.afterVerification({
|
|
433
|
+
ctx,
|
|
434
|
+
verification,
|
|
435
|
+
clientData: resp
|
|
436
|
+
});
|
|
357
437
|
await ctx.context.adapter.update({
|
|
358
438
|
model: "passkey",
|
|
359
439
|
where: [{
|
|
@@ -443,7 +523,13 @@ const listPasskeys = createAuthEndpoint("/passkey/list-user-passkeys", {
|
|
|
443
523
|
const deletePasskey = createAuthEndpoint("/passkey/delete-passkey", {
|
|
444
524
|
method: "POST",
|
|
445
525
|
body: z.object({ id: z.string().meta({ description: "The ID of the passkey to delete. Eg: \"some-passkey-id\"" }) }),
|
|
446
|
-
use: [sessionMiddleware
|
|
526
|
+
use: [sessionMiddleware, requireResourceOwnership({
|
|
527
|
+
model: "passkey",
|
|
528
|
+
idParam: "id",
|
|
529
|
+
idSource: "body",
|
|
530
|
+
notFoundError: PASSKEY_ERROR_CODES.PASSKEY_NOT_FOUND,
|
|
531
|
+
forbiddenStatus: "UNAUTHORIZED"
|
|
532
|
+
})],
|
|
447
533
|
metadata: { openapi: {
|
|
448
534
|
description: "Delete a specific passkey",
|
|
449
535
|
responses: { "200": {
|
|
@@ -459,20 +545,11 @@ const deletePasskey = createAuthEndpoint("/passkey/delete-passkey", {
|
|
|
459
545
|
} }
|
|
460
546
|
} }
|
|
461
547
|
}, async (ctx) => {
|
|
462
|
-
const passkey = await ctx.context.adapter.findOne({
|
|
463
|
-
model: "passkey",
|
|
464
|
-
where: [{
|
|
465
|
-
field: "id",
|
|
466
|
-
value: ctx.body.id
|
|
467
|
-
}]
|
|
468
|
-
});
|
|
469
|
-
if (!passkey) throw APIError.from("NOT_FOUND", PASSKEY_ERROR_CODES.PASSKEY_NOT_FOUND);
|
|
470
|
-
if (passkey.userId !== ctx.context.session.user.id) throw new APIError("UNAUTHORIZED");
|
|
471
548
|
await ctx.context.adapter.delete({
|
|
472
549
|
model: "passkey",
|
|
473
550
|
where: [{
|
|
474
551
|
field: "id",
|
|
475
|
-
value:
|
|
552
|
+
value: ctx.body.id
|
|
476
553
|
}]
|
|
477
554
|
});
|
|
478
555
|
return ctx.json({ status: true });
|
|
@@ -498,7 +575,14 @@ const updatePasskey = createAuthEndpoint("/passkey/update-passkey", {
|
|
|
498
575
|
id: z.string().meta({ description: `The ID of the passkey which will be updated. Eg: \"passkey-id\"` }),
|
|
499
576
|
name: z.string().meta({ description: `The new name which the passkey will be updated to. Eg: \"my-new-passkey-name\"` })
|
|
500
577
|
}),
|
|
501
|
-
use: [sessionMiddleware
|
|
578
|
+
use: [sessionMiddleware, requireResourceOwnership({
|
|
579
|
+
model: "passkey",
|
|
580
|
+
idParam: "id",
|
|
581
|
+
idSource: "body",
|
|
582
|
+
notFoundError: PASSKEY_ERROR_CODES.PASSKEY_NOT_FOUND,
|
|
583
|
+
forbiddenError: PASSKEY_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY,
|
|
584
|
+
forbiddenStatus: "UNAUTHORIZED"
|
|
585
|
+
})],
|
|
502
586
|
metadata: { openapi: {
|
|
503
587
|
description: "Update a specific passkey's name",
|
|
504
588
|
responses: { "200": {
|
|
@@ -511,15 +595,6 @@ const updatePasskey = createAuthEndpoint("/passkey/update-passkey", {
|
|
|
511
595
|
} }
|
|
512
596
|
} }
|
|
513
597
|
}, async (ctx) => {
|
|
514
|
-
const passkey = await ctx.context.adapter.findOne({
|
|
515
|
-
model: "passkey",
|
|
516
|
-
where: [{
|
|
517
|
-
field: "id",
|
|
518
|
-
value: ctx.body.id
|
|
519
|
-
}]
|
|
520
|
-
});
|
|
521
|
-
if (!passkey) throw APIError.from("NOT_FOUND", PASSKEY_ERROR_CODES.PASSKEY_NOT_FOUND);
|
|
522
|
-
if (passkey.userId !== ctx.context.session.user.id) throw APIError.from("UNAUTHORIZED", PASSKEY_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY);
|
|
523
598
|
const updatedPasskey = await ctx.context.adapter.update({
|
|
524
599
|
model: "passkey",
|
|
525
600
|
where: [{
|
|
@@ -595,6 +670,7 @@ const passkey = (options) => {
|
|
|
595
670
|
};
|
|
596
671
|
return {
|
|
597
672
|
id: "passkey",
|
|
673
|
+
version: PACKAGE_VERSION,
|
|
598
674
|
endpoints: {
|
|
599
675
|
generatePasskeyRegistrationOptions: generatePasskeyRegistrationOptions(opts, { maxAgeInSeconds: MAX_AGE_IN_SECONDS }),
|
|
600
676
|
generatePasskeyAuthenticationOptions: generatePasskeyAuthenticationOptions(opts, { maxAgeInSeconds: MAX_AGE_IN_SECONDS }),
|
|
@@ -611,5 +687,3 @@ const passkey = (options) => {
|
|
|
611
687
|
};
|
|
612
688
|
//#endregion
|
|
613
689
|
export { PASSKEY_ERROR_CODES, passkey };
|
|
614
|
-
|
|
615
|
-
//# sourceMappingURL=index.mjs.map
|
|
@@ -11,9 +11,13 @@ const PASSKEY_ERROR_CODES = defineErrorCodes({
|
|
|
11
11
|
PREVIOUSLY_REGISTERED: "Previously registered",
|
|
12
12
|
REGISTRATION_CANCELLED: "Registration cancelled",
|
|
13
13
|
AUTH_CANCELLED: "Auth cancelled",
|
|
14
|
-
UNKNOWN_ERROR: "Unknown error"
|
|
14
|
+
UNKNOWN_ERROR: "Unknown error",
|
|
15
|
+
SESSION_REQUIRED: "Passkey registration requires an authenticated session",
|
|
16
|
+
RESOLVE_USER_REQUIRED: "Passkey registration requires either an authenticated session or a resolveUser callback when requireSession is false",
|
|
17
|
+
RESOLVED_USER_INVALID: "Resolved user is invalid"
|
|
15
18
|
});
|
|
16
19
|
//#endregion
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
//#
|
|
20
|
+
//#region src/version.ts
|
|
21
|
+
const PACKAGE_VERSION = "1.6.0";
|
|
22
|
+
//#endregion
|
|
23
|
+
export { PASSKEY_ERROR_CODES as n, PACKAGE_VERSION as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-auth/passkey",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Passkey plugin for Better Auth",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"publishConfig": {
|
|
20
20
|
"access": "public"
|
|
21
21
|
},
|
|
22
|
+
"sideEffects": false,
|
|
22
23
|
"files": [
|
|
23
24
|
"dist"
|
|
24
25
|
],
|
|
@@ -54,21 +55,21 @@
|
|
|
54
55
|
},
|
|
55
56
|
"devDependencies": {
|
|
56
57
|
"tsdown": "0.21.1",
|
|
57
|
-
"@better-auth/core": "1.
|
|
58
|
-
"better-auth": "1.
|
|
58
|
+
"@better-auth/core": "1.6.0",
|
|
59
|
+
"better-auth": "1.6.0"
|
|
59
60
|
},
|
|
60
61
|
"peerDependencies": {
|
|
61
|
-
"@better-auth/utils": "0.
|
|
62
|
+
"@better-auth/utils": "0.4.0",
|
|
62
63
|
"@better-fetch/fetch": "1.1.21",
|
|
63
|
-
"better-call": "
|
|
64
|
+
"better-call": "1.3.5",
|
|
64
65
|
"nanostores": "^1.0.1",
|
|
65
|
-
"@better-auth/core": "1.
|
|
66
|
-
"better-auth": "1.
|
|
66
|
+
"@better-auth/core": "^1.6.0",
|
|
67
|
+
"better-auth": "^1.6.0"
|
|
67
68
|
},
|
|
68
69
|
"scripts": {
|
|
69
70
|
"build": "tsdown",
|
|
70
71
|
"dev": "tsdown --watch",
|
|
71
|
-
"lint:package": "publint run --strict",
|
|
72
|
+
"lint:package": "publint run --strict --pack false",
|
|
72
73
|
"lint:types": "attw --profile esm-only --pack .",
|
|
73
74
|
"typecheck": "tsc --project tsconfig.json",
|
|
74
75
|
"test": "vitest",
|
package/dist/client.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"client.mjs","names":[],"sources":["../src/client.ts"],"sourcesContent":["import type {\n\tBetterAuthClientPlugin,\n\tClientFetchOption,\n\tClientStore,\n} from \"@better-auth/core\";\nimport type { BetterFetch } from \"@better-fetch/fetch\";\nimport type {\n\tPublicKeyCredentialCreationOptionsJSON,\n\tPublicKeyCredentialRequestOptionsJSON,\n} from \"@simplewebauthn/browser\";\nimport {\n\tstartAuthentication,\n\tstartRegistration,\n\tWebAuthnError,\n} from \"@simplewebauthn/browser\";\nimport { useAuthQuery } from \"better-auth/client\";\nimport type { Session, User } from \"better-auth/types\";\nimport { atom } from \"nanostores\";\nimport type { passkey } from \".\";\nimport { PASSKEY_ERROR_CODES } from \"./error-codes\";\nimport type { Passkey } from \"./types\";\n\nexport const getPasskeyActions = (\n\t$fetch: BetterFetch,\n\t{\n\t\t$listPasskeys,\n\t\t$store,\n\t}: {\n\t\t$listPasskeys: ReturnType<typeof atom<any>>;\n\t\t$store: ClientStore;\n\t},\n) => {\n\tconst signInPasskey = async (\n\t\topts?:\n\t\t\t| {\n\t\t\t\t\tautoFill?: boolean;\n\t\t\t\t\tfetchOptions?: ClientFetchOption;\n\t\t\t }\n\t\t\t| undefined,\n\t\toptions?: ClientFetchOption | undefined,\n\t) => {\n\t\tconst response = await $fetch<PublicKeyCredentialRequestOptionsJSON>(\n\t\t\t\"/passkey/generate-authenticate-options\",\n\t\t\t{\n\t\t\t\tmethod: \"GET\",\n\t\t\t\tthrow: false,\n\t\t\t},\n\t\t);\n\t\tif (!response.data) {\n\t\t\treturn response;\n\t\t}\n\t\ttry {\n\t\t\tconst res = await startAuthentication({\n\t\t\t\toptionsJSON: response.data,\n\t\t\t\tuseBrowserAutofill: opts?.autoFill,\n\t\t\t});\n\t\t\tconst verified = await $fetch<{\n\t\t\t\tsession: Session;\n\t\t\t\tuser: User;\n\t\t\t}>(\"/passkey/verify-authentication\", {\n\t\t\t\tbody: {\n\t\t\t\t\tresponse: res,\n\t\t\t\t},\n\t\t\t\t...opts?.fetchOptions,\n\t\t\t\t...options,\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tthrow: false,\n\t\t\t});\n\t\t\t$listPasskeys.set(Math.random());\n\t\t\t$store.notify(\"$sessionSignal\");\n\n\t\t\treturn verified;\n\t\t} catch (err) {\n\t\t\t// Error logs ran on the front-end\n\t\t\tconsole.error(`[Better Auth] Error verifying passkey`, err);\n\t\t\treturn {\n\t\t\t\tdata: null,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"AUTH_CANCELLED\",\n\t\t\t\t\tmessage: PASSKEY_ERROR_CODES.AUTH_CANCELLED,\n\t\t\t\t\tstatus: 400,\n\t\t\t\t\tstatusText: \"BAD_REQUEST\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t};\n\n\tconst registerPasskey = async (\n\t\topts?:\n\t\t\t| {\n\t\t\t\t\tfetchOptions?: ClientFetchOption;\n\t\t\t\t\t/**\n\t\t\t\t\t * The name of the passkey. This is used to\n\t\t\t\t\t * identify the passkey in the UI.\n\t\t\t\t\t */\n\t\t\t\t\tname?: string;\n\n\t\t\t\t\t/**\n\t\t\t\t\t * The type of attachment for the passkey. Defaults to both\n\t\t\t\t\t * platform and cross-platform allowed, with platform preferred.\n\t\t\t\t\t */\n\t\t\t\t\tauthenticatorAttachment?: \"platform\" | \"cross-platform\";\n\n\t\t\t\t\t/**\n\t\t\t\t\t * Try to silently create a passkey with the password manager that the user just signed\n\t\t\t\t\t * in with.\n\t\t\t\t\t * @default false\n\t\t\t\t\t */\n\t\t\t\t\tuseAutoRegister?: boolean;\n\t\t\t }\n\t\t\t| undefined,\n\t\tfetchOpts?: ClientFetchOption | undefined,\n\t) => {\n\t\tconst options = await $fetch<PublicKeyCredentialCreationOptionsJSON>(\n\t\t\t\"/passkey/generate-register-options\",\n\t\t\t{\n\t\t\t\tmethod: \"GET\",\n\t\t\t\tquery: {\n\t\t\t\t\t...(opts?.authenticatorAttachment && {\n\t\t\t\t\t\tauthenticatorAttachment: opts.authenticatorAttachment,\n\t\t\t\t\t}),\n\t\t\t\t\t...(opts?.name && {\n\t\t\t\t\t\tname: opts.name,\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t\tthrow: false,\n\t\t\t},\n\t\t);\n\n\t\tif (!options.data) {\n\t\t\treturn options;\n\t\t}\n\t\ttry {\n\t\t\tconst res = await startRegistration({\n\t\t\t\toptionsJSON: options.data,\n\t\t\t\tuseAutoRegister: opts?.useAutoRegister,\n\t\t\t});\n\t\t\tconst verified = await $fetch<Passkey>(\"/passkey/verify-registration\", {\n\t\t\t\t...opts?.fetchOptions,\n\t\t\t\t...fetchOpts,\n\t\t\t\tbody: {\n\t\t\t\t\tresponse: res,\n\t\t\t\t\tname: opts?.name,\n\t\t\t\t},\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tthrow: false,\n\t\t\t});\n\n\t\t\tif (!verified.data) {\n\t\t\t\treturn verified;\n\t\t\t}\n\t\t\t$listPasskeys.set(Math.random());\n\t\t\treturn verified;\n\t\t} catch (e) {\n\t\t\tif (e instanceof WebAuthnError) {\n\t\t\t\tif (e.code === \"ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED\") {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tdata: null,\n\t\t\t\t\t\terror: {\n\t\t\t\t\t\t\tcode: e.code,\n\t\t\t\t\t\t\tmessage: PASSKEY_ERROR_CODES.PREVIOUSLY_REGISTERED,\n\t\t\t\t\t\t\tstatus: 400,\n\t\t\t\t\t\t\tstatusText: \"BAD_REQUEST\",\n\t\t\t\t\t\t},\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tif (e.code === \"ERROR_CEREMONY_ABORTED\") {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tdata: null,\n\t\t\t\t\t\terror: {\n\t\t\t\t\t\t\tcode: e.code,\n\t\t\t\t\t\t\tmessage: PASSKEY_ERROR_CODES.REGISTRATION_CANCELLED,\n\t\t\t\t\t\t\tstatus: 400,\n\t\t\t\t\t\t\tstatusText: \"BAD_REQUEST\",\n\t\t\t\t\t\t},\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\treturn {\n\t\t\t\t\tdata: null,\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode: e.code,\n\t\t\t\t\t\tmessage: e.message,\n\t\t\t\t\t\tstatus: 400,\n\t\t\t\t\t\tstatusText: \"BAD_REQUEST\",\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tdata: null,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"UNKNOWN_ERROR\",\n\t\t\t\t\tmessage:\n\t\t\t\t\t\te instanceof Error ? e.message : PASSKEY_ERROR_CODES.UNKNOWN_ERROR,\n\t\t\t\t\tstatus: 500,\n\t\t\t\t\tstatusText: \"INTERNAL_SERVER_ERROR\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t};\n\n\treturn {\n\t\tsignIn: {\n\t\t\t/**\n\t\t\t * Sign in with a registered passkey\n\t\t\t */\n\t\t\tpasskey: signInPasskey,\n\t\t},\n\t\tpasskey: {\n\t\t\t/**\n\t\t\t * Add a passkey to the user account\n\t\t\t */\n\t\t\taddPasskey: registerPasskey,\n\t\t},\n\t\t/**\n\t\t * Inferred Internal Types\n\t\t */\n\t\t$Infer: {} as {\n\t\t\tPasskey: Passkey;\n\t\t},\n\t};\n};\n\nexport const passkeyClient = () => {\n\tconst $listPasskeys = atom<any>();\n\treturn {\n\t\tid: \"passkey\",\n\t\t$InferServerPlugin: {} as ReturnType<typeof passkey>,\n\t\tgetActions: ($fetch, $store) =>\n\t\t\tgetPasskeyActions($fetch, {\n\t\t\t\t$listPasskeys,\n\t\t\t\t$store,\n\t\t\t}),\n\t\tgetAtoms($fetch) {\n\t\t\tconst listPasskeys = useAuthQuery<Passkey[]>(\n\t\t\t\t$listPasskeys,\n\t\t\t\t\"/passkey/list-user-passkeys\",\n\t\t\t\t$fetch,\n\t\t\t\t{\n\t\t\t\t\tmethod: \"GET\",\n\t\t\t\t},\n\t\t\t);\n\t\t\treturn {\n\t\t\t\tlistPasskeys,\n\t\t\t\t$listPasskeys,\n\t\t\t};\n\t\t},\n\t\tpathMethods: {\n\t\t\t\"/passkey/register\": \"POST\",\n\t\t\t\"/passkey/authenticate\": \"POST\",\n\t\t},\n\t\tatomListeners: [\n\t\t\t{\n\t\t\t\tmatcher(path) {\n\t\t\t\t\treturn (\n\t\t\t\t\t\tpath === \"/passkey/verify-registration\" ||\n\t\t\t\t\t\tpath === \"/passkey/delete-passkey\" ||\n\t\t\t\t\t\tpath === \"/passkey/update-passkey\" ||\n\t\t\t\t\t\tpath === \"/sign-out\"\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t\tsignal: \"$listPasskeys\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tmatcher: (path) => path === \"/passkey/verify-authentication\",\n\t\t\t\tsignal: \"$sessionSignal\",\n\t\t\t},\n\t\t],\n\t\t$ERROR_CODES: PASSKEY_ERROR_CODES,\n\t} satisfies BetterAuthClientPlugin;\n};\n\nexport type * from \"@simplewebauthn/server\";\nexport * from \"./error-codes\";\nexport type * from \"./types\";\n"],"mappings":"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"}
|