ultisuite-client/lib/auth/webauthn-utils.ts
R3D347HR4Y ee05c804f9
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
feat(auth): MFA stages and embedded recovery reset flow
Add authenticator-validate device picker, captcha/frame stages, WebAuthn
utils fix, and recovery query parsing for /reset-password links.
2026-06-20 01:21:29 +02:00

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,
},
}
}