Skip to content

Commit

Permalink
Add webauthn auth provider UI components
Browse files Browse the repository at this point in the history
  • Loading branch information
VDigitall committed Jul 28, 2024
1 parent 5d794e7 commit ed77537
Show file tree
Hide file tree
Showing 5 changed files with 633 additions and 1 deletion.
87 changes: 86 additions & 1 deletion src/auth/ha-auth-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ import {
redirectWithAuthCode,
submitLoginFlow,
} from "../data/auth";
import {
PublicKeyCredentialRequestOptions,
PublicKeyCredentialRequestOptionsJSON,
AuthenticationCredentialJSON,
AuthenticationCredential,
} from "../data/webauthn";
import {
DataEntryFlowStep,
DataEntryFlowStepForm,
Expand Down Expand Up @@ -215,7 +221,12 @@ export class HaAuthFlow extends LitElement {
`ui.panel.page-authorize.form.providers.${step.handler[0]}.abort.${step.reason}`
)}
`;
case "form":
case "form": {
if (step.step_id === "challenge") {
const publicKeyOptions =
step.description_placeholders!.webauthn_options;
this._getWebauthnCredentials(publicKeyOptions);

Check failure on line 228 in src/auth/ha-auth-flow.ts

View workflow job for this annotation

GitHub Actions / Lint and check format

Argument of type 'string' is not assignable to parameter of type 'PublicKeyCredentialRequestOptionsJSON'.
}
return html`
<h1>
${!["select_mfa_module", "mfa"].includes(step.step_id)
Expand Down Expand Up @@ -261,11 +272,30 @@ export class HaAuthFlow extends LitElement {
`
: ""}
`;
}
default:
return nothing;
}
}

private _base64url = {
encode: function (buffer) {
const base64 = window.btoa(
String.fromCharCode(...new Uint8Array(buffer))
);
return base64.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
},
decode: function (base64url) {
const base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
const binStr = window.atob(base64);
const bin = new Uint8Array(binStr.length);
for (let i = 0; i < binStr.length; i++) {
bin[i] = binStr.charCodeAt(i);
}
return bin.buffer;
},
};

private _storeTokenChanged(e: CustomEvent<HTMLInputElement>) {
this._storeToken = (e.currentTarget as HTMLInputElement).checked;
}
Expand Down Expand Up @@ -398,6 +428,61 @@ export class HaAuthFlow extends LitElement {
this._submitting = false;
}
}

private async _getWebauthnCredentials(
publicKeyCredentialRequestOptions: PublicKeyCredentialRequestOptionsJSON
) {
const publicKeyOptions: PublicKeyCredentialRequestOptions = {
...publicKeyCredentialRequestOptions,
challenge: this._base64url.decode(
publicKeyCredentialRequestOptions.challenge
),
allowCredentials: publicKeyCredentialRequestOptions.allowCredentials.map(
(cred) => ({
...cred,
id: this._base64url.decode(cred.id),
})
),
};
try {
const result = await navigator.credentials.get({
publicKey: publicKeyOptions,
});
const authenticationCredential = result as AuthenticationCredential;
const authenticationCredentialJSON: AuthenticationCredentialJSON = {
id: authenticationCredential.id,
authenticatorAttachment:
authenticationCredential.authenticatorAttachment,
rawId: this._base64url.encode(authenticationCredential.rawId),
response: {
userHandle: this._base64url.encode(
authenticationCredential.response.userHandle
),
clientDataJSON: this._base64url.encode(
authenticationCredential.response.clientDataJSON
),
authenticatorData: this._base64url.encode(
authenticationCredential.response.authenticatorData
),
signature: this._base64url.encode(
authenticationCredential.response.signature
),
},
type: authenticationCredential.type,
};
this._stepData = {
authentication_credential: authenticationCredentialJSON,
client_id: this.clientId,
};
this._handleSubmit(new Event("submit"));
} catch (err: any) {
// eslint-disable-next-line no-console
this._state = "error";
this._errorMessage = this._unknownError();
} finally {
this._submitting = false;
}
}
}

declare global {
Expand Down
180 changes: 180 additions & 0 deletions src/data/webauthn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
declare global {
interface HASSDomEvents {
"hass-refresh-passkeys": undefined;
}
}

export interface Passkey {
id: string;
credential_id: string;
name: string;
created_at: string;
last_used_at?: string;
}

export interface PublicKeyCredentialCreationOptionsJSON {
rp: PublicKeyCredentialRpEntity;
user: PublicKeyCredentialUserEntityJSON;
challenge: string;
pubKeyCredParams: PublicKeyCredentialParameters[];
timeout?: number;
excludeCredentials: PublicKeyCredentialDescriptorJSON[];
authenticatorSelection?: AuthenticatorSelectionCriteria;
attestation?: AttestationConveyancePreference;
extensions?: AuthenticationExtensionsClientInputs;
}

export interface PublicKeyCredentialUserEntityJSON {
id: string;
name: string;
displayName: string;
}

export interface PublicKeyCredentialDescriptorJSON {
type: PublicKeyCredentialType;
id: string;
transports?: AuthenticatorTransport[];
}

export interface PublicKeyCredentialCreationOptions {
rp: PublicKeyCredentialRpEntity;
user: PublicKeyCredentialUserEntity;
challenge: BufferSource;
pubKeyCredParams: PublicKeyCredentialParameters[];
timeout?: number;
excludeCredentials?: PublicKeyCredentialDescriptor[];
authenticatorSelection?: AuthenticatorSelectionCriteria;
attestation?: AttestationConveyancePreference;
extensions?: AuthenticationExtensionsClientInputs;
}

export interface PublicKeyCredentialRequestOptionsJSON {
id: string;
challenge: string;
timeout?: number;
rpId?: string;
allowCredentials: PublicKeyCredentialDescriptorJSON[];
userVerification?: UserVerificationRequirement;
extensions?: AuthenticationExtensionsClientInputs;
}

export interface PublicKeyCredentialRequestOptions {
id: string;
challenge: BufferSource;
timeout?: number;
rpId?: string;
allowCredentials?: PublicKeyCredentialDescriptor[];
userVerification?: UserVerificationRequirement;
extensions?: AuthenticationExtensionsClientInputs;
}

export interface PublicKeyCredentialRpEntity {
id: string;
name: string;
}

export interface PublicKeyCredentialUserEntity {
id: BufferSource;
name: string;
displayName: string;
}

export interface PublicKeyCredentialParameters {
type: PublicKeyCredentialType;
alg: COSEAlgorithmIdentifier;
}

export type PublicKeyCredentialType = "public-key";

export type COSEAlgorithmIdentifier = -7 | -257 | -65535 | -257 | -65535;

export interface PublicKeyCredentialDescriptor {
type: PublicKeyCredentialType;
id: BufferSource;
transports?: AuthenticatorTransport[];
}

export type AuthenticatorTransport = "usb" | "nfc" | "ble" | "internal";

export type AuthenticatorAttachment = "platform" | "cross-platform";

export type UserVerificationRequirement =
| "required"
| "preferred"
| "discouraged";

export interface AuthenticatorSelectionCriteria {
authenticatorAttachment?: AuthenticatorAttachment;
requireResidentKey?: boolean;
userVerification?: UserVerificationRequirement;
}

export type AttestationConveyancePreference = "none" | "indirect" | "direct";

export interface AuthenticationExtensionsClientInputs {
[key: string]: any;
}

export interface PublicKeyCredentialWithClientExtensionOutputs {
clientExtensionResults: AuthenticationExtensionsClientOutputs;
}

export interface AuthenticationExtensionsClientOutputs {
[key: string]: any;
}

export interface PublicKeyCredentialAttestationResponse {
clientDataJSON: BufferSource;
attestationObject: BufferSource;
}

export interface PublicKeyCredentialAssertionResponse {
clientDataJSON: BufferSource;
authenticatorData: BufferSource;
signature: BufferSource;
userHandle: BufferSource;
}

export interface PublicKeyRegistartionCredentialResponseJSON {
authenticatorAttachment: string;
id: string;
rawId: string;
response: PublicKeyCredentialAttestationResponseJSON;
type: string;
}

export interface PublicKeyCredentialAttestationResponseJSON {
clientDataJSON: string;
attestationObject: string;
}

export interface PublicKeyRegistartionCredentialResponse {
authenticatorAttachment: string;
id: string;
rawId: BufferSource;
response: PublicKeyCredentialAttestationResponse;
type: string;
}

export interface AuthenticationCredentialJSON {
authenticatorAttachment: string;
id: string;
rawId: string;
response: PublicKeyCredentialAssertionResponseJSON;
type: string;
}

export interface PublicKeyCredentialAssertionResponseJSON {
clientDataJSON: string;
authenticatorData: string;
signature: string;
userHandle: string;
}

export interface AuthenticationCredential {
authenticatorAttachment: string;
id: string;
rawId: BufferSource;
response: PublicKeyCredentialAssertionResponse;
type: string;
}
23 changes: 23 additions & 0 deletions src/panels/profile/ha-profile-section-security.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import "./ha-change-password-card";
import "./ha-long-lived-access-tokens-card";
import "./ha-mfa-modules-card";
import "./ha-refresh-tokens-card";
import "./ha-setup-passkey-card";
import { Passkey } from "../../data/webauthn";

@customElement("ha-profile-section-security")
class HaProfileSectionSecurity extends LitElement {
Expand All @@ -18,6 +20,8 @@ class HaProfileSectionSecurity extends LitElement {

@state() private _refreshTokens?: RefreshToken[];

@state() private _passkeys?: Passkey[];

@property({ attribute: false }) public route!: Route;

public connectedCallback() {
Expand All @@ -29,6 +33,9 @@ class HaProfileSectionSecurity extends LitElement {
if (!this._refreshTokens) {
this._refreshRefreshTokens();
}
if (!this._passkeys) {
this._refreshPasskeys();
}
}

protected render(): TemplateResult {
Expand All @@ -53,6 +60,13 @@ class HaProfileSectionSecurity extends LitElement {
></ha-change-password-card>
`
: ""}
<ha-setup-passkey-card
.hass=${this.hass}
.passkeys=${this._passkeys}
@hass-refresh-passkeys=${this._refreshPasskeys}
></ha-setup-passkey-card>
<ha-mfa-modules-card
.hass=${this.hass}
.mfaModules=${this.hass.user!.mfa_modules}
Expand Down Expand Up @@ -83,6 +97,15 @@ class HaProfileSectionSecurity extends LitElement {
});
}

private async _refreshPasskeys() {
if (!this.hass) {
return;
}
this._passkeys = await this.hass.callWS<Passkey[]>({
type: "config/auth_provider/passkey/list",
});
}

static get styles(): CSSResultGroup {
return [
haStyle,
Expand Down
Loading

0 comments on commit ed77537

Please sign in to comment.