linstrom/frontend-reactive/app/services/auth.ts

123 lines
4.1 KiB
TypeScript
Raw Normal View History

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<boolean> {
try {
const response = await fetch('/api/test-auth');
return response.status >= 200 && response.status < 300;
} catch (error: any) {
return false;
}
}
}
// 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;
}
}