stages/authenticator_validate: start rewrite to SPA

This commit is contained in:
Jens Langhammer
2021-02-23 13:50:47 +01:00
parent 7f53c97fb2
commit 3894895d32
14 changed files with 236 additions and 236 deletions

View File

@ -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`;
}

View File

@ -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 {

View File

@ -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}`));

View File

@ -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
});
}

View File

@ -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);
}
}

View File

@ -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;
}