linstrom/frontend-reactive/app/components/passkey.ts
mstar 4f761c20c0 Lots of frontend stuff
- Passkey support on the way
- Add biome file
- added humans.txt
- tests
2024-10-15 16:18:21 +02:00

119 lines
3.9 KiB
TypeScript

import { action } from '@ember/object';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import '@simplewebauthn/browser';
import {
startAuthentication,
startRegistration,
} from '@simplewebauthn/browser';
export interface PasskeySignature {
// The arguments accepted by the component
Args: {};
// Any blocks yielded by the component
Blocks: {
default: [];
};
// The element to which `...attributes` is applied in the component template
Element: null;
}
export default class Passkey extends Component<PasskeySignature> {
@tracked username: string = '';
@tracked error: string | undefined;
@action async startLogin() {
try {
// 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: this.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) {
this.error = undefined;
} else {
this.error = msg;
}
} catch (error: any) {
this.error = 'Error: ' + error.message;
}
}
@action async startRegistration() {
try {
// 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: this.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) {
this.error = undefined;
} else {
this.error = msg;
}
} catch (error: any) {
this.error = 'Error: ' + error.message;
}
}
}