Some checks are pending
E2E / Playwright e2e (push) Waiting to run
Add authenticator-validate device picker, captcha/frame stages, WebAuthn utils fix, and recovery query parsing for /reset-password links.
87 lines
2.8 KiB
TypeScript
87 lines
2.8 KiB
TypeScript
export function bufferFromBase64URL(value: string): ArrayBuffer {
|
|
const padded = value.replace(/-/g, "+").replace(/_/g, "/")
|
|
const pad = padded.length % 4 === 0 ? "" : "=".repeat(4 - (padded.length % 4))
|
|
const binary = atob(padded + pad)
|
|
const bytes = new Uint8Array(binary.length)
|
|
for (let i = 0; i < binary.length; i += 1) {
|
|
bytes[i] = binary.charCodeAt(i)
|
|
}
|
|
return bytes.buffer
|
|
}
|
|
|
|
export function bufferToBase64URL(buffer: ArrayBuffer): string {
|
|
const bytes = new Uint8Array(buffer)
|
|
let binary = ""
|
|
for (let i = 0; i < bytes.length; i += 1) {
|
|
binary += String.fromCharCode(bytes[i]!)
|
|
}
|
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")
|
|
}
|
|
|
|
export function normalizeRequestOptions(
|
|
options: PublicKeyCredentialRequestOptions
|
|
): PublicKeyCredentialRequestOptions {
|
|
const challenge = options.challenge
|
|
if (typeof challenge === "string") {
|
|
return { ...options, challenge: bufferFromBase64URL(challenge) }
|
|
}
|
|
return options
|
|
}
|
|
|
|
function isPublicKeyRequestOptions(value: unknown): value is PublicKeyCredentialRequestOptions {
|
|
if (!value || typeof value !== "object") return false
|
|
const record = value as Record<string, unknown>
|
|
return (
|
|
typeof record.challenge === "string" ||
|
|
typeof record.rp === "object" ||
|
|
Array.isArray(record.allowCredentials)
|
|
)
|
|
}
|
|
|
|
export function readWebAuthnRequestOptions(
|
|
source: Record<string, unknown> | null | undefined
|
|
): PublicKeyCredentialRequestOptions | null {
|
|
if (!source) return null
|
|
|
|
const wrapped = source.request_options ?? source.requestOptions
|
|
if (wrapped && typeof wrapped === "object") {
|
|
const nested = wrapped as { publicKey?: PublicKeyCredentialRequestOptions }
|
|
if (nested.publicKey) return nested.publicKey
|
|
if (isPublicKeyRequestOptions(wrapped)) return wrapped
|
|
}
|
|
|
|
if (isPublicKeyRequestOptions(source)) {
|
|
return source
|
|
}
|
|
|
|
const deviceChallenge = source.challenge
|
|
if (deviceChallenge && typeof deviceChallenge === "object") {
|
|
const nested = deviceChallenge as { publicKey?: PublicKeyCredentialRequestOptions }
|
|
if (nested.publicKey) return nested.publicKey
|
|
if (isPublicKeyRequestOptions(deviceChallenge)) {
|
|
return deviceChallenge as PublicKeyCredentialRequestOptions
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
export function serializeWebAuthnCredential(
|
|
credential: PublicKeyCredential
|
|
): Record<string, unknown> {
|
|
const response = credential.response as AuthenticatorAssertionResponse
|
|
return {
|
|
id: credential.id,
|
|
rawId: bufferToBase64URL(credential.rawId),
|
|
type: credential.type,
|
|
response: {
|
|
clientDataJSON: bufferToBase64URL(response.clientDataJSON),
|
|
authenticatorData: bufferToBase64URL(response.authenticatorData),
|
|
signature: bufferToBase64URL(response.signature),
|
|
userHandle: response.userHandle
|
|
? bufferToBase64URL(response.userHandle)
|
|
: null,
|
|
},
|
|
}
|
|
}
|