From 424bdf42f2bca3208f1d9091afefe257333cd7ed Mon Sep 17 00:00:00 2001 From: peintnermax Date: Tue, 17 Sep 2024 10:12:03 +0200 Subject: [PATCH] fix passkey retry, cleanup mfa set --- apps/login/readme.md | 14 +- apps/login/src/app/(login)/mfa/set/page.tsx | 66 ++++---- .../src/ui/ChooseSecondFactorToSetup.tsx | 52 ++++--- apps/login/src/ui/LoginPasskey.tsx | 141 ++++++++++-------- 4 files changed, 151 insertions(+), 122 deletions(-) diff --git a/apps/login/readme.md b/apps/login/readme.md index fac7969afae..d59b172d11a 100644 --- a/apps/login/readme.md +++ b/apps/login/readme.md @@ -158,13 +158,19 @@ After updating the session, the user is signed in. /mfa/set -This page requests a webAuthN challenge for the user and updates the session afterwards. +This page loads login Settings and the authentication methods for a user and shows setup options. Requests to the APIs made: - `getBrandingSettings(org?)` +- `getLoginSettings(user.org)` - `getSession()` -- `updateSession()` +- `listAuthenticationMethodTypes()` +- `getUserByID()` -When updating the session for the webAuthN challenge, we set `userVerificationRequirement` to `UserVerificationRequirement.REQUIRED` as this will request the webAuthN method as primary method to login. -After updating the session, the user is signed in. +If a user has already setup a certain method, a checkbox is shown alongside the button and the button is disabled. +OTP Email and OTP SMS only show up if the user has verified email or phone. +If the user chooses a method he is redirected to one of `/otp/time-based/set`, `/u2f/set`, `/otp/email/set`, or `/otp/sms/set`. +At the moment, U2F methods are hidden if a method is already added on the users resource. Reasoning is that the page should only be invoked for prompts. A self service page which shows up multiple u2f factors is implemented at a later stage. + +> NOTE: The session and therefore the user factor defines which login settings are checked for available options. diff --git a/apps/login/src/app/(login)/mfa/set/page.tsx b/apps/login/src/app/(login)/mfa/set/page.tsx index e3389185503..8c2dfcc09fe 100644 --- a/apps/login/src/app/(login)/mfa/set/page.tsx +++ b/apps/login/src/app/(login)/mfa/set/page.tsx @@ -12,6 +12,7 @@ import BackButton from "@/ui/BackButton"; import ChooseSecondFactorToSetup from "@/ui/ChooseSecondFactorToSetup"; import DynamicTheme from "@/ui/DynamicTheme"; import UserAvatar from "@/ui/UserAvatar"; +import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; export default async function Page({ searchParams, @@ -31,6 +32,28 @@ export default async function Page({ ? await loadSessionById(sessionId, organization) : await loadSessionByLoginname(loginName, organization); + async function getAuthMethodsAndUser(session?: Session) { + const userId = session?.factors?.user?.id; + + if (!userId) { + throw Error("Could not get user id from session"); + } + + return listAuthenticationMethodTypes(userId).then((methods) => { + return getUserByID(userId).then((user) => { + const humanUser = + user.user?.type.case === "human" ? user.user?.type.value : undefined; + + return { + factors: session?.factors, + authMethods: methods.authMethodTypes ?? [], + phoneVerified: humanUser?.phone?.isVerified ?? false, + emailVerified: humanUser?.email?.isVerified ?? false, + }; + }); + }); + } + async function loadSessionByLoginname( loginName?: string, organization?: string, @@ -39,24 +62,7 @@ export default async function Page({ loginName, organization, }).then((session) => { - if (session && session.factors?.user?.id) { - const userId = session.factors.user.id; - return listAuthenticationMethodTypes(userId).then((methods) => { - return getUserByID(userId).then((user) => { - const humanUser = - user.user?.type.case === "human" - ? user.user?.type.value - : undefined; - - return { - factors: session?.factors, - authMethods: methods.authMethodTypes ?? [], - phoneVerified: humanUser?.phone?.isVerified ?? false, - emailVerified: humanUser?.email?.isVerified ?? false, - }; - }); - }); - } + return getAuthMethodsAndUser(session); }); } @@ -65,29 +71,15 @@ export default async function Page({ return getSession({ sessionId: recent.id, sessionToken: recent.token, - }).then((response) => { - if (response?.session && response.session.factors?.user?.id) { - const userId = response.session.factors.user.id; - return listAuthenticationMethodTypes(userId).then((methods) => { - return getUserByID(userId).then((user) => { - const humanUser = - user.user?.type.case === "human" - ? user.user?.type.value - : undefined; - return { - factors: response.session?.factors, - authMethods: methods.authMethodTypes ?? [], - phoneVerified: humanUser?.phone?.isVerified ?? false, - emailVerified: humanUser?.email?.isVerified ?? false, - }; - }); - }); - } + }).then((sessionResponse) => { + return getAuthMethodsAndUser(sessionResponse.session); }); } const branding = await getBrandingSettings(organization); - const loginSettings = await getLoginSettings(organization); + const loginSettings = await getLoginSettings( + sessionWithData.factors?.user?.organizationId, + ); return ( diff --git a/apps/login/src/ui/ChooseSecondFactorToSetup.tsx b/apps/login/src/ui/ChooseSecondFactorToSetup.tsx index 94315a40e68..eb1b47806ee 100644 --- a/apps/login/src/ui/ChooseSecondFactorToSetup.tsx +++ b/apps/login/src/ui/ChooseSecondFactorToSetup.tsx @@ -1,6 +1,9 @@ "use client"; -import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { + LoginSettings, + SecondFactorType, +} from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { EMAIL, SMS, TOTP, U2F } from "./AuthMethods"; @@ -47,28 +50,37 @@ export default function ChooseSecondFactorToSetup({ return (
- {loginSettings.secondFactors.map((factor, i) => { - return factor === 1 - ? TOTP( + {loginSettings.secondFactors.map((factor) => { + switch (factor) { + case SecondFactorType.OTP: + return TOTP( userMethods.includes(AuthenticationMethodType.TOTP), "/otp/time-based/set?" + params, - ) - : factor === 2 - ? U2F( - userMethods.includes(AuthenticationMethodType.U2F), - "/u2f/set?" + params, + ); + case SecondFactorType.U2F: + return U2F( + userMethods.includes(AuthenticationMethodType.U2F), + "/u2f/set?" + params, + ); + case SecondFactorType.OTP_EMAIL: + return ( + emailVerified && + EMAIL( + userMethods.includes(AuthenticationMethodType.OTP_EMAIL), + "/otp/email/set?" + params, ) - : factor === 3 && emailVerified - ? EMAIL( - userMethods.includes(AuthenticationMethodType.OTP_EMAIL), - "/otp/email/set?" + params, - ) - : factor === 4 && phoneVerified - ? SMS( - userMethods.includes(AuthenticationMethodType.OTP_SMS), - "/otp/sms/set?" + params, - ) - : null; + ); + case SecondFactorType.OTP_SMS: + return ( + phoneVerified && + SMS( + userMethods.includes(AuthenticationMethodType.OTP_SMS), + "/otp/sms/set?" + params, + ) + ); + default: + return null; + } })}
); diff --git a/apps/login/src/ui/LoginPasskey.tsx b/apps/login/src/ui/LoginPasskey.tsx index 448d0a8220a..00dc283ae43 100644 --- a/apps/login/src/ui/LoginPasskey.tsx +++ b/apps/login/src/ui/LoginPasskey.tsx @@ -50,19 +50,20 @@ export default function LoginPasskey({ const pK = response?.challenges?.webAuthN?.publicKeyCredentialRequestOptions ?.publicKey; - if (pK) { - submitLoginAndContinue(pK) - .then(() => { - setLoading(false); - }) - .catch((error) => { - setError(error); - setLoading(false); - }); - } else { + + if (!pK) { setError("Could not request passkey challenge"); setLoading(false); } + + return submitLoginAndContinue(pK) + .then(() => { + setLoading(false); + }) + .catch((error) => { + setError(error); + setLoading(false); + }); }) .catch((error) => { setError(error); @@ -135,59 +136,57 @@ export default function LoginPasskey({ publicKey, }) .then((assertedCredential: any) => { - if (assertedCredential) { - const authData = new Uint8Array( - assertedCredential.response.authenticatorData, - ); - const clientDataJSON = new Uint8Array( - assertedCredential.response.clientDataJSON, - ); - const rawId = new Uint8Array(assertedCredential.rawId); - const sig = new Uint8Array(assertedCredential.response.signature); - const userHandle = new Uint8Array( - assertedCredential.response.userHandle, - ); - const data = { - id: assertedCredential.id, - rawId: coerceToBase64Url(rawId, "rawId"), - type: assertedCredential.type, - response: { - authenticatorData: coerceToBase64Url(authData, "authData"), - clientDataJSON: coerceToBase64Url( - clientDataJSON, - "clientDataJSON", - ), - signature: coerceToBase64Url(sig, "sig"), - userHandle: coerceToBase64Url(userHandle, "userHandle"), - }, - }; - return submitLogin(data).then((resp) => { - if (authRequestId && resp && resp.sessionId) { - return router.push( - `/login?` + - new URLSearchParams({ - sessionId: resp.sessionId, - authRequest: authRequestId, - }), - ); - } else { - const params = new URLSearchParams({}); - - if (authRequestId) { - params.set("authRequestId", authRequestId); - } - if (resp?.factors?.user?.loginName) { - params.set("loginName", resp.factors.user.loginName); - } - - return router.push(`/signedin?` + params); - } - }); - } else { + if (!assertedCredential) { setLoading(false); setError("An error on retrieving passkey"); - return null; + return; } + + const authData = new Uint8Array( + assertedCredential.response.authenticatorData, + ); + const clientDataJSON = new Uint8Array( + assertedCredential.response.clientDataJSON, + ); + const rawId = new Uint8Array(assertedCredential.rawId); + const sig = new Uint8Array(assertedCredential.response.signature); + const userHandle = new Uint8Array( + assertedCredential.response.userHandle, + ); + const data = { + id: assertedCredential.id, + rawId: coerceToBase64Url(rawId, "rawId"), + type: assertedCredential.type, + response: { + authenticatorData: coerceToBase64Url(authData, "authData"), + clientDataJSON: coerceToBase64Url(clientDataJSON, "clientDataJSON"), + signature: coerceToBase64Url(sig, "sig"), + userHandle: coerceToBase64Url(userHandle, "userHandle"), + }, + }; + + return submitLogin(data).then((resp) => { + if (authRequestId && resp && resp.sessionId) { + return router.push( + `/login?` + + new URLSearchParams({ + sessionId: resp.sessionId, + authRequest: authRequestId, + }), + ); + } else { + const params = new URLSearchParams({}); + + if (authRequestId) { + params.set("authRequestId", authRequestId); + } + if (resp?.factors?.user?.loginName) { + params.set("loginName", resp.factors.user.loginName); + } + + return router.push(`/signedin?` + params); + } + }); }) .catch((error) => { console.error(error); @@ -245,7 +244,27 @@ export default function LoginPasskey({ className="self-end" variant={ButtonVariants.Primary} disabled={loading} - onClick={() => updateSessionForChallenge()} + onClick={async () => { + const response = await updateSessionForChallenge(); + + const pK = + response?.challenges?.webAuthN?.publicKeyCredentialRequestOptions + ?.publicKey; + + if (!pK) { + setError("Could not request passkey challenge"); + setLoading(false); + } + + return submitLoginAndContinue(pK) + .then(() => { + setLoading(false); + }) + .catch((error) => { + setError(error); + setLoading(false); + }); + }} > {loading && } continue