/* Copyright (C) 2023-2026 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ /** * Passkey helper utilities for WebAuthn credential handling. * * These helpers convert between ArrayBuffer and Base64URL encodings and * normalise server-provided credential options into browser-compatible types. */ /* eslint-disable @typescript-eslint/no-explicit-any */ /** * Convert a base64url string to an ArrayBuffer. */ type NodeBufferCtor = { from(input: string, encoding: string): { toString(encoding: string): string } } export function base64UrlToArrayBuffer(value?: string | null): ArrayBuffer { if (!value) return new ArrayBuffer(0) const padding = '='.repeat((4 - (value.length % 4)) % 4) const base64 = (value + padding).replace(/-/g, '+').replace(/_/g, '/') const globalRef = globalThis as typeof globalThis & { Buffer?: NodeBufferCtor } const decode = typeof globalRef.atob === 'function' ? globalRef.atob.bind(globalRef) : (input: string) => { if (typeof globalRef.Buffer !== 'undefined') { return globalRef.Buffer.from(input, 'base64').toString('binary') } throw new Error( 'Base64 decoding is not supported in this environment' ) } const binary = decode(base64) const buffer = new ArrayBuffer(binary.length) const bytes = new Uint8Array(buffer) for (let i = 0; i < binary.length; i += 1) { bytes[i] = binary.charCodeAt(i) } return buffer } /** * Convert an ArrayBuffer to a base64url string. */ export function arrayBufferToBase64Url( buffer?: ArrayBuffer | ArrayBufferLike | null ): string { if (!buffer) return '' const globalRef = globalThis as typeof globalThis & { Buffer?: NodeBufferCtor } const bytes = new Uint8Array(buffer) let binary = '' for (let i = 0; i < bytes.byteLength; i += 1) { binary += String.fromCharCode(bytes[i]) } const encode = typeof globalRef.btoa === 'function' ? globalRef.btoa.bind(globalRef) : (input: string) => { if (typeof globalRef.Buffer !== 'undefined') { return globalRef.Buffer.from(input, 'binary').toString('base64') } throw new Error( 'Base64 encoding is not supported in this environment' ) } return encode(binary) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/g, '') } /** * Prepare credential creation options returned by the backend. */ export function prepareCredentialCreationOptions( payload: any ): PublicKeyCredentialCreationOptions { const options = payload?.publicKey ?? payload?.PublicKey ?? payload?.response ?? payload?.Response if (!options) { throw new Error( 'Unable to parse Passkey registration options from response' ) } const publicKey: PublicKeyCredentialCreationOptions & Record = { ...options, challenge: base64UrlToArrayBuffer(options.challenge), user: { ...options.user, id: base64UrlToArrayBuffer(options.user?.id), }, } if (Array.isArray(options.excludeCredentials)) { publicKey.excludeCredentials = options.excludeCredentials.map( (item: any) => ({ ...item, id: base64UrlToArrayBuffer(item.id), }) ) } if ( Array.isArray(options.attestationFormats) && options.attestationFormats.length === 0 ) { delete publicKey.attestationFormats } return publicKey } /** * Prepare credential request options returned by the backend. */ export function prepareCredentialRequestOptions( payload: any ): PublicKeyCredentialRequestOptions { const options = payload?.publicKey ?? payload?.PublicKey ?? payload?.response ?? payload?.Response if (!options) { throw new Error('Unable to parse Passkey login options from response') } const publicKey: PublicKeyCredentialRequestOptions & Record = { ...options, challenge: base64UrlToArrayBuffer(options.challenge), } if (Array.isArray(options.allowCredentials)) { publicKey.allowCredentials = options.allowCredentials.map((item: any) => ({ ...item, id: base64UrlToArrayBuffer(item.id), })) } return publicKey } /** * Build payload for registering a new credential. */ export function buildRegistrationResult( credential: PublicKeyCredential | null ): Record | null { if (!credential) return null const response = credential.response as AuthenticatorAttestationResponse & { getTransports?: () => string[] } const transports = typeof response.getTransports === 'function' ? response.getTransports() : undefined return { id: credential.id, rawId: arrayBufferToBase64Url(credential.rawId), type: credential.type, authenticatorAttachment: credential.authenticatorAttachment, response: { attestationObject: arrayBufferToBase64Url(response.attestationObject), clientDataJSON: arrayBufferToBase64Url(response.clientDataJSON), transports, }, clientExtensionResults: credential.getClientExtensionResults?.() ?? {}, } } /** * Build payload for verifying an existing credential. */ export function buildAssertionResult( credential: PublicKeyCredential | null ): Record | null { if (!credential) return null const response = credential.response as AuthenticatorAssertionResponse return { id: credential.id, rawId: arrayBufferToBase64Url(credential.rawId), type: credential.type, authenticatorAttachment: credential.authenticatorAttachment, response: { authenticatorData: arrayBufferToBase64Url(response.authenticatorData), clientDataJSON: arrayBufferToBase64Url(response.clientDataJSON), signature: arrayBufferToBase64Url(response.signature), userHandle: response.userHandle ? arrayBufferToBase64Url(response.userHandle) : null, }, clientExtensionResults: credential.getClientExtensionResults?.() ?? {}, } } /** * Check if current environment supports Passkey/WebAuthn. */ export async function isPasskeySupported(): Promise { if (typeof window === 'undefined') return false const { PublicKeyCredential } = window if (!PublicKeyCredential) return false if ( typeof PublicKeyCredential.isConditionalMediationAvailable === 'function' ) { try { const available = await PublicKeyCredential.isConditionalMediationAvailable() if (available) return true } catch { // ignore } } if ( typeof PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable === 'function' ) { try { return await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable() } catch { return false } } return true } /** * Execute an async Passkey credential creation flow. */ export async function createCredential( options: PublicKeyCredentialCreationOptions ) { return navigator.credentials.create({ publicKey: options }) } /** * Execute an async Passkey credential request flow. */ export async function getCredential( options: PublicKeyCredentialRequestOptions ) { return navigator.credentials.get({ publicKey: options }) }