-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathpasskey-auth.ts
More file actions
94 lines (82 loc) · 2.94 KB
/
passkey-auth.ts
File metadata and controls
94 lines (82 loc) · 2.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
import type { AuthenticationResponseJSON } from '@simplewebauthn/server'
import { verifyAuthenticationResponse } from '@simplewebauthn/server'
import { eq } from 'drizzle-orm'
import { getDb } from '../db/index.js'
import { passkeyCredentials } from '../db/schema/index.js'
const validTransports = ['ble', 'cable', 'hybrid', 'internal', 'nfc', 'smart-card', 'usb'] as const
type AuthenticatorTransportFuture = (typeof validTransports)[number]
function filterTransports(arr: string[] | null): AuthenticatorTransportFuture[] | undefined {
if (!arr?.length) return undefined
const set = new Set(validTransports)
return arr.filter((t): t is AuthenticatorTransportFuture =>
set.has(t as AuthenticatorTransportFuture),
)
}
export type VerifyPasskeyAuthResult =
| { ok: true; userId: string }
| { ok: false; code: string; message: string }
export async function verifyPasskeyAuth({
assertion,
expectedChallenge,
expectedOrigin,
expectedRPID,
}: {
assertion: AuthenticationResponseJSON
expectedChallenge: string
expectedOrigin: string
expectedRPID: string
}): Promise<VerifyPasskeyAuthResult> {
const credentialId = assertion.id
if (!credentialId)
return { ok: false, code: 'MISSING_CREDENTIAL_ID', message: 'Assertion missing credential id' }
const db = await getDb()
const [credential] = await db
.select()
.from(passkeyCredentials)
.where(eq(passkeyCredentials.credentialId, credentialId))
.limit(1)
if (!credential) return { ok: false, code: 'UNKNOWN_CREDENTIAL', message: 'Credential not found' }
const publicKey = Buffer.from(credential.publicKey, 'base64')
const counter = credential.counter
if (!Number.isInteger(counter) || Number.isNaN(counter) || counter < 0)
return {
ok: false,
code: 'INVALID_COUNTER',
message: 'Invalid credential counter',
}
let verification: Awaited<ReturnType<typeof verifyAuthenticationResponse>>
try {
verification = await verifyAuthenticationResponse({
response: assertion,
expectedChallenge,
expectedOrigin,
expectedRPID,
credential: {
id: credential.credentialId,
publicKey: new Uint8Array(publicKey),
counter,
transports: filterTransports(credential.transports) ?? undefined,
},
})
} catch (err) {
return {
ok: false,
code: 'VERIFICATION_FAILED',
message: err instanceof Error ? err.message : 'Verification failed',
}
}
if (!verification.verified || !verification.authenticationInfo)
return { ok: false, code: 'VERIFICATION_FAILED', message: 'Verification failed' }
const [updated] = await db
.update(passkeyCredentials)
.set({ counter: verification.authenticationInfo.newCounter })
.where(eq(passkeyCredentials.id, credential.id))
.returning()
if (!updated)
return {
ok: false,
code: 'COUNTER_UPDATE_FAILED',
message: 'Failed to update credential counter',
}
return { ok: true, userId: credential.userId }
}