diff --git a/apps/login/app/(login)/mfa/set/page.tsx b/apps/login/app/(login)/mfa/set/page.tsx index fd6ee21afe7..0fbd6860a34 100644 --- a/apps/login/app/(login)/mfa/set/page.tsx +++ b/apps/login/app/(login)/mfa/set/page.tsx @@ -2,6 +2,8 @@ import { getBrandingSettings, getLoginSettings, getSession, + getUserByID, + listAuthenticationMethodTypes, server, } from "#/lib/zitadel"; import Alert from "#/ui/Alert"; @@ -34,8 +36,15 @@ export default async function Page({ organization ); return getSession(server, recent.id, recent.token).then((response) => { - if (response?.session) { - return response.session; + if (response?.session && response.session.factors?.user?.id) { + return listAuthenticationMethodTypes( + response.session.factors.user.id + ).then((methods) => { + return { + factors: response.session?.factors, + authMethods: methods.authMethodTypes ?? [], + }; + }); } }); } @@ -43,8 +52,15 @@ export default async function Page({ 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; + if (response?.session && response.session.factors?.user?.id) { + return listAuthenticationMethodTypes( + response.session.factors.user.id + ).then((methods) => { + return { + factors: response.session?.factors, + authMethods: methods.authMethodTypes ?? [], + }; + }); } }); } @@ -67,19 +83,18 @@ export default async function Page({ > )} - {!sessionFactors &&
} - {!(loginName || sessionId) && ( Provide your active session as loginName param )} - {loginSettings ? ( + {loginSettings && sessionFactors ? ( ) : ( No second factors available to setup. diff --git a/apps/login/app/(login)/u2f/page.tsx b/apps/login/app/(login)/u2f/page.tsx index a22fe449632..d9e35533e43 100644 --- a/apps/login/app/(login)/u2f/page.tsx +++ b/apps/login/app/(login)/u2f/page.tsx @@ -1,6 +1,7 @@ import { getBrandingSettings, getLoginSettings, server } from "#/lib/zitadel"; import DynamicTheme from "#/ui/DynamicTheme"; import LoginOTP from "#/ui/LoginOTP"; +import LoginPasskey from "#/ui/LoginPasskey"; import VerifyU2F from "#/ui/VerifyU2F"; export default async function Page({ @@ -22,12 +23,13 @@ export default async function Page({

Verify your account with your device.

- + altPassword={false} + > ); diff --git a/apps/login/tailwind.config.js b/apps/login/tailwind.config.js index c7ef01f23aa..63361428a62 100644 --- a/apps/login/tailwind.config.js +++ b/apps/login/tailwind.config.js @@ -7,6 +7,7 @@ let colors = { text: { light: { contrast: {} }, dark: { contrast: {} } }, link: { light: { contrast: {} }, dark: { contrast: {} } }, }; + const shades = [ "50", "100", @@ -49,7 +50,51 @@ module.exports = { }, theme: { extend: { - colors, + colors: { + ...colors, + state: { + success: { + light: { + background: "#cbf4c9", + color: "#0e6245", + }, + dark: { + background: "#68cf8340", + color: "#cbf4c9", + }, + }, + error: { + light: { + background: "#ffc1c1", + color: "#620e0e", + }, + dark: { + background: "#af455359", + color: "#ffc1c1", + }, + }, + neutral: { + light: { + background: "#e4e7e4", + color: "#000000", + }, + dark: { + background: "#1a253c", + color: "#ffffff", + }, + }, + alert: { + light: { + background: "#fbbf24", + color: "#92400e", + }, + dark: { + background: "#92400e50", + color: "#fbbf24", + }, + }, + }, + }, animation: { shake: "shake .8s cubic-bezier(.36,.07,.19,.97) both;", }, diff --git a/apps/login/ui/ChooseSecondFactorToSetup.tsx b/apps/login/ui/ChooseSecondFactorToSetup.tsx index e4d53b8eb3c..d38e27d0923 100644 --- a/apps/login/ui/ChooseSecondFactorToSetup.tsx +++ b/apps/login/ui/ChooseSecondFactorToSetup.tsx @@ -1,7 +1,9 @@ "use client"; -import { LoginSettings } from "@zitadel/server"; +import { AuthenticationMethodType, LoginSettings } from "@zitadel/server"; import Link from "next/link"; +import { BadgeState, StateBadge } from "./StateBadge"; +import clsx from "clsx"; type Props = { loginName?: string; @@ -9,6 +11,7 @@ type Props = { authRequestId?: string; organization?: string; loginSettings: LoginSettings; + userMethods: AuthenticationMethodType[]; }; export default function ChooseSecondFactorToSetup({ @@ -17,140 +20,181 @@ export default function ChooseSecondFactorToSetup({ authRequestId, organization, loginSettings, + userMethods, }: Props) { + const cardClasses = (alreadyAdded: boolean) => + clsx( + "bg-background-light-400 dark:bg-background-dark-400 group block space-y-1.5 rounded-md px-5 py-3 border border-divider-light dark:border-divider-dark transition-all ", + alreadyAdded ? "opacity-50" : "hover:shadow-lg hover:dark:bg-white/10" + ); + + const TOTP = (alreadyAdded: boolean) => { + return ( + +
+ + + + + + + {" "} + Authenticator App + {alreadyAdded && ( + <> + + + + )} +
+ + ); + }; + + const U2F = (alreadyAdded: boolean) => { + return ( + +
+ + + + Universal Second Factor + {alreadyAdded && ( + <> + + + + )} +
+ + ); + }; + + const EMAIL = (alreadyAdded: boolean) => { + return ( + +
+ + + + + Code via Email + {alreadyAdded && ( + <> + + + + )} +
+ + ); + }; + + const SMS = (alreadyAdded: boolean) => { + return ( + +
+ + + + Code via SMS + {alreadyAdded && ( + <> + + + + )} +
+ + ); + }; + return (
{loginSettings.secondFactors.map((factor, i) => { return (
- {factor === 1 && ( - -
- - - - - - - {" "} - Authenticator App -
- - )} - - {factor === 2 && ( - -
- - - - Universal Second Factor -
- - )} - {factor === 3 && ( - -
- - - - - Code via Email -
- - )} - {factor === 4 && ( - -
- - - - Code via SMS -
- - )} + {factor === 1 && TOTP(userMethods.includes(4))} + {factor === 2 && U2F(userMethods.includes(5))} + {factor === 3 && EMAIL(userMethods.includes(7))} + {factor === 4 && SMS(userMethods.includes(6))}
); })}
); } + +function Setup() { + return Setup; +} diff --git a/apps/login/ui/StateBadge.tsx b/apps/login/ui/StateBadge.tsx new file mode 100644 index 00000000000..ff8605c7d5f --- /dev/null +++ b/apps/login/ui/StateBadge.tsx @@ -0,0 +1,35 @@ +import clsx from "clsx"; +import { ReactNode } from "react"; + +export enum BadgeState { + Info = "info", + Error = "error", + Success = "success", + Alert = "alert", +} + +export type StateBadgeProps = { + state: BadgeState; + children: ReactNode; +}; + +const getBadgeClasses = (state: BadgeState) => + clsx({ + "w-fit border-box h-18.5px flex flex-row items-center whitespace-nowrap tracking-wider leading-4 items-center justify-center px-2 py-2px text-12px rounded-full shadow-sm": + true, + "bg-state-success-light-background text-state-success-light-color dark:bg-state-success-dark-background dark:text-state-success-dark-color ": + state === BadgeState.Success, + "bg-state-neutral-light-background text-state-neutral-light-color dark:bg-state-neutral-dark-background dark:text-state-neutral-dark-color": + state === BadgeState.Info, + "bg-state-error-light-background text-state-error-light-color dark:bg-state-error-dark-background dark:text-state-error-dark-color": + state === BadgeState.Error, + "bg-state-alert-light-background text-state-alert-light-color dark:bg-state-alert-dark-background dark:text-state-alert-dark-color": + state === BadgeState.Alert, + }); + +export function StateBadge({ + state = BadgeState.Success, + children, +}: StateBadgeProps) { + return {children}; +} diff --git a/apps/login/ui/VerifyU2F.tsx b/apps/login/ui/VerifyU2F.tsx deleted file mode 100644 index a0a3d7502c0..00000000000 --- a/apps/login/ui/VerifyU2F.tsx +++ /dev/null @@ -1,233 +0,0 @@ -"use client"; - -import { useEffect, useRef, useState } from "react"; -import { useRouter } from "next/navigation"; -import { coerceToArrayBuffer, coerceToBase64Url } from "#/utils/base64"; -import { Button, ButtonVariants } from "./Button"; -import Alert from "./Alert"; -import { Spinner } from "./Spinner"; -import { Checks } from "@zitadel/server"; - -// either loginName or sessionId must be provided -type Props = { - loginName?: string; - sessionId?: string; - authRequestId?: string; - organization?: string; -}; - -export default function VerifyU2F({ - loginName, - sessionId, - authRequestId, - organization, -}: Props) { - const [error, setError] = useState(""); - const [loading, setLoading] = useState(false); - - const router = useRouter(); - - const initialized = useRef(false); - - useEffect(() => { - if (!initialized.current) { - initialized.current = true; - setLoading(true); - updateSessionForChallenge() - .then((response) => { - const pK = - response.challenges.webAuthN.publicKeyCredentialRequestOptions - .publicKey; - if (pK) { - submitLoginAndContinue(pK) - .then(() => { - setLoading(false); - }) - .catch((error) => { - setError(error); - setLoading(false); - }); - } else { - setError("Could not request passkey challenge"); - setLoading(false); - } - }) - .catch((error) => { - setError(error); - setLoading(false); - }); - } - }, []); - - async function updateSessionForChallenge() { - setLoading(true); - const res = await fetch("/api/session", { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - loginName, - sessionId, - organization, - challenges: { - webAuthN: { - domain: "", - userVerificationRequirement: 1, - }, - }, - authRequestId, - }), - }); - - setLoading(false); - if (!res.ok) { - const error = await res.json(); - throw error.details.details; - } - return res.json(); - } - - async function submitLogin(data: any) { - setLoading(true); - const res = await fetch("/api/session", { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - loginName, - sessionId, - organization, - checks: { - webAuthN: { credentialAssertionData: data }, - } as Checks, - authRequestId, - }), - }); - - const response = await res.json(); - - setLoading(false); - if (!res.ok) { - setError(response.details); - return Promise.reject(response.details); - } - return response; - } - - async function submitLoginAndContinue( - publicKey: any - ): Promise { - publicKey.challenge = coerceToArrayBuffer( - publicKey.challenge, - "publicKey.challenge" - ); - publicKey.allowCredentials.map((listItem: any) => { - listItem.id = coerceToArrayBuffer( - listItem.id, - "publicKey.allowCredentials.id" - ); - }); - - navigator.credentials - .get({ - 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 { - return router.push( - `/signedin?` + - new URLSearchParams( - authRequestId - ? { - loginName: resp.factors.user.loginName, - authRequestId, - } - : { - loginName: resp.factors.user.loginName, - } - ) - ); - } - }); - } else { - setLoading(false); - setError("An error on retrieving passkey"); - return null; - } - }) - .catch((error) => { - console.error(error); - setLoading(false); - // setError(error); - return null; - }); - } - - return ( -
- {error && ( -
- {error} -
- )} -
- - - - -
-
- ); -}