From 07e7e1eb8762c65ae6b6dfadd5108a94e51992b6 Mon Sep 17 00:00:00 2001 From: Dessalines <dessalines@users.noreply.github.com> Date: Thu, 2 Mar 2023 18:30:38 -0500 Subject: [PATCH] Adding 2FA support. Fixes #938 (#939) * Adding 2FA support. Fixes #938 * Updating totp_2fa names. --- package.json | 2 +- src/shared/components/home/login.tsx | 45 +++++++++++++- src/shared/components/person/settings.tsx | 71 +++++++++++++++++++++++ src/shared/utils.ts | 1 + yarn.lock | 8 +-- 5 files changed, 119 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 839a3d8..26f10e9 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "inferno-server": "^8.0.6", "isomorphic-cookie": "^1.2.4", "jwt-decode": "^3.1.2", - "lemmy-js-client": "0.17.2-rc.1", + "lemmy-js-client": "0.17.2-rc.3", "markdown-it": "^13.0.1", "markdown-it-container": "^3.0.0", "markdown-it-footnote": "^3.0.3", diff --git a/src/shared/components/home/login.tsx b/src/shared/components/home/login.tsx index 7cdde92..dea6484 100644 --- a/src/shared/components/home/login.tsx +++ b/src/shared/components/home/login.tsx @@ -26,8 +26,10 @@ interface State { form: { username_or_email?: string; password?: string; + totp_2fa_token?: string; }; loginLoading: boolean; + showTotp: boolean; siteRes: GetSiteResponse; } @@ -38,6 +40,7 @@ export class Login extends Component<any, State> { state: State = { form: {}, loginLoading: false, + showTotp: false, siteRes: this.isoData.site_res, }; @@ -141,6 +144,28 @@ export class Login extends Component<any, State> { </button> </div> </div> + {this.state.showTotp && ( + <div className="form-group row"> + <label + className="col-sm-6 col-form-label" + htmlFor="login-totp-token" + > + {i18n.t("two_factor_token")} + </label> + <div className="col-sm-6"> + <input + type="number" + inputMode="numeric" + className="form-control" + id="login-totp-token" + pattern="[0-9]*" + autoComplete="one-time-code" + value={this.state.form.totp_2fa_token} + onInput={linkEvent(this, this.handleLoginTotpChange)} + /> + </div> + </div> + )} <div className="form-group row"> <div className="col-sm-10"> <button type="submit" className="btn btn-secondary"> @@ -159,10 +184,12 @@ export class Login extends Component<any, State> { let lForm = i.state.form; let username_or_email = lForm.username_or_email; let password = lForm.password; + let totp_2fa_token = lForm.totp_2fa_token; if (username_or_email && password) { let form: LoginI = { username_or_email, password, + totp_2fa_token, }; WebSocketService.Instance.send(wsClient.login(form)); } @@ -173,6 +200,11 @@ export class Login extends Component<any, State> { i.setState(i.state); } + handleLoginTotpChange(i: Login, event: any) { + i.state.form.totp_2fa_token = event.target.value; + i.setState(i.state); + } + handleLoginPasswordChange(i: Login, event: any) { i.state.form.password = event.target.value; i.setState(i.state); @@ -191,9 +223,16 @@ export class Login extends Component<any, State> { let op = wsUserOp(msg); console.log(msg); if (msg.error) { - toast(i18n.t(msg.error), "danger"); - this.setState({ form: {} }); - return; + // If the error comes back that the token is missing, show the TOTP field + if (msg.error == "missing_totp_token") { + this.setState({ showTotp: true, loginLoading: false }); + toast(i18n.t("enter_two_factor_code")); + return; + } else { + toast(i18n.t(msg.error), "danger"); + this.setState({ form: {}, loginLoading: false }); + return; + } } else { if (op == UserOperation.Login) { let data = wsJsonToRes<LoginResponse>(msg); diff --git a/src/shared/components/person/settings.tsx b/src/shared/components/person/settings.tsx index 734ae71..a349d1c 100644 --- a/src/shared/components/person/settings.tsx +++ b/src/shared/components/person/settings.tsx @@ -86,6 +86,7 @@ interface SettingsState { show_read_posts?: boolean; show_new_post_notifs?: boolean; discussion_languages?: number[]; + generate_totp_2fa?: boolean; }; changePasswordForm: { new_password?: string; @@ -789,6 +790,7 @@ export class Settings extends Component<any, SettingsState> { </label> </div> </div> + {this.totpSection()} <div className="form-group"> <button type="submit" className="btn btn-block btn-secondary mr-4"> {this.state.saveUserSettingsLoading ? ( @@ -853,6 +855,58 @@ export class Settings extends Component<any, SettingsState> { ); } + totpSection() { + let totpUrl = + UserService.Instance.myUserInfo?.local_user_view.local_user.totp_2fa_url; + + return ( + <> + {!totpUrl && ( + <div className="form-group"> + <div className="form-check"> + <input + className="form-check-input" + id="user-generate-totp" + type="checkbox" + checked={this.state.saveUserSettingsForm.generate_totp_2fa} + onChange={linkEvent(this, this.handleGenerateTotp)} + /> + <label className="form-check-label" htmlFor="user-generate-totp"> + {i18n.t("set_up_two_factor")} + </label> + </div> + </div> + )} + + {totpUrl && ( + <> + <div> + <a className="btn btn-secondary mb-2" href={totpUrl}> + {i18n.t("two_factor_link")} + </a> + </div> + <div className="form-group"> + <div className="form-check"> + <input + className="form-check-input" + id="user-remove-totp" + type="checkbox" + checked={ + this.state.saveUserSettingsForm.generate_totp_2fa == false + } + onChange={linkEvent(this, this.handleRemoveTotp)} + /> + <label className="form-check-label" htmlFor="user-remove-totp"> + {i18n.t("remove_two_factor")} + </label> + </div> + </div> + </> + )} + </> + ); + } + setupBlockPersonChoices() { if (isBrowser()) { let selectId: any = document.getElementById("block-person-filter"); @@ -1017,6 +1071,23 @@ export class Settings extends Component<any, SettingsState> { i.setState(i.state); } + handleGenerateTotp(i: Settings, event: any) { + // Coerce false to undefined here, so it won't generate it. + let checked: boolean | undefined = event.target.checked || undefined; + if (checked) { + toast(i18n.t("two_factor_setup_instructions")); + } + i.state.saveUserSettingsForm.generate_totp_2fa = checked; + i.setState(i.state); + } + + handleRemoveTotp(i: Settings, event: any) { + // Coerce true to undefined here, so it won't generate it. + let checked: boolean | undefined = !event.target.checked && undefined; + i.state.saveUserSettingsForm.generate_totp_2fa = checked; + i.setState(i.state); + } + handleSendNotificationsToEmailChange(i: Settings, event: any) { i.state.saveUserSettingsForm.send_notifications_to_email = event.target.checked; diff --git a/src/shared/utils.ts b/src/shared/utils.ts index 09da9db..bfbd538 100644 --- a/src/shared/utils.ts +++ b/src/shared/utils.ts @@ -500,6 +500,7 @@ export function toast(text: string, background = "success") { backgroundColor: backgroundColor, gravity: "bottom", position: "left", + duration: 5000, }).showToast(); } } diff --git a/yarn.lock b/yarn.lock index b9a50db..cf26c24 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5403,10 +5403,10 @@ leac@^0.6.0: resolved "https://registry.yarnpkg.com/leac/-/leac-0.6.0.tgz#dcf136e382e666bd2475f44a1096061b70dc0912" integrity sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg== -lemmy-js-client@0.17.2-rc.1: - version "0.17.2-rc.1" - resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.17.2-rc.1.tgz#fe8d1508311bbf245acc98c2c3e47e2165a95b14" - integrity sha512-YrOXuCofgkqp28krmPTQZAfUWL5zEDA0sRJ0abKcgf/I8YYkYkUkPS9TOORN5Lv3bc8RAAz4+2/zLHqYL/Tnow== +lemmy-js-client@0.17.2-rc.3: + version "0.17.2-rc.3" + resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.17.2-rc.3.tgz#dc2a33e9228aef260b03a6e1f55698a2f975f979" + integrity sha512-FlWEPMrW2Q/FbtihLOHq2YtcRuoX7700LweCnsm6R6dD6SzsnWy9nKJhn24fcjcR2o6tw0oZKgP0ccq9jPDgfQ== dependencies: node-fetch "2.6.6" -- 2.44.1