webauthn: update login flow to support other 2fa methods

This commit is contained in:
Matthew Penner 2021-07-17 12:48:14 -06:00
parent 42a3e740ba
commit 31c2ef5279
13 changed files with 255 additions and 41 deletions

View file

@ -0,0 +1,9 @@
import http from '@/api/http';
export default (id: number): Promise<void> => {
return new Promise((resolve, reject) => {
http.delete(`/api/client/account/webauthn/${id}`)
.then(() => resolve())
.catch(reject);
});
};

View file

@ -0,0 +1,23 @@
import http from '@/api/http';
export interface Key {
id: number;
name: string;
createdAt: Date;
lastUsedAt: Date;
}
export const rawDataToKey = (data: any): Key => ({
id: data.id,
name: data.name,
createdAt: new Date(data.created_at),
lastUsedAt: new Date(data.last_used_at) || new Date(),
});
export default (): Promise<Key[]> => {
return new Promise((resolve, reject) => {
http.get('/api/client/account/webauthn')
.then(({ data }) => resolve((data.data || []).map((d: any) => rawDataToKey(d.attributes))))
.catch(reject);
});
};

View file

@ -0,0 +1,51 @@
import http from '@/api/http';
import { LoginResponse } from '@/api/auth/login';
import { base64Decode, bufferDecode, bufferEncode, decodeCredentials } from '@/api/account/webauthn/registerKey';
export default (token: string, publicKey: PublicKeyCredentialRequestOptions): Promise<LoginResponse> => {
return new Promise((resolve, reject) => {
console.log(token);
console.log(publicKey);
const publicKeyCredential = Object.assign({}, publicKey);
publicKeyCredential.challenge = bufferDecode(base64Decode(publicKey.challenge.toString()));
if (publicKey.allowCredentials) {
publicKeyCredential.allowCredentials = decodeCredentials(publicKey.allowCredentials);
}
navigator.credentials.get({
publicKey: publicKeyCredential,
}).then((c) => {
if (c === null) {
return;
}
const credential = c as PublicKeyCredential;
const response = credential.response as AuthenticatorAssertionResponse;
const data = {
confirmation_token: token,
data: JSON.stringify({
id: credential.id,
type: credential.type,
rawId: bufferEncode(credential.rawId),
response: {
authenticatorData: bufferEncode(response.authenticatorData),
clientDataJSON: bufferEncode(response.clientDataJSON),
signature: bufferEncode(response.signature),
userHandle: response.userHandle ? bufferEncode(response.userHandle) : null,
},
}),
};
console.log(data);
http.post('/auth/login/checkpoint/key', data).then(response => {
return resolve({
complete: response.data.complete,
intended: response.data.data?.intended || undefined,
});
}).catch(reject);
}).catch(reject);
});
};

View file

@ -0,0 +1,73 @@
import http from '@/api/http';
import { Key, rawDataToKey } from '@/api/account/webauthn/getWebauthn';
export const base64Decode = (input: string): string => {
input = input.replace(/-/g, '+').replace(/_/g, '/');
const pad = input.length % 4;
if (pad) {
if (pad === 1) {
throw new Error('InvalidLengthError: Input base64url string is the wrong length to determine padding');
}
input += new Array(5 - pad).join('=');
}
return input;
};
export const bufferDecode = (value: string): ArrayBuffer => {
return Uint8Array.from(window.atob(value), c => c.charCodeAt(0));
};
export const bufferEncode = (value: ArrayBuffer): string => {
// @ts-ignore
return window.btoa(String.fromCharCode.apply(null, new Uint8Array(value)));
};
export const decodeCredentials = (credentials: PublicKeyCredentialDescriptor[]) => {
return credentials.map(c => {
return {
id: bufferDecode(base64Decode(c.id.toString())),
type: c.type,
transports: c.transports,
};
});
};
export default (name: string): Promise<Key> => {
return new Promise((resolve, reject) => {
http.get('/api/client/account/webauthn/register').then((res) => {
const publicKey = res.data.public_key;
const publicKeyCredential = Object.assign({}, publicKey);
publicKeyCredential.user.id = bufferDecode(publicKey.user.id);
publicKeyCredential.challenge = bufferDecode(base64Decode(publicKey.challenge));
if (publicKey.excludeCredentials) {
publicKeyCredential.excludeCredentials = decodeCredentials(publicKey.excludeCredentials);
}
return navigator.credentials.create({
publicKey: publicKeyCredential,
});
}).then((c) => {
if (c === null) {
return;
}
const credential = c as PublicKeyCredential;
const response = credential.response as AuthenticatorAttestationResponse;
http.post('/api/client/account/webauthn/register', {
name: name,
register: JSON.stringify({
id: credential.id,
type: credential.type,
rawId: bufferEncode(credential.rawId),
response: {
attestationObject: bufferEncode(response.attestationObject),
clientDataJSON: bufferEncode(response.clientDataJSON),
},
}),
}).then(({ data }) => resolve(rawDataToKey(data.attributes))).catch(reject);
}).catch(reject);
});
};

View file

@ -1,9 +1,11 @@
import http from '@/api/http';
export interface LoginResponse {
methods?: string[];
complete: boolean;
intended?: string;
confirmationToken?: string;
publicKey?: any;
}
export interface LoginData {
@ -19,15 +21,18 @@ export default ({ username, password, recaptchaData }: LoginData): Promise<Login
password,
'g-recaptcha-response': recaptchaData,
})
.then(response => {
if (!(response.data instanceof Object)) {
.then(({ data }) => {
if (!(data instanceof Object)) {
return reject(new Error('An error occurred while processing the login request.'));
}
return resolve({
complete: response.data.data.complete,
intended: response.data.data.intended || undefined,
confirmationToken: response.data.data.confirmation_token || undefined,
methods: data.methods,
complete: data.complete,
intended: data.intended || undefined,
confirmationToken: data.confirmation_token || undefined,
// eslint-disable-next-line camelcase
publicKey: data.webauthn?.public_key || undefined,
});
})
.catch(reject);

View file

@ -9,8 +9,8 @@ export default (token: string, code: string, recoveryToken?: string): Promise<Lo
recovery_token: (recoveryToken && recoveryToken.length > 0) ? recoveryToken : undefined,
})
.then(response => resolve({
complete: response.data.data.complete,
intended: response.data.data.intended || undefined,
complete: response.data.complete,
intended: response.data.intended || undefined,
}))
.catch(reject);
});