2024-10-24 14:15:08 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
2024-10-28 15:33:17 +00:00
|
|
|
|
|
|
|
// 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<boolean> {
|
|
|
|
// 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<boolean> {
|
|
|
|
// 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;
|
|
|
|
}
|
2024-10-24 14:15:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
}
|