File size: 3,391 Bytes
45a105b | 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 95 96 97 | // WebAuthn helpers. Challenges are ALWAYS generated by the backend; the browser only
// converts the base64url fields and runs navigator.credentials. Credential responses /
// authenticator data are never logged.
export function b64urlToBuf(s: string): ArrayBuffer {
const pad = '='.repeat((4 - (s.length % 4)) % 4)
const b64 = (s + pad).replace(/-/g, '+').replace(/_/g, '/')
const bin = atob(b64)
const bytes = new Uint8Array(bin.length)
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i)
return bytes.buffer
}
export function bufToB64url(buf: ArrayBuffer): string {
const bytes = new Uint8Array(buf)
let s = ''
for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i])
return btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
}
export function isWebAuthnSupported(): boolean {
return typeof window !== 'undefined' && !!window.PublicKeyCredential
}
export async function platformAuthenticatorAvailable(): Promise<boolean> {
try {
return (
isWebAuthnSupported() &&
(await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable())
)
} catch {
return false
}
}
// Backend register-options -> browser create() input.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function toCreationOptions(opts: any): CredentialCreationOptions {
const publicKey = { ...opts }
publicKey.challenge = b64urlToBuf(opts.challenge)
publicKey.user = { ...opts.user, id: b64urlToBuf(opts.user.id) }
if (Array.isArray(opts.excludeCredentials)) {
publicKey.excludeCredentials = opts.excludeCredentials.map(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(c: any) => ({ ...c, id: b64urlToBuf(c.id) }),
)
}
return { publicKey }
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function toRequestOptions(opts: any): CredentialRequestOptions {
const publicKey = { ...opts }
publicKey.challenge = b64urlToBuf(opts.challenge)
if (Array.isArray(opts.allowCredentials)) {
publicKey.allowCredentials = opts.allowCredentials.map(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(c: any) => ({ ...c, id: b64urlToBuf(c.id) }),
)
}
return { publicKey }
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function serializeRegistration(cred: PublicKeyCredential): any {
const r = cred.response as AuthenticatorAttestationResponse
return {
id: cred.id,
rawId: bufToB64url(cred.rawId),
type: cred.type,
response: {
attestationObject: bufToB64url(r.attestationObject),
clientDataJSON: bufToB64url(r.clientDataJSON),
},
clientExtensionResults: cred.getClientExtensionResults?.() ?? {},
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function serializeAssertion(cred: PublicKeyCredential): any {
const r = cred.response as AuthenticatorAssertionResponse
return {
id: cred.id,
rawId: bufToB64url(cred.rawId),
type: cred.type,
response: {
authenticatorData: bufToB64url(r.authenticatorData),
clientDataJSON: bufToB64url(r.clientDataJSON),
signature: bufToB64url(r.signature),
userHandle: r.userHandle ? bufToB64url(r.userHandle) : null,
},
}
}
export class PasskeyCancelled extends Error {}
export class PasskeyUnsupported extends Error {}
|