Add MFA login flow support for cloud component (#23188)

* 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
This commit is contained in:
Krisjanis Lejejs 2024-12-17 19:55:07 +00:00 committed by GitHub
parent 0ecdae2551
commit 1c076d22a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 139 additions and 33 deletions

View File

@ -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) =>

View File

@ -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")
@ -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;
@ -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();

View File

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

View File

@ -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";
@ -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) {
@ -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(
@ -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();

View File

@ -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?"
},