From 4244b284c845d75d8dbe03c440b33deffc31cd7c Mon Sep 17 00:00:00 2001 From: Victor Tran Date: Mon, 23 Sep 2024 21:07:06 +1000 Subject: [PATCH] Introduce TokenAcquisitionSession --- Parlance.ClientApp/src/components/NavMenu.tsx | 1 - .../modals/account/LoginErrorModal.tsx | 1 - .../modals/account/LoginOtpModal.tsx | 126 +++---- .../modals/account/LoginPasswordModal.tsx | 32 +- .../account/LoginPasswordResetModal.tsx | 18 +- .../account/LoginSecurityKeyFailureModal.tsx | 15 +- .../modals/account/LoginUsernameModal.tsx | 18 +- .../modals/account/resets/EmailResetModal.tsx | 10 +- .../account/resets/PasswordResetModal.tsx | 13 +- .../src/helpers/TokenAcquisitionSession.tsx | 313 ++++++++++++++++++ .../src/helpers/UserManager.tsx | 245 +------------- Parlance.ClientApp/src/interfaces/users.ts | 2 + 12 files changed, 450 insertions(+), 344 deletions(-) create mode 100644 Parlance.ClientApp/src/helpers/TokenAcquisitionSession.tsx diff --git a/Parlance.ClientApp/src/components/NavMenu.tsx b/Parlance.ClientApp/src/components/NavMenu.tsx index e18310bf..c4b70ffd 100644 --- a/Parlance.ClientApp/src/components/NavMenu.tsx +++ b/Parlance.ClientApp/src/components/NavMenu.tsx @@ -25,7 +25,6 @@ export default function NavMenu() { if (UserManager.isLoggedIn) { Modal.mount(); } else { - UserManager.clearLoginDetails(); Modal.mount(); } }; diff --git a/Parlance.ClientApp/src/components/modals/account/LoginErrorModal.tsx b/Parlance.ClientApp/src/components/modals/account/LoginErrorModal.tsx index 4f94e879..5651646b 100644 --- a/Parlance.ClientApp/src/components/modals/account/LoginErrorModal.tsx +++ b/Parlance.ClientApp/src/components/modals/account/LoginErrorModal.tsx @@ -14,7 +14,6 @@ export function LoginErrorModal() { { text: t("LOG_IN_AGAIN"), onClick: () => { - UserManager.clearLoginDetails(); Modal.mount(); }, }, diff --git a/Parlance.ClientApp/src/components/modals/account/LoginOtpModal.tsx b/Parlance.ClientApp/src/components/modals/account/LoginOtpModal.tsx index d0619469..2f74dd20 100644 --- a/Parlance.ClientApp/src/components/modals/account/LoginOtpModal.tsx +++ b/Parlance.ClientApp/src/components/modals/account/LoginOtpModal.tsx @@ -1,84 +1,60 @@ import Modal from "../../Modal"; -import React, { FormEvent, ReactElement } from "react"; -import UserManager from "../../../helpers/UserManager"; +import React, { useState } from "react"; import { LoginPasswordModal } from "./LoginPasswordModal"; -import { TFunction, withTranslation } from "react-i18next"; +import { useTranslation } from "react-i18next"; import LineEdit from "../../LineEdit"; import { VerticalLayout, VerticalSpacer } from "../../Layouts"; import Styles from "./LoginOtpModal.module.css"; +import { TokenAcquisitionSession } from "@/helpers/TokenAcquisitionSession"; -interface LoginOtpModalProps { - t: TFunction; -} - -interface LoginOtpModalState { - otp: string; -} - -class LoginOtpModalComponent extends React.Component< - LoginOtpModalProps, - LoginOtpModalState -> { - constructor(props: LoginOtpModalProps) { - super(props); +export function LoginOtpModal({ + acquisitionSession, +}: { + acquisitionSession: TokenAcquisitionSession; +}) { + const [otp, setOtp] = useState(""); + const { t } = useTranslation(); - this.state = { - otp: "", - }; - } - - otpTextChanged(e: FormEvent) { - this.setState({ - otp: (e.target as HTMLInputElement).value, - }); - } - - render() { - return ( - Modal.mount(), + return ( + + Modal.mount( + , + ), + }, + { + text: t("NEXT"), + onClick: () => { + acquisitionSession.setLoginDetail("otpToken", otp); + acquisitionSession.attemptLogin(); }, - { - text: this.props.t("NEXT"), - onClick: () => { - UserManager.setLoginDetail( - "otpToken", - this.state.otp, - ); - UserManager.attemptLogin(); - }, - }, - ]} - > -
- - - {this.props.t( - "LOG_IN_TWO_FACTOR_AUTHENTICATION_PROMPT_1", - )} - - - {this.props.t( - "LOG_IN_TWO_FACTOR_AUTHENTICATION_PROMPT_2", - )} - - - - -
-
- ); - } + }, + ]} + > +
+ + + {t("LOG_IN_TWO_FACTOR_AUTHENTICATION_PROMPT_1")} + + + {t("LOG_IN_TWO_FACTOR_AUTHENTICATION_PROMPT_2")} + + + + setOtp((e.target as HTMLInputElement).value) + } + /> + +
+
+ ); } - -export const LoginOtpModal = withTranslation()(LoginOtpModalComponent); diff --git a/Parlance.ClientApp/src/components/modals/account/LoginPasswordModal.tsx b/Parlance.ClientApp/src/components/modals/account/LoginPasswordModal.tsx index f0ab8294..ad78b6a0 100644 --- a/Parlance.ClientApp/src/components/modals/account/LoginPasswordModal.tsx +++ b/Parlance.ClientApp/src/components/modals/account/LoginPasswordModal.tsx @@ -6,18 +6,17 @@ import { useTranslation } from "react-i18next"; import LineEdit from "../../LineEdit"; import ModalList from "../../ModalList"; import { VerticalSpacer } from "@/components/Layouts"; +import { TokenAcquisitionSession } from "@/helpers/TokenAcquisitionSession"; -export function LoginPasswordModal() { - const [password, setPassword] = useState( - UserManager.loginDetail("prePassword"), - ); +export function LoginPasswordModal({ + acquisitionSession, +}: { + acquisitionSession: TokenAcquisitionSession; +}) { + const [password, setPassword] = useState(acquisitionSession.prePassword); const { t } = useTranslation(); - useEffect(() => { - UserManager.setLoginDetail("prePassword"); - }, []); - - const loginTypes = UserManager.loginTypes!.map(type => { + const loginTypes = acquisitionSession.loginTypes.map(type => { switch (type) { case "password": return ( @@ -48,7 +47,8 @@ export function LoginPasswordModal() { {[ { text: t("LOG_IN_USE_SECURITY_KEY_PROMPT"), - onClick: () => UserManager.attemptFido2Login(), + onClick: () => + acquisitionSession.attemptFido2Login(), }, ]} @@ -59,23 +59,23 @@ export function LoginPasswordModal() { return ( Modal.mount(), + onClick: () => acquisitionSession.quit(), }, { text: t("FORGOT_PASSWORD"), - onClick: () => UserManager.triggerPasswordReset(), + onClick: () => acquisitionSession.triggerPasswordReset(), }, { text: t("NEXT"), onClick: () => { - UserManager.setLoginDetail("password", password); - UserManager.setLoginDetail("type", "password"); - UserManager.attemptLogin(); + acquisitionSession.setLoginDetail("password", password); + acquisitionSession.setLoginDetail("type", "password"); + acquisitionSession.attemptLogin(); }, }, ]} diff --git a/Parlance.ClientApp/src/components/modals/account/LoginPasswordResetModal.tsx b/Parlance.ClientApp/src/components/modals/account/LoginPasswordResetModal.tsx index 2daf1780..07eb8cc7 100644 --- a/Parlance.ClientApp/src/components/modals/account/LoginPasswordResetModal.tsx +++ b/Parlance.ClientApp/src/components/modals/account/LoginPasswordResetModal.tsx @@ -1,11 +1,14 @@ import Modal from "../../Modal"; import React, { useState } from "react"; -import LoginUsernameModal from "./LoginUsernameModal"; -import UserManager from "../../../helpers/UserManager"; import { useTranslation } from "react-i18next"; import LineEdit from "../../LineEdit"; +import { TokenAcquisitionSession } from "@/helpers/TokenAcquisitionSession"; -export function LoginPasswordResetModal() { +export function LoginPasswordResetModal({ + acquisitionSession, +}: { + acquisitionSession: TokenAcquisitionSession; +}) { const [password, setPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); const { t } = useTranslation(); @@ -16,7 +19,7 @@ export function LoginPasswordResetModal() { buttons={[ { text: t("CANCEL"), - onClick: () => Modal.mount(), + onClick: () => acquisitionSession.quit(), }, { text: t("OK"), @@ -25,8 +28,11 @@ export function LoginPasswordResetModal() { return; } - UserManager.setLoginDetail("newPassword", password); - UserManager.attemptLogin(); + acquisitionSession.setLoginDetail( + "newPassword", + password, + ); + acquisitionSession.attemptLogin(); }, }, ]} diff --git a/Parlance.ClientApp/src/components/modals/account/LoginSecurityKeyFailureModal.tsx b/Parlance.ClientApp/src/components/modals/account/LoginSecurityKeyFailureModal.tsx index 8ea35722..d1dbb5db 100644 --- a/Parlance.ClientApp/src/components/modals/account/LoginSecurityKeyFailureModal.tsx +++ b/Parlance.ClientApp/src/components/modals/account/LoginSecurityKeyFailureModal.tsx @@ -2,8 +2,13 @@ import { useTranslation } from "react-i18next"; import Modal from "../../Modal"; import UserManager from "@/helpers/UserManager"; import { LoginPasswordModal } from "./LoginPasswordModal"; +import { TokenAcquisitionSession } from "@/helpers/TokenAcquisitionSession"; -export function LoginSecurityKeyFailureModal() { +export function LoginSecurityKeyFailureModal({ + acquisitionSession, +}: { + acquisitionSession: TokenAcquisitionSession; +}) { const { t } = useTranslation(); return ( @@ -12,12 +17,16 @@ export function LoginSecurityKeyFailureModal() { { text: t("SECURITY_KEY_USE_PASSWORD_INSTEAD"), onClick: () => { - Modal.mount(); + Modal.mount( + , + ); }, }, { text: t("SECURITY_KEY_RETRY_LOGIN"), - onClick: () => UserManager.attemptFido2Login(), + onClick: () => acquisitionSession.attemptFido2Login(), }, ]} > diff --git a/Parlance.ClientApp/src/components/modals/account/LoginUsernameModal.tsx b/Parlance.ClientApp/src/components/modals/account/LoginUsernameModal.tsx index 7886a70c..3dd7608b 100644 --- a/Parlance.ClientApp/src/components/modals/account/LoginUsernameModal.tsx +++ b/Parlance.ClientApp/src/components/modals/account/LoginUsernameModal.tsx @@ -11,9 +11,7 @@ import { ServerInformationContext } from "@/context/ServerInformationContext"; import { VerticalSpacer } from "@/components/Layouts"; export default function LoginUsernameModal() { - const [username, setUsername] = useState( - UserManager.loginDetail("username"), - ); + const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const { t } = useTranslation(); const serverInformation = useContext(ServerInformationContext); @@ -31,14 +29,12 @@ export default function LoginUsernameModal() { text: t("NEXT"), onClick: async () => { try { - Modal.mount(); - await UserManager.setUsername(username); - if (password) - await UserManager.setLoginDetail( - "prePassword", - password, - ); - Modal.mount(); + const token = await UserManager.obtainToken( + username, + "login", + password ?? "", + ); + await UserManager.setToken(token); } catch { Modal.mount(); } diff --git a/Parlance.ClientApp/src/components/modals/account/resets/EmailResetModal.tsx b/Parlance.ClientApp/src/components/modals/account/resets/EmailResetModal.tsx index 11b9a68b..7c9a3331 100644 --- a/Parlance.ClientApp/src/components/modals/account/resets/EmailResetModal.tsx +++ b/Parlance.ClientApp/src/components/modals/account/resets/EmailResetModal.tsx @@ -9,13 +9,16 @@ import { PasswordResetMethod, PasswordResetMethodEmail, } from "@/interfaces/users"; +import { TokenAcquisitionSession } from "@/helpers/TokenAcquisitionSession"; export function EmailResetModal({ method, resetMethods, + acquisitionSession, }: { method: PasswordResetMethodEmail; resetMethods: PasswordResetMethod[]; + acquisitionSession: TokenAcquisitionSession; }) { const [email, setEmail] = useState(""); const { t } = useTranslation(); @@ -28,13 +31,16 @@ export function EmailResetModal({ text: t("BACK"), onClick: () => Modal.mount( - , + , ), }, { text: t("RESET_PASSWORD"), onClick: () => - UserManager.performPasswordReset("email", { + acquisitionSession.performPasswordReset("email", { email: email, }), }, diff --git a/Parlance.ClientApp/src/components/modals/account/resets/PasswordResetModal.tsx b/Parlance.ClientApp/src/components/modals/account/resets/PasswordResetModal.tsx index f1045563..aa9f04fb 100644 --- a/Parlance.ClientApp/src/components/modals/account/resets/PasswordResetModal.tsx +++ b/Parlance.ClientApp/src/components/modals/account/resets/PasswordResetModal.tsx @@ -5,11 +5,14 @@ import { EmailResetModal } from "./EmailResetModal"; import { LoginPasswordModal } from "../LoginPasswordModal"; import { useTranslation } from "react-i18next"; import { PasswordResetMethod } from "@/interfaces/users"; +import { TokenAcquisitionSession } from "@/helpers/TokenAcquisitionSession"; export function PasswordResetModal({ resetMethods, + acquisitionSession, }: { resetMethods: PasswordResetMethod[]; + acquisitionSession: TokenAcquisitionSession; }) { let { t } = useTranslation(); @@ -19,7 +22,12 @@ export function PasswordResetModal({ buttons={[ { text: t("CANCEL"), - onClick: () => Modal.mount(), + onClick: () => + Modal.mount( + , + ), }, ]} > @@ -39,6 +47,9 @@ export function PasswordResetModal({ , ), }; diff --git a/Parlance.ClientApp/src/helpers/TokenAcquisitionSession.tsx b/Parlance.ClientApp/src/helpers/TokenAcquisitionSession.tsx new file mode 100644 index 00000000..219e6b2a --- /dev/null +++ b/Parlance.ClientApp/src/helpers/TokenAcquisitionSession.tsx @@ -0,0 +1,313 @@ +import { + LoginType, + PasswordResetChallenge, + PasswordResetMethod, + PasswordResetType, + TokenPurpose, + TokenResponseFido, + TokenResponseFidoOptionsCredentials, + TokenResponseToken, +} from "@/interfaces/users"; +import Fetch from "@/helpers/Fetch"; +import Modal from "@/components/Modal"; +import LoadingModal from "@/components/modals/LoadingModal"; +import { RegisterSecurityKeyAdvertisement } from "@/components/modals/account/securityKeys/RegisterSecurityKeyAdvertisement"; +import { LoginSecurityKeyFailureModal } from "@/components/modals/account/LoginSecurityKeyFailureModal"; +import i18n from "@/helpers/i18n"; +import { LoginOtpModal } from "@/components/modals/account/LoginOtpModal"; +import { LoginPasswordResetModal } from "@/components/modals/account/LoginPasswordResetModal"; +import { LoginPasswordModal } from "@/components/modals/account/LoginPasswordModal"; +import { LoginSecurityKeyModal } from "@/components/modals/account/LoginSecurityKeyModal"; +import { decode, encode } from "@/helpers/Base64"; +import { PasswordResetModal } from "@/components/modals/account/resets/PasswordResetModal"; + +export class TokenAcquisitionSession { + private readonly purpose: TokenPurpose; + private readonly _username: string; + private readonly _prePassword: string; + private _availableLoginTypes: LoginType[] = []; + private readonly _successFunction: (token: string) => void; + private readonly _failureFunction: () => void; + private loginSessionDetails: Record = {}; + + constructor( + username: string, + purpose: TokenPurpose, + prePassword: string, + successFunction: (token: string) => void, + failureFunction: () => void, + ) { + this._username = username; + this._prePassword = prePassword; + this.purpose = purpose; + this._successFunction = successFunction; + this._failureFunction = failureFunction; + } + + get prePassword() { + return this._prePassword; + } + + async loadLoginTypes() { + this._availableLoginTypes = await Fetch.post( + "/api/user/tokentypes", + { + username: this._username, + purpose: this.purpose, + }, + ); + } + + async attemptLogin({ + fido2Details, + }: { fido2Details?: TokenResponseFido } = {}) { + Modal.mount(); + + try { + let response = await Fetch.post( + `/api/user/token`, + { ...this.loginSessionDetails, username: this._username }, + ); + Modal.unmount(); + this._successFunction(response.token); + + if ( + !fido2Details && + window.PublicKeyCredential && + !localStorage.getItem("passkey-advertisement-never-ask") && + this.purpose == "login" + ) { + Modal.mount( + , + ); + return; + } + } catch (e: FetchResponse) { + let json = await e.json(); + + if (this.loginSessionDetails.newPassword) { + this.loginSessionDetails.password = + this.loginSessionDetails.newPassword; + delete this.loginSessionDetails.newPassword; + } + + this.setLoginDetail("keyTokenId", null); + this.setLoginDetail("keyResponse", null); + + if (fido2Details) { + Modal.mount( + , + ); + return; + } + + switch (json.status) { + case "DisabledAccount": + Modal.mount( + this.quit(), + }, + ]} + > + {i18n.t("ACCOUNT_DISABLED_PROMPT")} + , + ); + return; + case "OtpRequired": + Modal.mount(); + return; + case "PasswordResetRequired": + Modal.mount( + , + ); + return; + case "PasswordResetRequestRequired": + Modal.mount( + this.triggerPasswordReset(), + }, + ]} + > + {i18n.t("RESET_PASSWORD_PROMPT")} + , + ); + return; + default: + Modal.mount( + , + ); + } + } + } + + async attemptFido2Login() { + Modal.mount(); + + let details: TokenResponseFido; + try { + details = await Fetch.post("/api/user/token", { + type: "fido", + username: this._username, + }); + } catch { + Modal.mount( + , + ); + return; + } + + //Perform webauthn authentication + // noinspection ExceptionCaughtLocallyJS + try { + let assertion = (await navigator.credentials.get({ + publicKey: { + challenge: decode(details.options.challenge), + allowCredentials: details.options.allowCredentials.map( + (x: TokenResponseFidoOptionsCredentials) => ({ + type: x.type, + id: decode(x.id), + }), + ), + userVerification: details.options.userVerification, + extensions: details.options.extensions, + }, + })) as PublicKeyCredential; + + console.log(assertion); + if (!assertion) throw assertion; + + const response = + assertion.response as AuthenticatorAssertionResponse; + + this.setLoginDetail("type", "fido"); + this.setLoginDetail("keyTokenId", details.id); + this.setLoginDetail("keyResponse", { + authenticatorAttachment: assertion.authenticatorAttachment, + id: assertion.id, + rawId: encode(assertion.rawId), + type: assertion.type, + response: { + authenticatorData: encode(response.authenticatorData), + clientDataJSON: encode(response.clientDataJSON), + signature: encode(response.signature), + userHandle: encode(response.userHandle!), + }, + }); + + await this.attemptLogin({ + fido2Details: details, + }); + } catch (e) { + Modal.mount(); + } + } + + get loginTypes() { + return this._availableLoginTypes; + } + + get username() { + return this._username; + } + + setLoginDetail(key: string, value?: any) { + this.loginSessionDetails[key] = value; + } + + quit() { + this._failureFunction(); + } + async triggerPasswordReset() { + Modal.mount(); + try { + let response = await Fetch.post( + `/api/user/reset/methods`, + { + username: this._username, + }, + ); + Modal.mount( + , + ); + } catch (e) { + Modal.mount( + + Modal.mount( + , + ), + }, + ]} + > + {i18n.t("PASSWORD_RECOVERY_ERROR_PROMPT")} + , + ); + } + } + + async performPasswordReset( + type: PasswordResetType, + challenge: PasswordResetChallenge, + ) { + Modal.mount(); + try { + await Fetch.post("/api/user/reset", { + username: this._username, + type, + challenge, + }); + Modal.mount( + + Modal.mount( + , + ), + }, + ]} + > + {i18n.t("PASSWORD_RECOVERY_SUCCESS_PROMPT")} + , + ); + } catch (e) { + Modal.mount( + this.quit() }, + ]} + > + {i18n.t("PASSWORD_RECOVERY_ERROR_PROMPT_2")} + , + ); + } + } +} diff --git a/Parlance.ClientApp/src/helpers/UserManager.tsx b/Parlance.ClientApp/src/helpers/UserManager.tsx index e1d1b90e..e09a8fff 100644 --- a/Parlance.ClientApp/src/helpers/UserManager.tsx +++ b/Parlance.ClientApp/src/helpers/UserManager.tsx @@ -19,20 +19,19 @@ import { PasswordResetChallenge, PasswordResetMethod, PasswordResetType, + TokenPurpose, TokenResponseFido, TokenResponseFidoOptionsCredentials, TokenResponseToken, User, } from "@/interfaces/users"; +import { TokenAcquisitionSession } from "@/helpers/TokenAcquisitionSession"; class UserManager extends EventEmitter { - #loginSessionDetails: Record; #currentUser: User | null; - #availableLoginTypes?: LoginType[]; constructor() { super(); - this.#loginSessionDetails = {}; this.#currentUser = null; this.updateDetails(); @@ -60,8 +59,21 @@ class UserManager extends EventEmitter { return `https://www.gravatar.com/avatar/${md5}`; } - get loginTypes() { - return this.#availableLoginTypes; + obtainToken(username: string, purpose: TokenPurpose, prePassword: string) { + return new Promise(async (res, rej) => { + const acquisitionSession = new TokenAcquisitionSession( + username, + purpose, + prePassword, + res, + rej, + ); + Modal.mount(); + await acquisitionSession.loadLoginTypes(); + Modal.mount( + , + ); + }); } async updateDetails() { @@ -81,168 +93,6 @@ class UserManager extends EventEmitter { } } - setLoginDetail(key: string, value?: any) { - this.#loginSessionDetails[key] = value; - } - - loginDetail(key: string) { - return this.#loginSessionDetails[key] || ""; - } - - clearLoginDetails() { - this.#loginSessionDetails = {}; - } - - async setUsername(username: string) { - this.#availableLoginTypes = await Fetch.post("/api/user/tokentypes", { - username: username, - }); - this.setLoginDetail("username", username); - } - - async attemptLogin({ fido2Details = null } = {}) { - Modal.mount(); - - try { - let response = await Fetch.post( - `/api/user/token`, - this.#loginSessionDetails, - ); - await this.setToken(response.token); - - if ( - !fido2Details && - window.PublicKeyCredential && - !localStorage.getItem("passkey-advertisement-never-ask") - ) { - Modal.mount( - , - ); - return; - } - - Modal.unmount(); - } catch (e: FetchResponse) { - let json = await e.json(); - - if (this.#loginSessionDetails.newPassword) { - this.#loginSessionDetails.password = - this.#loginSessionDetails.newPassword; - delete this.#loginSessionDetails.newPassword; - } - - this.setLoginDetail("keyTokenId", null); - this.setLoginDetail("keyResponse", null); - - if (fido2Details) { - Modal.mount(); - return; - } - - switch (json.status) { - case "DisabledAccount": - Modal.mount( - - {i18n.t("ACCOUNT_DISABLED_PROMPT")} - , - ); - return; - case "OtpRequired": - Modal.mount(); - return; - case "PasswordResetRequired": - Modal.mount(); - return; - case "PasswordResetRequestRequired": - Modal.mount( - this.triggerPasswordReset(), - }, - ]} - > - {i18n.t("RESET_PASSWORD_PROMPT")} - , - ); - return; - default: - Modal.mount(); - } - } - } - - async triggerPasswordReset() { - Modal.mount(); - try { - let response = await Fetch.post( - `/api/user/reset/methods`, - { - username: this.#loginSessionDetails.username, - }, - ); - Modal.mount(); - } catch (e) { - Modal.mount( - Modal.mount(), - }, - ]} - > - {i18n.t("PASSWORD_RECOVERY_ERROR_PROMPT")} - , - ); - } - } - - async performPasswordReset( - type: PasswordResetType, - challenge: PasswordResetChallenge, - ) { - Modal.mount(); - try { - await Fetch.post("/api/user/reset", { - username: this.#loginSessionDetails.username, - type, - challenge, - }); - Modal.mount( - Modal.mount(), - }, - ]} - > - {i18n.t("PASSWORD_RECOVERY_SUCCESS_PROMPT")} - , - ); - } catch (e) { - Modal.mount( - - {i18n.t("PASSWORD_RECOVERY_ERROR_PROMPT_2")} - , - ); - } - } - async setToken(token: string) { localStorage.setItem("token", token); await this.updateDetails(); @@ -252,67 +102,6 @@ class UserManager extends EventEmitter { localStorage.removeItem("token"); await this.updateDetails(); } - - async attemptFido2Login() { - Modal.mount(); - - let details: any; - try { - details = await Fetch.post("/api/user/token", { - type: "fido", - username: this.loginDetail("username"), - }); - } catch { - Modal.mount(); - return; - } - - //Perform webauthn authentication - // noinspection ExceptionCaughtLocallyJS - try { - let assertion = (await navigator.credentials.get({ - publicKey: { - challenge: decode(details.options.challenge), - allowCredentials: details.options.allowCredentials.map( - (x: TokenResponseFidoOptionsCredentials) => ({ - type: x.type, - id: decode(x.id), - }), - ), - userVerification: details.options.userVerification, - extensions: details.options.extensions, - }, - })) as PublicKeyCredential; - - console.log(assertion); - if (!assertion) throw assertion; - - const response = - assertion.response as AuthenticatorAssertionResponse; - - this.setLoginDetail("type", "fido"); - this.setLoginDetail("keyTokenId", details.id); - this.setLoginDetail("keyResponse", { - authenticatorAttachment: assertion.authenticatorAttachment, - id: assertion.id, - rawId: encode(assertion.rawId), - type: assertion.type, - response: { - authenticatorData: encode(response.authenticatorData), - clientDataJSON: encode(response.clientDataJSON), - signature: encode(response.signature), - userHandle: encode(response.userHandle!), - }, - }); - - await this.attemptLogin({ - fido2Details: details, - }); - } catch (e) { - console.log(e); - Modal.mount(); - } - } } let mgr = new UserManager(); diff --git a/Parlance.ClientApp/src/interfaces/users.ts b/Parlance.ClientApp/src/interfaces/users.ts index 2dee67da..d1689dcb 100644 --- a/Parlance.ClientApp/src/interfaces/users.ts +++ b/Parlance.ClientApp/src/interfaces/users.ts @@ -77,3 +77,5 @@ export interface NotificationConsent { consentProvided: boolean; preferredUserName: string; } + +export type TokenPurpose = "login" | "accountModification";