@better-auth/passkey 1.4.0-beta.16
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/LICENSE.md +17 -0
- package/README.md +54 -0
- package/dist/client.d.ts +199 -0
- package/dist/client.js +145 -0
- package/dist/index-B_gZWjC3.d.ts +740 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +558 -0
- package/package.json +73 -0
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
2
|
+
import { base64 } from "@better-auth/utils/base64";
|
|
3
|
+
import { generateAuthenticationOptions, generateRegistrationOptions, verifyAuthenticationResponse, verifyRegistrationResponse } from "@simplewebauthn/server";
|
|
4
|
+
import { generateId } from "better-auth";
|
|
5
|
+
import { freshSessionMiddleware, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
|
|
6
|
+
import { setSessionCookie } from "better-auth/cookies";
|
|
7
|
+
import { generateRandomString } from "better-auth/crypto";
|
|
8
|
+
import { mergeSchema } from "better-auth/db";
|
|
9
|
+
import { APIError } from "better-call";
|
|
10
|
+
import * as z from "zod";
|
|
11
|
+
import { defineErrorCodes } from "@better-auth/core/utils";
|
|
12
|
+
|
|
13
|
+
//#region src/error-codes.ts
|
|
14
|
+
const PASSKEY_ERROR_CODES = defineErrorCodes({
|
|
15
|
+
CHALLENGE_NOT_FOUND: "Challenge not found",
|
|
16
|
+
YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY: "You are not allowed to register this passkey",
|
|
17
|
+
FAILED_TO_VERIFY_REGISTRATION: "Failed to verify registration",
|
|
18
|
+
PASSKEY_NOT_FOUND: "Passkey not found",
|
|
19
|
+
AUTHENTICATION_FAILED: "Authentication failed",
|
|
20
|
+
UNABLE_TO_CREATE_SESSION: "Unable to create session",
|
|
21
|
+
FAILED_TO_UPDATE_PASSKEY: "Failed to update passkey"
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/schema.ts
|
|
26
|
+
const schema = { passkey: { fields: {
|
|
27
|
+
name: {
|
|
28
|
+
type: "string",
|
|
29
|
+
required: false
|
|
30
|
+
},
|
|
31
|
+
publicKey: {
|
|
32
|
+
type: "string",
|
|
33
|
+
required: true
|
|
34
|
+
},
|
|
35
|
+
userId: {
|
|
36
|
+
type: "string",
|
|
37
|
+
references: {
|
|
38
|
+
model: "user",
|
|
39
|
+
field: "id"
|
|
40
|
+
},
|
|
41
|
+
required: true
|
|
42
|
+
},
|
|
43
|
+
credentialID: {
|
|
44
|
+
type: "string",
|
|
45
|
+
required: true
|
|
46
|
+
},
|
|
47
|
+
counter: {
|
|
48
|
+
type: "number",
|
|
49
|
+
required: true
|
|
50
|
+
},
|
|
51
|
+
deviceType: {
|
|
52
|
+
type: "string",
|
|
53
|
+
required: true
|
|
54
|
+
},
|
|
55
|
+
backedUp: {
|
|
56
|
+
type: "boolean",
|
|
57
|
+
required: true
|
|
58
|
+
},
|
|
59
|
+
transports: {
|
|
60
|
+
type: "string",
|
|
61
|
+
required: false
|
|
62
|
+
},
|
|
63
|
+
createdAt: {
|
|
64
|
+
type: "date",
|
|
65
|
+
required: false
|
|
66
|
+
},
|
|
67
|
+
aaguid: {
|
|
68
|
+
type: "string",
|
|
69
|
+
required: false
|
|
70
|
+
}
|
|
71
|
+
} } };
|
|
72
|
+
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/utils.ts
|
|
75
|
+
function getRpID(options, baseURL) {
|
|
76
|
+
return options.rpID || (baseURL ? new URL(baseURL).hostname : "localhost");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region src/index.ts
|
|
81
|
+
const passkey = (options) => {
|
|
82
|
+
const opts = {
|
|
83
|
+
origin: null,
|
|
84
|
+
...options,
|
|
85
|
+
advanced: {
|
|
86
|
+
webAuthnChallengeCookie: "better-auth-passkey",
|
|
87
|
+
...options?.advanced
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const expirationTime = new Date(Date.now() + 1e3 * 60 * 5);
|
|
91
|
+
const currentTime = /* @__PURE__ */ new Date();
|
|
92
|
+
const maxAgeInSeconds = Math.floor((expirationTime.getTime() - currentTime.getTime()) / 1e3);
|
|
93
|
+
return {
|
|
94
|
+
id: "passkey",
|
|
95
|
+
endpoints: {
|
|
96
|
+
generatePasskeyRegistrationOptions: createAuthEndpoint("/passkey/generate-register-options", {
|
|
97
|
+
method: "GET",
|
|
98
|
+
use: [freshSessionMiddleware],
|
|
99
|
+
query: z.object({
|
|
100
|
+
authenticatorAttachment: z.enum(["platform", "cross-platform"]).optional(),
|
|
101
|
+
name: z.string().optional()
|
|
102
|
+
}).optional(),
|
|
103
|
+
metadata: {
|
|
104
|
+
client: false,
|
|
105
|
+
openapi: {
|
|
106
|
+
description: "Generate registration options for a new passkey",
|
|
107
|
+
responses: { 200: {
|
|
108
|
+
description: "Success",
|
|
109
|
+
parameters: { query: {
|
|
110
|
+
authenticatorAttachment: {
|
|
111
|
+
description: `Type of authenticator to use for registration.
|
|
112
|
+
"platform" for device-specific authenticators,
|
|
113
|
+
"cross-platform" for authenticators that can be used across devices.`,
|
|
114
|
+
required: false
|
|
115
|
+
},
|
|
116
|
+
name: {
|
|
117
|
+
description: `Optional custom name for the passkey.
|
|
118
|
+
This can help identify the passkey when managing multiple credentials.`,
|
|
119
|
+
required: false
|
|
120
|
+
}
|
|
121
|
+
} },
|
|
122
|
+
content: { "application/json": { schema: {
|
|
123
|
+
type: "object",
|
|
124
|
+
properties: {
|
|
125
|
+
challenge: { type: "string" },
|
|
126
|
+
rp: {
|
|
127
|
+
type: "object",
|
|
128
|
+
properties: {
|
|
129
|
+
name: { type: "string" },
|
|
130
|
+
id: { type: "string" }
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
user: {
|
|
134
|
+
type: "object",
|
|
135
|
+
properties: {
|
|
136
|
+
id: { type: "string" },
|
|
137
|
+
name: { type: "string" },
|
|
138
|
+
displayName: { type: "string" }
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
pubKeyCredParams: {
|
|
142
|
+
type: "array",
|
|
143
|
+
items: {
|
|
144
|
+
type: "object",
|
|
145
|
+
properties: {
|
|
146
|
+
type: { type: "string" },
|
|
147
|
+
alg: { type: "number" }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
timeout: { type: "number" },
|
|
152
|
+
excludeCredentials: {
|
|
153
|
+
type: "array",
|
|
154
|
+
items: {
|
|
155
|
+
type: "object",
|
|
156
|
+
properties: {
|
|
157
|
+
id: { type: "string" },
|
|
158
|
+
type: { type: "string" },
|
|
159
|
+
transports: {
|
|
160
|
+
type: "array",
|
|
161
|
+
items: { type: "string" }
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
authenticatorSelection: {
|
|
167
|
+
type: "object",
|
|
168
|
+
properties: {
|
|
169
|
+
authenticatorAttachment: { type: "string" },
|
|
170
|
+
requireResidentKey: { type: "boolean" },
|
|
171
|
+
userVerification: { type: "string" }
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
attestation: { type: "string" },
|
|
175
|
+
extensions: { type: "object" }
|
|
176
|
+
}
|
|
177
|
+
} } }
|
|
178
|
+
} }
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}, async (ctx) => {
|
|
182
|
+
const { session } = ctx.context;
|
|
183
|
+
const userPasskeys = await ctx.context.adapter.findMany({
|
|
184
|
+
model: "passkey",
|
|
185
|
+
where: [{
|
|
186
|
+
field: "userId",
|
|
187
|
+
value: session.user.id
|
|
188
|
+
}]
|
|
189
|
+
});
|
|
190
|
+
const userID = new TextEncoder().encode(generateRandomString(32, "a-z", "0-9"));
|
|
191
|
+
let options$1;
|
|
192
|
+
options$1 = await generateRegistrationOptions({
|
|
193
|
+
rpName: opts.rpName || ctx.context.appName,
|
|
194
|
+
rpID: getRpID(opts, ctx.context.options.baseURL),
|
|
195
|
+
userID,
|
|
196
|
+
userName: ctx.query?.name || session.user.email || session.user.id,
|
|
197
|
+
userDisplayName: session.user.email || session.user.id,
|
|
198
|
+
attestationType: "none",
|
|
199
|
+
excludeCredentials: userPasskeys.map((passkey$1) => ({
|
|
200
|
+
id: passkey$1.credentialID,
|
|
201
|
+
transports: passkey$1.transports?.split(",")
|
|
202
|
+
})),
|
|
203
|
+
authenticatorSelection: {
|
|
204
|
+
residentKey: "preferred",
|
|
205
|
+
userVerification: "preferred",
|
|
206
|
+
...opts.authenticatorSelection || {},
|
|
207
|
+
...ctx.query?.authenticatorAttachment ? { authenticatorAttachment: ctx.query.authenticatorAttachment } : {}
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
const id = generateId(32);
|
|
211
|
+
const webAuthnCookie = ctx.context.createAuthCookie(opts.advanced.webAuthnChallengeCookie);
|
|
212
|
+
await ctx.setSignedCookie(webAuthnCookie.name, id, ctx.context.secret, {
|
|
213
|
+
...webAuthnCookie.attributes,
|
|
214
|
+
maxAge: maxAgeInSeconds
|
|
215
|
+
});
|
|
216
|
+
await ctx.context.internalAdapter.createVerificationValue({
|
|
217
|
+
identifier: id,
|
|
218
|
+
value: JSON.stringify({
|
|
219
|
+
expectedChallenge: options$1.challenge,
|
|
220
|
+
userData: { id: session.user.id }
|
|
221
|
+
}),
|
|
222
|
+
expiresAt: expirationTime
|
|
223
|
+
});
|
|
224
|
+
return ctx.json(options$1, { status: 200 });
|
|
225
|
+
}),
|
|
226
|
+
generatePasskeyAuthenticationOptions: createAuthEndpoint("/passkey/generate-authenticate-options", {
|
|
227
|
+
method: "POST",
|
|
228
|
+
metadata: { openapi: {
|
|
229
|
+
description: "Generate authentication options for a passkey",
|
|
230
|
+
responses: { 200: {
|
|
231
|
+
description: "Success",
|
|
232
|
+
content: { "application/json": { schema: {
|
|
233
|
+
type: "object",
|
|
234
|
+
properties: {
|
|
235
|
+
challenge: { type: "string" },
|
|
236
|
+
rp: {
|
|
237
|
+
type: "object",
|
|
238
|
+
properties: {
|
|
239
|
+
name: { type: "string" },
|
|
240
|
+
id: { type: "string" }
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
user: {
|
|
244
|
+
type: "object",
|
|
245
|
+
properties: {
|
|
246
|
+
id: { type: "string" },
|
|
247
|
+
name: { type: "string" },
|
|
248
|
+
displayName: { type: "string" }
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
timeout: { type: "number" },
|
|
252
|
+
allowCredentials: {
|
|
253
|
+
type: "array",
|
|
254
|
+
items: {
|
|
255
|
+
type: "object",
|
|
256
|
+
properties: {
|
|
257
|
+
id: { type: "string" },
|
|
258
|
+
type: { type: "string" },
|
|
259
|
+
transports: {
|
|
260
|
+
type: "array",
|
|
261
|
+
items: { type: "string" }
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
userVerification: { type: "string" },
|
|
267
|
+
authenticatorSelection: {
|
|
268
|
+
type: "object",
|
|
269
|
+
properties: {
|
|
270
|
+
authenticatorAttachment: { type: "string" },
|
|
271
|
+
requireResidentKey: { type: "boolean" },
|
|
272
|
+
userVerification: { type: "string" }
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
extensions: { type: "object" }
|
|
276
|
+
}
|
|
277
|
+
} } }
|
|
278
|
+
} }
|
|
279
|
+
} }
|
|
280
|
+
}, async (ctx) => {
|
|
281
|
+
const session = await getSessionFromCtx(ctx);
|
|
282
|
+
let userPasskeys = [];
|
|
283
|
+
if (session) userPasskeys = await ctx.context.adapter.findMany({
|
|
284
|
+
model: "passkey",
|
|
285
|
+
where: [{
|
|
286
|
+
field: "userId",
|
|
287
|
+
value: session.user.id
|
|
288
|
+
}]
|
|
289
|
+
});
|
|
290
|
+
const options$1 = await generateAuthenticationOptions({
|
|
291
|
+
rpID: getRpID(opts, ctx.context.options.baseURL),
|
|
292
|
+
userVerification: "preferred",
|
|
293
|
+
...userPasskeys.length ? { allowCredentials: userPasskeys.map((passkey$1) => ({
|
|
294
|
+
id: passkey$1.credentialID,
|
|
295
|
+
transports: passkey$1.transports?.split(",")
|
|
296
|
+
})) } : {}
|
|
297
|
+
});
|
|
298
|
+
const data = {
|
|
299
|
+
expectedChallenge: options$1.challenge,
|
|
300
|
+
userData: { id: session?.user.id || "" }
|
|
301
|
+
};
|
|
302
|
+
const id = generateId(32);
|
|
303
|
+
const webAuthnCookie = ctx.context.createAuthCookie(opts.advanced.webAuthnChallengeCookie);
|
|
304
|
+
await ctx.setSignedCookie(webAuthnCookie.name, id, ctx.context.secret, {
|
|
305
|
+
...webAuthnCookie.attributes,
|
|
306
|
+
maxAge: maxAgeInSeconds
|
|
307
|
+
});
|
|
308
|
+
await ctx.context.internalAdapter.createVerificationValue({
|
|
309
|
+
identifier: id,
|
|
310
|
+
value: JSON.stringify(data),
|
|
311
|
+
expiresAt: expirationTime
|
|
312
|
+
});
|
|
313
|
+
return ctx.json(options$1, { status: 200 });
|
|
314
|
+
}),
|
|
315
|
+
verifyPasskeyRegistration: createAuthEndpoint("/passkey/verify-registration", {
|
|
316
|
+
method: "POST",
|
|
317
|
+
body: z.object({
|
|
318
|
+
response: z.any(),
|
|
319
|
+
name: z.string().meta({ description: "Name of the passkey" }).optional()
|
|
320
|
+
}),
|
|
321
|
+
use: [freshSessionMiddleware],
|
|
322
|
+
metadata: { openapi: {
|
|
323
|
+
description: "Verify registration of a new passkey",
|
|
324
|
+
responses: {
|
|
325
|
+
200: {
|
|
326
|
+
description: "Success",
|
|
327
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/Passkey" } } }
|
|
328
|
+
},
|
|
329
|
+
400: { description: "Bad request" }
|
|
330
|
+
}
|
|
331
|
+
} }
|
|
332
|
+
}, async (ctx) => {
|
|
333
|
+
const origin = options?.origin || ctx.headers?.get("origin") || "";
|
|
334
|
+
if (!origin) return ctx.json(null, { status: 400 });
|
|
335
|
+
const resp = ctx.body.response;
|
|
336
|
+
const webAuthnCookie = ctx.context.createAuthCookie(opts.advanced.webAuthnChallengeCookie);
|
|
337
|
+
const challengeId = await ctx.getSignedCookie(webAuthnCookie.name, ctx.context.secret);
|
|
338
|
+
if (!challengeId) throw new APIError("BAD_REQUEST", { message: PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND });
|
|
339
|
+
const data = await ctx.context.internalAdapter.findVerificationValue(challengeId);
|
|
340
|
+
if (!data) return ctx.json(null, { status: 400 });
|
|
341
|
+
const { expectedChallenge, userData } = JSON.parse(data.value);
|
|
342
|
+
if (userData.id !== ctx.context.session.user.id) throw new APIError("UNAUTHORIZED", { message: PASSKEY_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY });
|
|
343
|
+
try {
|
|
344
|
+
const { verified, registrationInfo } = await verifyRegistrationResponse({
|
|
345
|
+
response: resp,
|
|
346
|
+
expectedChallenge,
|
|
347
|
+
expectedOrigin: origin,
|
|
348
|
+
expectedRPID: getRpID(opts, ctx.context.options.baseURL),
|
|
349
|
+
requireUserVerification: false
|
|
350
|
+
});
|
|
351
|
+
if (!verified || !registrationInfo) return ctx.json(null, { status: 400 });
|
|
352
|
+
const { aaguid, credentialDeviceType, credentialBackedUp, credential, credentialType } = registrationInfo;
|
|
353
|
+
const pubKey = base64.encode(credential.publicKey);
|
|
354
|
+
const newPasskey = {
|
|
355
|
+
name: ctx.body.name,
|
|
356
|
+
userId: userData.id,
|
|
357
|
+
credentialID: credential.id,
|
|
358
|
+
publicKey: pubKey,
|
|
359
|
+
counter: credential.counter,
|
|
360
|
+
deviceType: credentialDeviceType,
|
|
361
|
+
transports: resp.response.transports.join(","),
|
|
362
|
+
backedUp: credentialBackedUp,
|
|
363
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
364
|
+
aaguid
|
|
365
|
+
};
|
|
366
|
+
const newPasskeyRes = await ctx.context.adapter.create({
|
|
367
|
+
model: "passkey",
|
|
368
|
+
data: newPasskey
|
|
369
|
+
});
|
|
370
|
+
return ctx.json(newPasskeyRes, { status: 200 });
|
|
371
|
+
} catch (e) {
|
|
372
|
+
console.log(e);
|
|
373
|
+
throw new APIError("INTERNAL_SERVER_ERROR", { message: PASSKEY_ERROR_CODES.FAILED_TO_VERIFY_REGISTRATION });
|
|
374
|
+
}
|
|
375
|
+
}),
|
|
376
|
+
verifyPasskeyAuthentication: createAuthEndpoint("/passkey/verify-authentication", {
|
|
377
|
+
method: "POST",
|
|
378
|
+
body: z.object({ response: z.record(z.any(), z.any()) }),
|
|
379
|
+
metadata: {
|
|
380
|
+
openapi: {
|
|
381
|
+
description: "Verify authentication of a passkey",
|
|
382
|
+
responses: { 200: {
|
|
383
|
+
description: "Success",
|
|
384
|
+
content: { "application/json": { schema: {
|
|
385
|
+
type: "object",
|
|
386
|
+
properties: {
|
|
387
|
+
session: { $ref: "#/components/schemas/Session" },
|
|
388
|
+
user: { $ref: "#/components/schemas/User" }
|
|
389
|
+
}
|
|
390
|
+
} } }
|
|
391
|
+
} }
|
|
392
|
+
},
|
|
393
|
+
$Infer: { body: {} }
|
|
394
|
+
}
|
|
395
|
+
}, async (ctx) => {
|
|
396
|
+
const origin = options?.origin || ctx.headers?.get("origin") || "";
|
|
397
|
+
if (!origin) throw new APIError("BAD_REQUEST", { message: "origin missing" });
|
|
398
|
+
const resp = ctx.body.response;
|
|
399
|
+
const webAuthnCookie = ctx.context.createAuthCookie(opts.advanced.webAuthnChallengeCookie);
|
|
400
|
+
const challengeId = await ctx.getSignedCookie(webAuthnCookie.name, ctx.context.secret);
|
|
401
|
+
if (!challengeId) throw new APIError("BAD_REQUEST", { message: PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND });
|
|
402
|
+
const data = await ctx.context.internalAdapter.findVerificationValue(challengeId);
|
|
403
|
+
if (!data) throw new APIError("BAD_REQUEST", { message: PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND });
|
|
404
|
+
const { expectedChallenge } = JSON.parse(data.value);
|
|
405
|
+
const passkey$1 = await ctx.context.adapter.findOne({
|
|
406
|
+
model: "passkey",
|
|
407
|
+
where: [{
|
|
408
|
+
field: "credentialID",
|
|
409
|
+
value: resp.id
|
|
410
|
+
}]
|
|
411
|
+
});
|
|
412
|
+
if (!passkey$1) throw new APIError("UNAUTHORIZED", { message: PASSKEY_ERROR_CODES.PASSKEY_NOT_FOUND });
|
|
413
|
+
try {
|
|
414
|
+
const verification = await verifyAuthenticationResponse({
|
|
415
|
+
response: resp,
|
|
416
|
+
expectedChallenge,
|
|
417
|
+
expectedOrigin: origin,
|
|
418
|
+
expectedRPID: getRpID(opts, ctx.context.options.baseURL),
|
|
419
|
+
credential: {
|
|
420
|
+
id: passkey$1.credentialID,
|
|
421
|
+
publicKey: base64.decode(passkey$1.publicKey),
|
|
422
|
+
counter: passkey$1.counter,
|
|
423
|
+
transports: passkey$1.transports?.split(",")
|
|
424
|
+
},
|
|
425
|
+
requireUserVerification: false
|
|
426
|
+
});
|
|
427
|
+
const { verified } = verification;
|
|
428
|
+
if (!verified) throw new APIError("UNAUTHORIZED", { message: PASSKEY_ERROR_CODES.AUTHENTICATION_FAILED });
|
|
429
|
+
await ctx.context.adapter.update({
|
|
430
|
+
model: "passkey",
|
|
431
|
+
where: [{
|
|
432
|
+
field: "id",
|
|
433
|
+
value: passkey$1.id
|
|
434
|
+
}],
|
|
435
|
+
update: { counter: verification.authenticationInfo.newCounter }
|
|
436
|
+
});
|
|
437
|
+
const s = await ctx.context.internalAdapter.createSession(passkey$1.userId);
|
|
438
|
+
if (!s) throw new APIError("INTERNAL_SERVER_ERROR", { message: PASSKEY_ERROR_CODES.UNABLE_TO_CREATE_SESSION });
|
|
439
|
+
const user = await ctx.context.internalAdapter.findUserById(passkey$1.userId);
|
|
440
|
+
if (!user) throw new APIError("INTERNAL_SERVER_ERROR", { message: "User not found" });
|
|
441
|
+
await setSessionCookie(ctx, {
|
|
442
|
+
session: s,
|
|
443
|
+
user
|
|
444
|
+
});
|
|
445
|
+
return ctx.json({ session: s }, { status: 200 });
|
|
446
|
+
} catch (e) {
|
|
447
|
+
ctx.context.logger.error("Failed to verify authentication", e);
|
|
448
|
+
throw new APIError("BAD_REQUEST", { message: PASSKEY_ERROR_CODES.AUTHENTICATION_FAILED });
|
|
449
|
+
}
|
|
450
|
+
}),
|
|
451
|
+
listPasskeys: createAuthEndpoint("/passkey/list-user-passkeys", {
|
|
452
|
+
method: "GET",
|
|
453
|
+
use: [sessionMiddleware],
|
|
454
|
+
metadata: { openapi: {
|
|
455
|
+
description: "List all passkeys for the authenticated user",
|
|
456
|
+
responses: { "200": {
|
|
457
|
+
description: "Passkeys retrieved successfully",
|
|
458
|
+
content: { "application/json": { schema: {
|
|
459
|
+
type: "array",
|
|
460
|
+
items: {
|
|
461
|
+
$ref: "#/components/schemas/Passkey",
|
|
462
|
+
required: [
|
|
463
|
+
"id",
|
|
464
|
+
"userId",
|
|
465
|
+
"publicKey",
|
|
466
|
+
"createdAt",
|
|
467
|
+
"updatedAt"
|
|
468
|
+
]
|
|
469
|
+
},
|
|
470
|
+
description: "Array of passkey objects associated with the user"
|
|
471
|
+
} } }
|
|
472
|
+
} }
|
|
473
|
+
} }
|
|
474
|
+
}, async (ctx) => {
|
|
475
|
+
const passkeys = await ctx.context.adapter.findMany({
|
|
476
|
+
model: "passkey",
|
|
477
|
+
where: [{
|
|
478
|
+
field: "userId",
|
|
479
|
+
value: ctx.context.session.user.id
|
|
480
|
+
}]
|
|
481
|
+
});
|
|
482
|
+
return ctx.json(passkeys, { status: 200 });
|
|
483
|
+
}),
|
|
484
|
+
deletePasskey: createAuthEndpoint("/passkey/delete-passkey", {
|
|
485
|
+
method: "POST",
|
|
486
|
+
body: z.object({ id: z.string().meta({ description: "The ID of the passkey to delete. Eg: \"some-passkey-id\"" }) }),
|
|
487
|
+
use: [sessionMiddleware],
|
|
488
|
+
metadata: { openapi: {
|
|
489
|
+
description: "Delete a specific passkey",
|
|
490
|
+
responses: { "200": {
|
|
491
|
+
description: "Passkey deleted successfully",
|
|
492
|
+
content: { "application/json": { schema: {
|
|
493
|
+
type: "object",
|
|
494
|
+
properties: { status: {
|
|
495
|
+
type: "boolean",
|
|
496
|
+
description: "Indicates whether the deletion was successful"
|
|
497
|
+
} },
|
|
498
|
+
required: ["status"]
|
|
499
|
+
} } }
|
|
500
|
+
} }
|
|
501
|
+
} }
|
|
502
|
+
}, async (ctx) => {
|
|
503
|
+
await ctx.context.adapter.delete({
|
|
504
|
+
model: "passkey",
|
|
505
|
+
where: [{
|
|
506
|
+
field: "id",
|
|
507
|
+
value: ctx.body.id
|
|
508
|
+
}]
|
|
509
|
+
});
|
|
510
|
+
return ctx.json(null, { status: 200 });
|
|
511
|
+
}),
|
|
512
|
+
updatePasskey: createAuthEndpoint("/passkey/update-passkey", {
|
|
513
|
+
method: "POST",
|
|
514
|
+
body: z.object({
|
|
515
|
+
id: z.string().meta({ description: `The ID of the passkey which will be updated. Eg: \"passkey-id\"` }),
|
|
516
|
+
name: z.string().meta({ description: `The new name which the passkey will be updated to. Eg: \"my-new-passkey-name\"` })
|
|
517
|
+
}),
|
|
518
|
+
use: [sessionMiddleware],
|
|
519
|
+
metadata: { openapi: {
|
|
520
|
+
description: "Update a specific passkey's name",
|
|
521
|
+
responses: { "200": {
|
|
522
|
+
description: "Passkey updated successfully",
|
|
523
|
+
content: { "application/json": { schema: {
|
|
524
|
+
type: "object",
|
|
525
|
+
properties: { passkey: { $ref: "#/components/schemas/Passkey" } },
|
|
526
|
+
required: ["passkey"]
|
|
527
|
+
} } }
|
|
528
|
+
} }
|
|
529
|
+
} }
|
|
530
|
+
}, async (ctx) => {
|
|
531
|
+
const passkey$1 = await ctx.context.adapter.findOne({
|
|
532
|
+
model: "passkey",
|
|
533
|
+
where: [{
|
|
534
|
+
field: "id",
|
|
535
|
+
value: ctx.body.id
|
|
536
|
+
}]
|
|
537
|
+
});
|
|
538
|
+
if (!passkey$1) throw new APIError("NOT_FOUND", { message: PASSKEY_ERROR_CODES.PASSKEY_NOT_FOUND });
|
|
539
|
+
if (passkey$1.userId !== ctx.context.session.user.id) throw new APIError("UNAUTHORIZED", { message: PASSKEY_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY });
|
|
540
|
+
const updatedPasskey = await ctx.context.adapter.update({
|
|
541
|
+
model: "passkey",
|
|
542
|
+
where: [{
|
|
543
|
+
field: "id",
|
|
544
|
+
value: ctx.body.id
|
|
545
|
+
}],
|
|
546
|
+
update: { name: ctx.body.name }
|
|
547
|
+
});
|
|
548
|
+
if (!updatedPasskey) throw new APIError("INTERNAL_SERVER_ERROR", { message: PASSKEY_ERROR_CODES.FAILED_TO_UPDATE_PASSKEY });
|
|
549
|
+
return ctx.json({ passkey: updatedPasskey }, { status: 200 });
|
|
550
|
+
})
|
|
551
|
+
},
|
|
552
|
+
schema: mergeSchema(schema, options?.schema),
|
|
553
|
+
$ERROR_CODES: PASSKEY_ERROR_CODES
|
|
554
|
+
};
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
//#endregion
|
|
558
|
+
export { passkey };
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@better-auth/passkey",
|
|
3
|
+
"version": "1.4.0-beta.16",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Passkey plugin for Better Auth",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"module": "dist/index.js",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"better-auth-dev-source": "./src/index.ts",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./client": {
|
|
18
|
+
"better-auth-dev-source": "./src/client.ts",
|
|
19
|
+
"types": "./dist/client.d.ts",
|
|
20
|
+
"default": "./dist/client.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"typesVersions": {
|
|
24
|
+
"*": {
|
|
25
|
+
"*": [
|
|
26
|
+
"./dist/index.d.ts"
|
|
27
|
+
],
|
|
28
|
+
"client": [
|
|
29
|
+
"./dist/client.d.ts"
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"tsdown": "^0.15.11",
|
|
35
|
+
"@better-auth/core": "1.4.0-beta.16",
|
|
36
|
+
"better-auth": "1.4.0-beta.16"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@simplewebauthn/browser": "^13.1.2",
|
|
40
|
+
"@simplewebauthn/server": "^13.1.2",
|
|
41
|
+
"zod": "^4.1.5"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"nanostores": "^1.0.1",
|
|
45
|
+
"@better-auth/utils": "0.3.0",
|
|
46
|
+
"better-call": "1.0.24",
|
|
47
|
+
"@better-fetch/fetch": "1.1.18",
|
|
48
|
+
"@better-auth/core": "1.4.0-beta.16",
|
|
49
|
+
"better-auth": "1.4.0-beta.16"
|
|
50
|
+
},
|
|
51
|
+
"files": [
|
|
52
|
+
"dist"
|
|
53
|
+
],
|
|
54
|
+
"repository": {
|
|
55
|
+
"type": "git",
|
|
56
|
+
"url": "https://github.com/better-auth/better-auth",
|
|
57
|
+
"directory": "packages/passkey"
|
|
58
|
+
},
|
|
59
|
+
"homepage": "https://www.better-auth.com/docs/plugins/passkey",
|
|
60
|
+
"keywords": [
|
|
61
|
+
"auth",
|
|
62
|
+
"passkey",
|
|
63
|
+
"typescript",
|
|
64
|
+
"better-auth"
|
|
65
|
+
],
|
|
66
|
+
"license": "MIT",
|
|
67
|
+
"scripts": {
|
|
68
|
+
"test": "vitest",
|
|
69
|
+
"build": "tsdown",
|
|
70
|
+
"dev": "tsdown --watch",
|
|
71
|
+
"typecheck": "tsc --project tsconfig.json"
|
|
72
|
+
}
|
|
73
|
+
}
|