stages/authenticator_validate: start rewrite to SPA
This commit is contained in:
@ -1,9 +1,48 @@
|
||||
import { customElement, html, LitElement, TemplateResult } from "lit-element";
|
||||
import { customElement, html, property, TemplateResult } from "lit-element";
|
||||
import { WithUserInfoChallenge } from "../../../api/Flows";
|
||||
import { BaseStage, StageHost } from "../base";
|
||||
import "./AuthenticatorValidateStageWebAuthn";
|
||||
|
||||
export enum DeviceClasses {
|
||||
STATIC = "static",
|
||||
TOTP = "totp",
|
||||
WEBAUTHN = "webauthn",
|
||||
}
|
||||
|
||||
export interface AuthenticatorValidateStageChallenge extends WithUserInfoChallenge {
|
||||
users_device_classes: DeviceClasses[];
|
||||
class_challenges: { [key in DeviceClasses]: unknown };
|
||||
}
|
||||
|
||||
export interface AuthenticatorValidateStageChallengeResponse {
|
||||
device_challenges: { [key in DeviceClasses]: unknown} ;
|
||||
}
|
||||
|
||||
@customElement("ak-stage-authenticator-validate")
|
||||
export class AuthenticatorValidateStage extends LitElement {
|
||||
export class AuthenticatorValidateStage extends BaseStage implements StageHost {
|
||||
|
||||
@property({ attribute: false })
|
||||
challenge?: AuthenticatorValidateStageChallenge;
|
||||
|
||||
renderDeviceClass(deviceClass: DeviceClasses): TemplateResult {
|
||||
switch (deviceClass) {
|
||||
case DeviceClasses.STATIC:
|
||||
case DeviceClasses.TOTP:
|
||||
return html``;
|
||||
case DeviceClasses.WEBAUTHN:
|
||||
return html`<ak-stage-authenticator-validate-webauthn .host=${this} .challenge=${this.challenge}></ak-stage-authenticator-validate-webauthn>`;
|
||||
}
|
||||
}
|
||||
|
||||
submit(formData?: FormData): Promise<void> {
|
||||
return this.host?.submit(formData) || Promise.resolve();
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
// User only has a single device class, so we don't show a picker
|
||||
if (this.challenge?.users_device_classes.length === 1) {
|
||||
return this.renderDeviceClass(this.challenge.users_device_classes[0]);
|
||||
}
|
||||
return html`ak-stage-authenticator-validate`;
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,15 @@
|
||||
import { gettext } from "django";
|
||||
import { customElement, html, LitElement, property, TemplateResult } from "lit-element";
|
||||
import { customElement, html, property, TemplateResult } from "lit-element";
|
||||
import { SpinnerSize } from "../../Spinner";
|
||||
import { getCredentialRequestOptionsFromServer, postAssertionToServer, transformAssertionForServer, transformCredentialRequestOptions } from "./utils";
|
||||
import { transformAssertionForServer, transformCredentialRequestOptions } from "../authenticator_webauthn/utils";
|
||||
import { BaseStage } from "../base";
|
||||
import { AuthenticatorValidateStageChallenge, DeviceClasses } from "./AuthenticatorValidateStage";
|
||||
|
||||
@customElement("ak-stage-webauthn-auth")
|
||||
export class WebAuthnAuth extends LitElement {
|
||||
@customElement("ak-stage-authenticator-validate-webauthn")
|
||||
export class AuthenticatorValidateStageWebAuthn extends BaseStage {
|
||||
|
||||
@property({attribute: false})
|
||||
challenge?: AuthenticatorValidateStageChallenge;
|
||||
|
||||
@property({ type: Boolean })
|
||||
authenticateRunning = false;
|
||||
@ -13,18 +18,10 @@ export class WebAuthnAuth extends LitElement {
|
||||
authenticateMessage = "";
|
||||
|
||||
async authenticate(): Promise<void> {
|
||||
// post the login data to the server to retrieve the PublicKeyCredentialRequestOptions
|
||||
let credentialRequestOptionsFromServer;
|
||||
try {
|
||||
credentialRequestOptionsFromServer = await getCredentialRequestOptionsFromServer();
|
||||
} catch (err) {
|
||||
throw new Error(gettext(`Error when getting request options from server: ${err}`));
|
||||
}
|
||||
|
||||
// convert certain members of the PublicKeyCredentialRequestOptions into
|
||||
// byte arrays as expected by the spec.
|
||||
const transformedCredentialRequestOptions = transformCredentialRequestOptions(
|
||||
credentialRequestOptionsFromServer);
|
||||
const credentialRequestOptions = <PublicKeyCredentialRequestOptions>this.challenge?.class_challenges[DeviceClasses.WEBAUTHN];
|
||||
const transformedCredentialRequestOptions = transformCredentialRequestOptions(credentialRequestOptions);
|
||||
|
||||
// request the authenticator to create an assertion signature using the
|
||||
// credential private key
|
||||
@ -42,26 +39,16 @@ export class WebAuthnAuth extends LitElement {
|
||||
|
||||
// we now have an authentication assertion! encode the byte arrays contained
|
||||
// in the assertion data as strings for posting to the server
|
||||
const transformedAssertionForServer = transformAssertionForServer(assertion);
|
||||
const transformedAssertionForServer = transformAssertionForServer(<PublicKeyCredential>assertion);
|
||||
|
||||
// post the assertion to the server for verification.
|
||||
try {
|
||||
await postAssertionToServer(transformedAssertionForServer);
|
||||
const formData = new FormData();
|
||||
formData.set(`response[${DeviceClasses.WEBAUTHN}]`, JSON.stringify(transformedAssertionForServer));
|
||||
await this.host?.submit(formData);
|
||||
} catch (err) {
|
||||
throw new Error(gettext(`Error when validating assertion on server: ${err}`));
|
||||
}
|
||||
|
||||
this.finishStage();
|
||||
}
|
||||
|
||||
finishStage(): void {
|
||||
// Mark this stage as done
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("ak-flow-submit", {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
firstUpdated(): void {
|
@ -13,7 +13,7 @@ export interface WebAuthnAuthenticatorRegisterChallengeResponse {
|
||||
response: Assertion;
|
||||
}
|
||||
|
||||
@customElement("ak-stage-authenticator-webauthn-register")
|
||||
@customElement("ak-stage-authenticator-webauthn")
|
||||
export class WebAuthnAuthenticatorRegisterStage extends BaseStage {
|
||||
|
||||
@property({ attribute: false })
|
||||
@ -58,7 +58,7 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage {
|
||||
// and storing the public key
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.set("response", JSON.stringify(newAssertionForServer))
|
||||
formData.set("response", JSON.stringify(newAssertionForServer));
|
||||
await this.host?.submit(formData);
|
||||
} catch (err) {
|
||||
throw new Error(gettext(`Server validation of credential failed: ${err}`));
|
||||
|
@ -21,20 +21,6 @@ export function hexEncode(buf: Uint8Array): string {
|
||||
.join("");
|
||||
}
|
||||
|
||||
export interface GenericResponse {
|
||||
fail?: string;
|
||||
success?: string;
|
||||
[key: string]: string | number | GenericResponse | undefined;
|
||||
}
|
||||
|
||||
async function fetchJSON(url: string, options: RequestInit): Promise<GenericResponse> {
|
||||
const response = await fetch(url, options);
|
||||
const body = await response.json();
|
||||
if (body.fail)
|
||||
throw body.fail;
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms items in the credentialCreateOptions generated on the server
|
||||
* into byte arrays expected by the navigator.credentials.create() call
|
||||
@ -84,20 +70,6 @@ export function transformNewAssertionForServer(newAssertion: PublicKeyCredential
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get PublicKeyCredentialRequestOptions for this user from the server
|
||||
* formData of the registration form
|
||||
* @param {FormData} formData
|
||||
*/
|
||||
export async function getCredentialRequestOptionsFromServer(): Promise<GenericResponse> {
|
||||
return await fetchJSON(
|
||||
"/-/user/authenticator/webauthn/begin-assertion/",
|
||||
{
|
||||
method: "POST",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function u8arr(input: string): Uint8Array {
|
||||
return Uint8Array.from(atob(input.replace(/_/g, "/").replace(/-/g, "+")), c => c.charCodeAt(0));
|
||||
}
|
||||
@ -150,20 +122,3 @@ export function transformAssertionForServer(newAssertion: PublicKeyCredential):
|
||||
assertionClientExtensions: JSON.stringify(assertionClientExtensions)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Post the assertion to the server for validation and logging the user in.
|
||||
* @param {Object} assertionDataForServer
|
||||
*/
|
||||
export async function postAssertionToServer(assertionDataForServer: Assertion): Promise<GenericResponse> {
|
||||
const formData = new FormData();
|
||||
Object.entries(assertionDataForServer).forEach(([key, value]) => {
|
||||
formData.set(key, value);
|
||||
});
|
||||
|
||||
return await fetchJSON(
|
||||
"/-/user/authenticator/webauthn/verify-assertion/", {
|
||||
method: "POST",
|
||||
body: formData
|
||||
});
|
||||
}
|
||||
|
@ -1,13 +1,17 @@
|
||||
import { LitElement } from "lit-element";
|
||||
import { FlowExecutor } from "../../pages/generic/FlowExecutor";
|
||||
|
||||
export interface StageHost {
|
||||
submit(formData?: FormData): Promise<void>;
|
||||
}
|
||||
|
||||
export class BaseStage extends LitElement {
|
||||
|
||||
host?: FlowExecutor;
|
||||
host?: StageHost;
|
||||
|
||||
submit(e: Event): void {
|
||||
submitForm(e: Event): void {
|
||||
e.preventDefault();
|
||||
const form = new FormData(this.shadowRoot?.querySelector("form") || undefined);
|
||||
this.host?.submit(form);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -24,9 +24,10 @@ import { AuthenticatorStaticChallenge } from "../../elements/stages/authenticato
|
||||
import { WebAuthnAuthenticatorRegisterChallenge } from "../../elements/stages/authenticator_webauthn/WebAuthnAuthenticatorRegisterStage";
|
||||
import { COMMON_STYLES } from "../../common/styles";
|
||||
import { SpinnerSize } from "../../elements/Spinner";
|
||||
import { StageHost } from "../../elements/stages/base";
|
||||
|
||||
@customElement("ak-flow-executor")
|
||||
export class FlowExecutor extends LitElement {
|
||||
export class FlowExecutor extends LitElement implements StageHost {
|
||||
@property()
|
||||
flowSlug = "";
|
||||
|
||||
@ -158,8 +159,8 @@ export class FlowExecutor extends LitElement {
|
||||
return html`<ak-stage-authenticator-totp .host=${this} .challenge=${this.challenge as AuthenticatorTOTPChallenge}></ak-stage-authenticator-totp>`;
|
||||
case "ak-stage-authenticator-static":
|
||||
return html`<ak-stage-authenticator-static .host=${this} .challenge=${this.challenge as AuthenticatorStaticChallenge}></ak-stage-authenticator-static>`;
|
||||
case "ak-stage-authenticator-webauthn-register":
|
||||
return html`<ak-stage-authenticator-webauthn-register .host=${this} .challenge=${this.challenge as WebAuthnAuthenticatorRegisterChallenge}></ak-stage-authenticator-webauthn-register>`;
|
||||
case "ak-stage-authenticator-webauthn":
|
||||
return html`<ak-stage-authenticator-webauthn .host=${this} .challenge=${this.challenge as WebAuthnAuthenticatorRegisterChallenge}></ak-stage-authenticator-webauthn>`;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
Reference in New Issue
Block a user