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 && (
-
-
-
- )}
+ {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 && (
-
- )}
-
-
-
-
-
-
-
- );
-}