jonnybarnes.uk/resources/js/auth.js
Jonny Barnes 03c8f20a8c
feat: Add Passkey support
- Added a button for logging in with Passkeys in `login.blade.php`
- Refactored the `register` method and added the `login` method in `auth.js`
- Made various modifications and additions to the passkey functionality in `PasskeysController.php`
- Added event listener for login-passkey element in `app.js`
- Modified the passkeys table schema and made modifications to `Passkey.php`
- Changed the redirect route in the `login` method of `AuthController.php`
- Made modifications and additions to the routes in `web.php`
- Added `"web-auth/webauthn-lib": "^4.7"` to the list of required packages in `composer.json`
- Changed the redirect URL in `AdminTest.php`
2023-10-27 20:22:40 +01:00

167 lines
5.6 KiB
JavaScript

class Auth {
constructor() {}
async register() {
const createOptions = await this.getCreateOptions();
const publicKeyCredentialCreationOptions = {
challenge: this.base64URLStringToBuffer(createOptions.challenge),
rp: {
id: createOptions.rp.id,
name: createOptions.rp.name,
},
user: {
id: new TextEncoder().encode(window.atob(createOptions.user.id)),
name: createOptions.user.name,
displayName: createOptions.user.displayName,
},
pubKeyCredParams: createOptions.pubKeyCredParams,
excludeCredentials: [],
authenticatorSelection: createOptions.authenticatorSelection,
timeout: 60000,
};
const credential = await navigator.credentials.create({
publicKey: publicKeyCredentialCreationOptions
});
if (!credential) {
throw new Error('Error generating a passkey');
}
const authenticatorAttestationResponse = {
id: credential.id ? credential.id : null,
type: credential.type ? credential.type : null,
rawId: credential.rawId ? this.bufferToBase64URLString(credential.rawId) : null,
response: {
attestationObject: credential.response.attestationObject ? this.bufferToBase64URLString(credential.response.attestationObject) : null,
clientDataJSON: credential.response.clientDataJSON ? this.bufferToBase64URLString(credential.response.clientDataJSON) : null,
}
};
const registerCredential = await window.fetch('/admin/passkeys/register', {
method: 'POST',
body: JSON.stringify(authenticatorAttestationResponse),
cache: 'no-cache',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
},
});
if (!registerCredential.ok) {
throw new Error('Error saving the passkey');
}
window.location.reload();
}
async getCreateOptions() {
const response = await fetch('/admin/passkeys/register', {
method: 'GET',
});
return await response.json();
}
async login() {
const loginData = await this.getLoginData();
const publicKeyCredential = await navigator.credentials.get({
publicKey: {
challenge: this.base64URLStringToBuffer(loginData.challenge),
userVerification: loginData.userVerification,
timeout: 60000,
}
});
if (!publicKeyCredential) {
throw new Error('Authentication failed');
}
const authenticatorAttestationResponse = {
id: publicKeyCredential.id ? publicKeyCredential.id : '',
type: publicKeyCredential.type ? publicKeyCredential.type : '',
rawId: publicKeyCredential.rawId ? this.bufferToBase64URLString(publicKeyCredential.rawId) : '',
response: {
authenticatorData: publicKeyCredential.response.authenticatorData ? this.bufferToBase64URLString(publicKeyCredential.response.authenticatorData) : '',
clientDataJSON: publicKeyCredential.response.clientDataJSON ? this.bufferToBase64URLString(publicKeyCredential.response.clientDataJSON) : '',
signature: publicKeyCredential.response.signature ? this.bufferToBase64URLString(publicKeyCredential.response.signature) : '',
userHandle: publicKeyCredential.response.userHandle ? this.bufferToBase64URLString(publicKeyCredential.response.userHandle) : '',
},
};
const loginAttempt = await window.fetch('/login/passkey', {
method: 'POST',
body: JSON.stringify(authenticatorAttestationResponse),
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
},
});
if (!loginAttempt.ok) {
throw new Error('Login failed');
}
window.location.assign('/admin');
}
async getLoginData() {
const response = await fetch('/login/passkey', {
method: 'GET',
});
return await response.json();
}
/**
* Convert a base64 URL string to a buffer.
*
* Sourced from https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/browser/src/helpers/base64URLStringToBuffer.ts#L8
*
* @param {string} base64URLString
* @returns {ArrayBuffer}
*/
base64URLStringToBuffer(base64URLString) {
// Convert from Base64URL to Base64
const base64 = base64URLString.replace(/-/g, '+').replace(/_/g, '/');
/**
* Pad with '=' until it's a multiple of four
* (4 - (85 % 4 = 1) = 3) % 4 = 3 padding
* (4 - (86 % 4 = 2) = 2) % 4 = 2 padding
* (4 - (87 % 4 = 3) = 1) % 4 = 1 padding
* (4 - (88 % 4 = 0) = 4) % 4 = 0 padding
*/
const padLength = (4 - (base64.length % 4)) % 4;
const padded = base64.padEnd(base64.length + padLength, '=');
// Convert to a binary string
const binary = window.atob(padded);
// Convert binary string to buffer
const buffer = new ArrayBuffer(binary.length);
const bytes = new Uint8Array(buffer);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return buffer;
}
/**
* Convert a buffer to a base64 URL string.
*
* Sourced from https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/browser/src/helpers/bufferToBase64URLString.ts#L7
*
* @param {ArrayBuffer} buffer
* @returns {string}
*/
bufferToBase64URLString(buffer) {
const bytes = new Uint8Array(buffer);
let str = '';
for (const charCode of bytes) {
str += String.fromCharCode(charCode);
}
const base64String = btoa(str);
return base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
}
export { Auth };