Skip to content

Commit

Permalink
Add MFA login flow support for cloud component (#23188)
Browse files Browse the repository at this point in the history
* Add MFA login flow support for cloud component

* Update MFA login in voice assistant setup flow

* Sync errors with core

* Add translations to the TOTP dialog
  • Loading branch information
klejejs authored Dec 17, 2024
1 parent 0ecdae2 commit 1c076d2
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 33 deletions.
27 changes: 18 additions & 9 deletions src/data/cloud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,18 +70,27 @@ export interface CloudWebhook {
managed?: boolean;
}

export const cloudLogin = (
hass: HomeAssistant,
email: string,
password: string
) =>
interface CloudLoginBase {
hass: HomeAssistant;
email: string;
}

export interface CloudLoginPassword extends CloudLoginBase {
password: string;
}

export interface CloudLoginMFA extends CloudLoginBase {
code: string;
}

export const cloudLogin = ({
hass,
...rest
}: CloudLoginPassword | CloudLoginMFA) =>
hass.callApi<{ success: boolean; cloud_pipeline?: string }>(
"POST",
"cloud/login",
{
email,
password,
}
rest
);

export const cloudLogout = (hass: HomeAssistant) =>
Expand Down
69 changes: 57 additions & 12 deletions src/dialogs/voice-assistant-setup/cloud/cloud-step-signin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import "../../../components/ha-textfield";
import type { HaTextField } from "../../../components/ha-textfield";
import { cloudLogin } from "../../../data/cloud";
import type { HomeAssistant } from "../../../types";
import { showAlertDialog } from "../../generic/show-dialog-box";
import {
showAlertDialog,
showPromptDialog,
} from "../../generic/show-dialog-box";
import { AssistantSetupStyles } from "../styles";

@customElement("cloud-step-signin")
Expand Down Expand Up @@ -106,12 +109,36 @@ export class CloudStepSignin extends LitElement {

this._requestInProgress = true;

const doLogin = async (username: string) => {
const doLogin = async (username: string, code?: string) => {
try {
await cloudLogin(this.hass, username, password);
await cloudLogin({
hass: this.hass,
email: username,
...(code ? { code } : { password }),
});
} catch (err: any) {
const errCode = err && err.body && err.body.code;

if (errCode === "mfarequired") {
const totpCode = await showPromptDialog(this, {
title: this.hass.localize(
"ui.panel.config.cloud.login.totp_code_prompt_title"
),
inputLabel: this.hass.localize(
"ui.panel.config.cloud.login.totp_code"
),
inputType: "text",
defaultValue: "",
confirmText: this.hass.localize(
"ui.panel.config.cloud.login.submit"
),
});
if (totpCode !== null && totpCode !== "") {
await doLogin(username, totpCode);
return;
}
}

if (errCode === "usernotfound" && username !== username.toLowerCase()) {
await doLogin(username.toLowerCase());
return;
Expand All @@ -130,15 +157,33 @@ export class CloudStepSignin extends LitElement {

this._requestInProgress = false;

if (errCode === "UserNotConfirmed") {
this._error = this.hass.localize(
"ui.panel.config.cloud.login.alert_email_confirm_necessary"
);
} else {
this._error =
err && err.body && err.body.message
? err.body.message
: "Unknown error";
switch (errCode) {
case "UserNotConfirmed":
this._error = this.hass.localize(
"ui.panel.config.cloud.login.alert_email_confirm_necessary"
);
break;
case "mfarequired":
this._error = this.hass.localize(
"ui.panel.config.cloud.login.alert_mfa_code_required"
);
break;
case "mfaexpiredornotstarted":
this._error = this.hass.localize(
"ui.panel.config.cloud.login.alert_mfa_expired_or_not_started"
);
break;
case "invalidtotpcode":
this._error = this.hass.localize(
"ui.panel.config.cloud.login.alert_totp_code_invalid"
);
break;
default:
this._error =
err && err.body && err.body.message
? err.body.message
: "Unknown error";
break;
}

emailField.focus();
Expand Down
6 changes: 5 additions & 1 deletion src/dialogs/voice-assistant-setup/cloud/cloud-step-signup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,11 @@ export class CloudStepSignup extends LitElement {
}

try {
await cloudLogin(this.hass, this._email, this._password);
await cloudLogin({
hass: this.hass,
email: this._email,
password: this._password,
});
fireEvent(this, "cloud-step", { step: "DONE" });
} catch (e: any) {
if (e?.body?.code === "usernotconfirmed") {
Expand Down
64 changes: 53 additions & 11 deletions src/panels/config/cloud/login/cloud-login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { cloudLogin, removeCloudData } from "../../../../data/cloud";
import {
showAlertDialog,
showConfirmationDialog,
showPromptDialog,
} from "../../../../dialogs/generic/show-dialog-box";
import "../../../../layouts/hass-subpage";
import { haStyle } from "../../../../resources/styles";
Expand Down Expand Up @@ -230,9 +231,13 @@ export class CloudLogin extends LitElement {

this._requestInProgress = true;

const doLogin = async (username: string) => {
const doLogin = async (username: string, code?: string) => {
try {
const result = await cloudLogin(this.hass, username, password);
const result = await cloudLogin({
hass: this.hass,
email: username,
...(code ? { code } : { password }),
});
this.email = "";
this._password = "";
if (result.cloud_pipeline) {
Expand All @@ -252,6 +257,25 @@ export class CloudLogin extends LitElement {
fireEvent(this, "ha-refresh-cloud-status");
} catch (err: any) {
const errCode = err && err.body && err.body.code;
if (errCode === "mfarequired") {
const totpCode = await showPromptDialog(this, {
title: this.hass.localize(
"ui.panel.config.cloud.login.totp_code_prompt_title"
),
inputLabel: this.hass.localize(
"ui.panel.config.cloud.login.totp_code"
),
inputType: "text",
defaultValue: "",
confirmText: this.hass.localize(
"ui.panel.config.cloud.login.submit"
),
});
if (totpCode !== null && totpCode !== "") {
await doLogin(username, totpCode);
return;
}
}
if (errCode === "PasswordChangeRequired") {
showAlertDialog(this, {
title: this.hass.localize(
Expand All @@ -269,15 +293,33 @@ export class CloudLogin extends LitElement {
this._password = "";
this._requestInProgress = false;

if (errCode === "UserNotConfirmed") {
this._error = this.hass.localize(
"ui.panel.config.cloud.login.alert_email_confirm_necessary"
);
} else {
this._error =
err && err.body && err.body.message
? err.body.message
: "Unknown error";
switch (errCode) {
case "UserNotConfirmed":
this._error = this.hass.localize(
"ui.panel.config.cloud.login.alert_email_confirm_necessary"
);
break;
case "mfarequired":
this._error = this.hass.localize(
"ui.panel.config.cloud.login.alert_mfa_code_required"
);
break;
case "mfaexpiredornotstarted":
this._error = this.hass.localize(
"ui.panel.config.cloud.login.alert_mfa_expired_or_not_started"
);
break;
case "invalidtotpcode":
this._error = this.hass.localize(
"ui.panel.config.cloud.login.alert_totp_code_invalid"
);
break;
default:
this._error =
err && err.body && err.body.message
? err.body.message
: "Unknown error";
break;
}

emailField.focus();
Expand Down
6 changes: 6 additions & 0 deletions src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -4016,11 +4016,17 @@
"email_error_msg": "Invalid email",
"password": "Password",
"password_error_msg": "Passwords are at least 8 characters",
"totp_code_prompt_title": "Two-factor authentication",
"totp_code": "TOTP code",
"submit": "Submit",
"forgot_password": "Forgot password?",
"start_trial": "Start your free 1 month trial",
"trial_info": "No payment information necessary",
"alert_password_change_required": "You need to change your password before logging in.",
"alert_email_confirm_necessary": "You need to confirm your email before logging in.",
"alert_mfa_code_required": "You need to enter your two-factor authentication code.",
"alert_mfa_expired_or_not_started": "Multi-factor authentication expired, or not started. Please try again.",
"alert_totp_code_invalid": "Invalid two-factor authentication code.",
"cloud_pipeline_title": "Want to use Home Assistant Cloud for your voice assistant?",
"cloud_pipeline_text": "We created a new assistant for you, using the superior text-to-speech and speech-to-text engines from Home Assistant Cloud. Would you like to set this assistant as the preferred assistant?"
},
Expand Down

0 comments on commit 1c076d2

Please sign in to comment.