From 42df2c42d1cb4144a7ad99032ac3922dcf27c7eb Mon Sep 17 00:00:00 2001 From: peintnermax Date: Thu, 18 Apr 2024 15:56:20 +0200 Subject: [PATCH] u2f pages, choose 2 factor page --- apps/login/app/(login)/mfa/set/page.tsx | 82 +++++++++-- apps/login/app/(login)/otp/[method]/page.tsx | 12 +- apps/login/app/(login)/u2f/page.tsx | 34 +++++ apps/login/app/(login)/u2f/set/page.tsx | 76 ++++++++++ apps/login/ui/ChooseSecondFactorToSetup.tsx | 141 +++++++++++++++++++ apps/login/ui/PasswordForm.tsx | 33 +++-- 6 files changed, 345 insertions(+), 33 deletions(-) create mode 100644 apps/login/app/(login)/u2f/page.tsx create mode 100644 apps/login/app/(login)/u2f/set/page.tsx create mode 100644 apps/login/ui/ChooseSecondFactorToSetup.tsx diff --git a/apps/login/app/(login)/mfa/set/page.tsx b/apps/login/app/(login)/mfa/set/page.tsx index 8cb7bb79264..fd6ee21afe7 100644 --- a/apps/login/app/(login)/mfa/set/page.tsx +++ b/apps/login/app/(login)/mfa/set/page.tsx @@ -1,35 +1,89 @@ -import { getBrandingSettings, server } from "#/lib/zitadel"; -import { Button, ButtonVariants } from "#/ui/Button"; +import { + getBrandingSettings, + getLoginSettings, + getSession, + server, +} from "#/lib/zitadel"; +import Alert from "#/ui/Alert"; +import ChooseSecondFactorToSetup from "#/ui/ChooseSecondFactorToSetup"; import DynamicTheme from "#/ui/DynamicTheme"; -import { TextInput } from "#/ui/Input"; import UserAvatar from "#/ui/UserAvatar"; -import { useRouter } from "next/navigation"; +import { + getMostRecentCookieWithLoginname, + getSessionCookieById, +} from "#/utils/cookies"; export default async function Page({ searchParams, }: { searchParams: Record; }) { - const { loginName, authRequestId, sessionId, organization, code, submit } = + const { loginName, altPassword, authRequestId, organization, sessionId } = searchParams; + const sessionFactors = sessionId + ? await loadSessionById(sessionId, organization) + : await loadSessionByLoginname(loginName, organization); + + async function loadSessionByLoginname( + loginName?: string, + organization?: string + ) { + const recent = await getMostRecentCookieWithLoginname( + loginName, + organization + ); + return getSession(server, recent.id, recent.token).then((response) => { + if (response?.session) { + return response.session; + } + }); + } + + async function loadSessionById(sessionId: string, organization?: string) { + const recent = await getSessionCookieById(sessionId, organization); + return getSession(server, recent.id, recent.token).then((response) => { + if (response?.session) { + return response.session; + } + }); + } + const branding = await getBrandingSettings(server, organization); + const loginSettings = await getLoginSettings(server, organization); return (
-

Verify 2-Factor

+

Set up 2-Factor

Choose one of the following second factors.

- -
- -
+ {sessionFactors && ( + + )} + + {!sessionFactors &&
} + + {!(loginName || sessionId) && ( + Provide your active session as loginName param + )} + + {loginSettings ? ( + + ) : ( + No second factors available to setup. + )}
); diff --git a/apps/login/app/(login)/otp/[method]/page.tsx b/apps/login/app/(login)/otp/[method]/page.tsx index 157801e07b9..be2f37f90cf 100644 --- a/apps/login/app/(login)/otp/[method]/page.tsx +++ b/apps/login/app/(login)/otp/[method]/page.tsx @@ -30,11 +30,8 @@ export default async function Page({ {method === "email" && (

Enter the code you got via your email.

)} - {method === "u2f" && ( -

Verify your account with your device.

- )} - {method && ["time-based", "sms", "email"].includes(method) ? ( + {method && ( - ) : ( - )} diff --git a/apps/login/app/(login)/u2f/page.tsx b/apps/login/app/(login)/u2f/page.tsx new file mode 100644 index 00000000000..a22fe449632 --- /dev/null +++ b/apps/login/app/(login)/u2f/page.tsx @@ -0,0 +1,34 @@ +import { getBrandingSettings, getLoginSettings, server } from "#/lib/zitadel"; +import DynamicTheme from "#/ui/DynamicTheme"; +import LoginOTP from "#/ui/LoginOTP"; +import VerifyU2F from "#/ui/VerifyU2F"; + +export default async function Page({ + searchParams, + params, +}: { + searchParams: Record; + params: Record; +}) { + const { loginName, authRequestId, sessionId, organization, code, submit } = + searchParams; + + const branding = await getBrandingSettings(server, organization); + + return ( + +
+

Verify 2-Factor

+ +

Verify your account with your device.

+ + +
+
+ ); +} diff --git a/apps/login/app/(login)/u2f/set/page.tsx b/apps/login/app/(login)/u2f/set/page.tsx new file mode 100644 index 00000000000..3b9570b0e50 --- /dev/null +++ b/apps/login/app/(login)/u2f/set/page.tsx @@ -0,0 +1,76 @@ +import { + addOTPEmail, + addOTPSMS, + getBrandingSettings, + getSession, + registerTOTP, + server, +} from "#/lib/zitadel"; +import DynamicTheme from "#/ui/DynamicTheme"; +import TOTPRegister from "#/ui/TOTPRegister"; +import { getMostRecentCookieWithLoginname } from "#/utils/cookies"; + +export default async function Page({ + searchParams, + params, +}: { + searchParams: Record; + params: Record; +}) { + const { loginName, organization } = searchParams; + const { method } = params; + + const branding = await getBrandingSettings(server, organization); + + const totpResponse = await loadSession(loginName, organization).then( + ({ session, token }) => { + if (session && session.factors?.user?.id) { + if (method === "time-based") { + return registerTOTP(session.factors.user.id, token); + } else if (method === "sms") { + return addOTPSMS(session.factors.user.id); + } else if (method === "email") { + return addOTPEmail(session.factors.user.id); + } else { + throw new Error("Invalid method"); + } + } else { + throw new Error("No session found"); + } + } + ); + + async function loadSession(loginName?: string, organization?: string) { + const recent = await getMostRecentCookieWithLoginname( + loginName, + organization + ); + + return getSession(server, recent.id, recent.token).then((response) => { + return { session: response?.session, token: recent.token }; + }); + } + + return ( + +
+

Register Device

+

+ Choose a device to register for 2-Factor Authentication. +

+ +
+ {/* {auth &&
{auth.to}
} */} + {totpResponse && + "uri" in totpResponse && + "secret" in totpResponse && ( + + )} +
+
+
+ ); +} diff --git a/apps/login/ui/ChooseSecondFactorToSetup.tsx b/apps/login/ui/ChooseSecondFactorToSetup.tsx new file mode 100644 index 00000000000..364beac6fa6 --- /dev/null +++ b/apps/login/ui/ChooseSecondFactorToSetup.tsx @@ -0,0 +1,141 @@ +"use client"; + +import { LoginSettings } from "@zitadel/server"; + +type Props = { + loginName?: string; + sessionId?: string; + authRequestId?: string; + organization?: string; + loginSettings: LoginSettings; +}; + +export default function ChooseSecondFactorToSetup({ + loginName, + sessionId, + authRequestId, + organization, + loginSettings, +}: Props) { + return ( +
+ {loginSettings.secondFactors.map((factor, i) => { + return ( + + ); + })} +
+ ); +} diff --git a/apps/login/ui/PasswordForm.tsx b/apps/login/ui/PasswordForm.tsx index 4ebcf0f5c7b..c5fe51c0853 100644 --- a/apps/login/ui/PasswordForm.tsx +++ b/apps/login/ui/PasswordForm.tsx @@ -71,8 +71,8 @@ export default function PasswordForm({ function submitPasswordAndContinue(value: Inputs): Promise { return submitPassword(value).then((resp) => { - // if user has mfa -> /totp - // if mfa is forced -> /mfa/set + // if user has mfa -> /otp/[method] or /u2f + // if mfa is forced and user has no mfa -> /mfa/set // if no passwordless -> /passkey/add if (resp.authFactors?.length == 1) { const params = new URLSearchParams({ @@ -88,17 +88,20 @@ export default function PasswordForm({ } let method; - if ((resp.authFactors as AuthFactor[])[0].otp) { + const factor = (resp.authFactors as AuthFactor[])[0]; + if (factor.otp) { method = "time-based"; - } else if ((resp.authFactors as AuthFactor[])[0].otpSms) { + return router.push(`/otp/${method}?` + params); + } else if (factor.otpSms) { method = "sms"; - } else if ((resp.authFactors as AuthFactor[])[0].otpEmail) { + return router.push(`/otp/${method}?` + params); + } else if (factor.otpEmail) { method = "email"; - } else if ((resp.authFactors as AuthFactor[])[0].u2f) { + return router.push(`/otp/${method}?` + params); + } else if (factor.u2f) { method = "u2f"; + return router.push(`/u2f?` + params); } - - return router.push(`/otp/${method}?` + params); } else if (resp.authFactors?.length >= 1) { const params = new URLSearchParams({ loginName: resp.factors.user.loginName, @@ -113,6 +116,20 @@ export default function PasswordForm({ } return router.push(`/mfa?` + params); + } else if (loginSettings?.forceMfa && !resp.authFactors?.length) { + const params = new URLSearchParams({ + loginName: resp.factors.user.loginName, + }); + + if (authRequestId) { + params.append("authRequest", authRequestId); + } + + if (organization) { + params.append("organization", organization); + } + + return router.push(`/mfa/set?` + params); } else if ( resp.factors && !resp.factors.passwordless && // if session was not verified with a passkey