// src/webauthn/webauthn.service.ts import { generateRegistrationOptions, verifyRegistrationResponse, generateAuthenticationOptions, verifyAuthenticationResponse, VerifiedRegistrationResponse, VerifiedAuthenticationResponse, RegistrationResponseJSON, } from '@simplewebauthn/server'; import { Injectable, Logger } from '@nestjs/common'; import { isoUint8Array } from '@simplewebauthn/server/helpers'; import { isoBase64URL } from '@simplewebauthn/server/helpers'; import { serverConfig } from 'src/config/config'; @Injectable() export class WebauthnService { private logger: Logger = new Logger(`WebAuthn`) private rpName = serverConfig.rpName private rpID = serverConfig.rpId private origin = serverConfig.origin public devices: any[] = []; // In-memory device store constructor() { this.logger.log(`Web Authn service instantiated...`) } async generateRegistrationOptions(user: { id: string; username: string; devices: { credentialID: Uint8Array; transports?: AuthenticatorTransport[]; }[]; }) { return generateRegistrationOptions({ rpName: this.rpName, rpID: this.rpID, userID: isoUint8Array.fromUTF8String(user.id), userName: user.username, timeout: 60000, attestationType: 'none', excludeCredentials: user.devices.map(dev => ({ id: isoBase64URL.fromBuffer(dev.credentialID), type: 'public-key', transports: dev.transports, })), authenticatorSelection: { userVerification: 'preferred', residentKey: 'preferred', }, }); } async verifyRegistrationResponse( responseBody: RegistrationResponseJSON, expectedChallenge: string, ): Promise { this.logger.log('Verifying registration response...'); this.logger.debug(JSON.stringify(responseBody, null, 2)); return verifyRegistrationResponse({ response: responseBody, expectedChallenge, expectedOrigin: this.origin, expectedRPID: this.rpID, }); } async generateAuthenticationOptions(registeredDevices?: { credentialID: Uint8Array; transports?: AuthenticatorTransport[]; }[]) { const allowCredentials = registeredDevices?.length ? registeredDevices.map(dev => ({ id: isoBase64URL.fromBuffer(dev.credentialID), type: 'public-key', transports: dev.transports, })) : undefined; // 💡 omit if no devices or we want discoverable login return generateAuthenticationOptions({ rpID: this.rpID, timeout: 60000, userVerification: 'preferred', allowCredentials, // ✅ undefined = allow browser to try passkeys }); } async verifyAuthenticationResponse( responseBody: any, expectedChallenge: string, credentialPublicKey: Uint8Array, credentialID: string, // stored as base64url in DB counter: number, ): Promise { return verifyAuthenticationResponse({ response: responseBody, expectedChallenge, expectedOrigin: this.origin, expectedRPID: this.rpID, credential: { id: credentialID, // must be base64url string publicKey: credentialPublicKey, counter, }, }); } storeDeviceInfo(deviceInfo: any): void { this.devices.push(deviceInfo) console.log(this.devices) } findDeviceByCredentialID( devices: { credentialID: Uint8Array; credentialPublicKey: Uint8Array; counter: number; transports?: AuthenticatorTransport[]; }[], credentialID: string, // base64url ) { return devices.find(dev => isoBase64URL.fromBuffer(dev.credentialID) === credentialID ); } }