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; managed?: boolean;
} }
export const cloudLogin = ( interface CloudLoginBase {
hass: HomeAssistant, hass: HomeAssistant;
email: string, email: string;
password: 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 }>( hass.callApi<{ success: boolean; cloud_pipeline?: string }>(
"POST", "POST",
"cloud/login", "cloud/login",
{ rest
email,
password,
}
); );
export const cloudLogout = (hass: HomeAssistant) => export const cloudLogout = (hass: HomeAssistant) =>

View File

@ -11,7 +11,10 @@ import "../../../components/ha-textfield";
import type { HaTextField } from "../../../components/ha-textfield"; import type { HaTextField } from "../../../components/ha-textfield";
import { cloudLogin } from "../../../data/cloud"; import { cloudLogin } from "../../../data/cloud";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import { showAlertDialog } from "../../generic/show-dialog-box"; import {
showAlertDialog,
showPromptDialog,
} from "../../generic/show-dialog-box";
import { AssistantSetupStyles } from "../styles"; import { AssistantSetupStyles } from "../styles";
@customElement("cloud-step-signin") @customElement("cloud-step-signin")
@ -106,12 +109,36 @@ export class CloudStepSignin extends LitElement {
this._requestInProgress = true; this._requestInProgress = true;
const doLogin = async (username: string) => { const doLogin = async (username: string, code?: string) => {
try { try {
await cloudLogin(this.hass, username, password); await cloudLogin({
hass: this.hass,
email: username,
...(code ? { code } : { password }),
});
} catch (err: any) { } catch (err: any) {
const errCode = err && err.body && err.body.code; 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()) { if (errCode === "usernotfound" && username !== username.toLowerCase()) {
await doLogin(username.toLowerCase()); await doLogin(username.toLowerCase());
return; return;
@ -130,15 +157,33 @@ export class CloudStepSignin extends LitElement {
this._requestInProgress = false; this._requestInProgress = false;
if (errCode === "UserNotConfirmed") { switch (errCode) {
case "UserNotConfirmed":
this._error = this.hass.localize( this._error = this.hass.localize(
"ui.panel.config.cloud.login.alert_email_confirm_necessary" "ui.panel.config.cloud.login.alert_email_confirm_necessary"
); );
} else { 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 = this._error =
err && err.body && err.body.message err && err.body && err.body.message
? err.body.message ? err.body.message
: "Unknown error"; : "Unknown error";
break;
} }
emailField.focus(); emailField.focus();

View File

@ -190,7 +190,11 @@ export class CloudStepSignup extends LitElement {
} }
try { 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" }); fireEvent(this, "cloud-step", { step: "DONE" });
} catch (e: any) { } catch (e: any) {
if (e?.body?.code === "usernotconfirmed") { if (e?.body?.code === "usernotconfirmed") {

View File

@ -21,6 +21,7 @@ import { cloudLogin, removeCloudData } from "../../../../data/cloud";
import { import {
showAlertDialog, showAlertDialog,
showConfirmationDialog, showConfirmationDialog,
showPromptDialog,
} from "../../../../dialogs/generic/show-dialog-box"; } from "../../../../dialogs/generic/show-dialog-box";
import "../../../../layouts/hass-subpage"; import "../../../../layouts/hass-subpage";
import { haStyle } from "../../../../resources/styles"; import { haStyle } from "../../../../resources/styles";
@ -230,9 +231,13 @@ export class CloudLogin extends LitElement {
this._requestInProgress = true; this._requestInProgress = true;
const doLogin = async (username: string) => { const doLogin = async (username: string, code?: string) => {
try { try {
const result = await cloudLogin(this.hass, username, password); const result = await cloudLogin({
hass: this.hass,
email: username,
...(code ? { code } : { password }),
});
this.email = ""; this.email = "";
this._password = ""; this._password = "";
if (result.cloud_pipeline) { if (result.cloud_pipeline) {
@ -252,6 +257,25 @@ export class CloudLogin extends LitElement {
fireEvent(this, "ha-refresh-cloud-status"); fireEvent(this, "ha-refresh-cloud-status");
} catch (err: any) { } catch (err: any) {
const errCode = err && err.body && err.body.code; 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") { if (errCode === "PasswordChangeRequired") {
showAlertDialog(this, { showAlertDialog(this, {
title: this.hass.localize( title: this.hass.localize(
@ -269,15 +293,33 @@ export class CloudLogin extends LitElement {
this._password = ""; this._password = "";
this._requestInProgress = false; this._requestInProgress = false;
if (errCode === "UserNotConfirmed") { switch (errCode) {
case "UserNotConfirmed":
this._error = this.hass.localize( this._error = this.hass.localize(
"ui.panel.config.cloud.login.alert_email_confirm_necessary" "ui.panel.config.cloud.login.alert_email_confirm_necessary"
); );
} else { 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 = this._error =
err && err.body && err.body.message err && err.body && err.body.message
? err.body.message ? err.body.message
: "Unknown error"; : "Unknown error";
break;
} }
emailField.focus(); emailField.focus();

View File

@ -4016,11 +4016,17 @@
"email_error_msg": "Invalid email", "email_error_msg": "Invalid email",
"password": "Password", "password": "Password",
"password_error_msg": "Passwords are at least 8 characters", "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?", "forgot_password": "Forgot password?",
"start_trial": "Start your free 1 month trial", "start_trial": "Start your free 1 month trial",
"trial_info": "No payment information necessary", "trial_info": "No payment information necessary",
"alert_password_change_required": "You need to change your password before logging in.", "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_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_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?" "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?"
}, },