import Service from '@ember/service'; import { startAuthentication, startRegistration, } from '@simplewebauthn/browser'; export default class AuthService extends Service { async startLogin(username: string) { // Get login options from your server. Here, we also receive the challenge. const response = await fetch('/webauthn/passkey/loginBegin', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: username }), }); // Check if the login options are ok. if (!response.ok) { const msg = await response.json(); throw new Error('Failed to get login options from server: ' + msg); } // Convert the login options to JSON. const options = await response.json(); // This triggers the browser to display the passkey / WebAuthn modal (e.g. Face ID, Touch ID, Windows Hello). // A new assertionResponse is created. This also means that the challenge has been signed. const assertionResponse = await startAuthentication(options.publicKey); // Send assertionResponse back to server for verification. const verificationResponse = await fetch('/webauthn/passkey/loginFinish', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(assertionResponse), }); const msg = await verificationResponse.json(); if (verificationResponse.ok) { return; } else { throw new Error( 'Bad response code: ' + verificationResponse.status + '. Content: ' + msg, ); } } async startRegistration(username: string) { // Get registration options from your server. Here, we also receive the challenge. const response = await fetch('/webauthn/passkey/registerBegin', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: username }), }); // Check if the registration options are ok. if (!response.ok) { const msg = await response.json(); throw new Error( 'User already exists or failed to get registration options from server: ' + msg, ); } // Convert the registration options to JSON. const options = await response.json(); console.log('registration start', options); // This triggers the browser to display the passkey / WebAuthn modal (e.g. Face ID, Touch ID, Windows Hello). // A new attestation is created. This also means a new public-private-key pair is created. const attestationResponse = await startRegistration(options.publicKey); console.log('Attempting to complete registration', attestationResponse); // Send attestationResponse back to server for verification and storage. const verificationResponse = await fetch( '/webauthn/passkey/registerFinish', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(attestationResponse), }, ); const msg = await verificationResponse.json(); if (verificationResponse.ok) { return; } else { throw new Error( 'Bad response code: ' + verificationResponse.status + '. Content: ' + msg, ); } } // Check if the client is currently logged in // Happens via caling an endpoint that's only available if logged in but doesn't return anything other than "ok" async getLoginState(): Promise { try { const response = await fetch('/api/test-auth'); return response.status >= 200 && response.status < 300; } catch (error: any) { return false; } } // Check if a given username exist on the server // Note: The server enforces this check itself during both registration and login // but provides the information too so that frontends can provide a better UX async doesUsernameExist(username: string): Promise { // TODO: Make API call to check if username/handle is already in use return true; } // Check if a given username is allowed to log in. // This includes a check for the existence of the username in the first place // A username may not log in for various reasons, two of which are the account not being approved yet // or the account being barred login from an admin // Note: The server enforces this check itself during login. However, it also provides an API endpoint // for performing this check to allow frontends to have a better UX async canUsernameLogin(username: string): Promise { // Can't login into a non-existing account if (!(await this.doesUsernameExist(username))) return false; // TODO: Make API call to check if username is allowed to login return true; } } // Don't remove this declaration: this is what enables TypeScript to resolve // this service using `Owner.lookup('service:auth')`, as well // as to check when you pass the service name as an argument to the decorator, // like `@service('auth') declare altName: AuthService;`. declare module '@ember/service' { interface Registry { auth: AuthService; } }