mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-12 06:52:24 +00:00
u2f pages, choose 2 factor page
This commit is contained in:
@@ -1,35 +1,89 @@
|
|||||||
import { getBrandingSettings, server } from "#/lib/zitadel";
|
import {
|
||||||
import { Button, ButtonVariants } from "#/ui/Button";
|
getBrandingSettings,
|
||||||
|
getLoginSettings,
|
||||||
|
getSession,
|
||||||
|
server,
|
||||||
|
} from "#/lib/zitadel";
|
||||||
|
import Alert from "#/ui/Alert";
|
||||||
|
import ChooseSecondFactorToSetup from "#/ui/ChooseSecondFactorToSetup";
|
||||||
import DynamicTheme from "#/ui/DynamicTheme";
|
import DynamicTheme from "#/ui/DynamicTheme";
|
||||||
import { TextInput } from "#/ui/Input";
|
|
||||||
import UserAvatar from "#/ui/UserAvatar";
|
import UserAvatar from "#/ui/UserAvatar";
|
||||||
import { useRouter } from "next/navigation";
|
import {
|
||||||
|
getMostRecentCookieWithLoginname,
|
||||||
|
getSessionCookieById,
|
||||||
|
} from "#/utils/cookies";
|
||||||
|
|
||||||
export default async function Page({
|
export default async function Page({
|
||||||
searchParams,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
searchParams: Record<string | number | symbol, string | undefined>;
|
searchParams: Record<string | number | symbol, string | undefined>;
|
||||||
}) {
|
}) {
|
||||||
const { loginName, authRequestId, sessionId, organization, code, submit } =
|
const { loginName, altPassword, authRequestId, organization, sessionId } =
|
||||||
searchParams;
|
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 branding = await getBrandingSettings(server, organization);
|
||||||
|
const loginSettings = await getLoginSettings(server, organization);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DynamicTheme branding={branding}>
|
<DynamicTheme branding={branding}>
|
||||||
<div className="flex flex-col items-center space-y-4">
|
<div className="flex flex-col items-center space-y-4">
|
||||||
<h1>Verify 2-Factor</h1>
|
<h1>Set up 2-Factor</h1>
|
||||||
|
|
||||||
<p className="ztdl-p">Choose one of the following second factors.</p>
|
<p className="ztdl-p">Choose one of the following second factors.</p>
|
||||||
|
|
||||||
<UserAvatar
|
{sessionFactors && (
|
||||||
showDropdown
|
<UserAvatar
|
||||||
displayName="Max Peintner"
|
loginName={loginName ?? sessionFactors.factors?.user?.loginName}
|
||||||
loginName="max@zitadel.com"
|
displayName={sessionFactors.factors?.user?.displayName}
|
||||||
></UserAvatar>
|
showDropdown
|
||||||
<div className="w-full">
|
></UserAvatar>
|
||||||
<TextInput type="password" label="Password" />
|
)}
|
||||||
</div>
|
|
||||||
|
{!sessionFactors && <div className="py-4"></div>}
|
||||||
|
|
||||||
|
{!(loginName || sessionId) && (
|
||||||
|
<Alert>Provide your active session as loginName param</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{loginSettings ? (
|
||||||
|
<ChooseSecondFactorToSetup
|
||||||
|
loginName={loginName}
|
||||||
|
sessionId={sessionId}
|
||||||
|
authRequestId={authRequestId}
|
||||||
|
organization={organization}
|
||||||
|
loginSettings={loginSettings}
|
||||||
|
></ChooseSecondFactorToSetup>
|
||||||
|
) : (
|
||||||
|
<Alert>No second factors available to setup.</Alert>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</DynamicTheme>
|
</DynamicTheme>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -30,11 +30,8 @@ export default async function Page({
|
|||||||
{method === "email" && (
|
{method === "email" && (
|
||||||
<p className="ztdl-p">Enter the code you got via your email.</p>
|
<p className="ztdl-p">Enter the code you got via your email.</p>
|
||||||
)}
|
)}
|
||||||
{method === "u2f" && (
|
|
||||||
<p className="ztdl-p">Verify your account with your device.</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{method && ["time-based", "sms", "email"].includes(method) ? (
|
{method && (
|
||||||
<LoginOTP
|
<LoginOTP
|
||||||
loginName={loginName}
|
loginName={loginName}
|
||||||
sessionId={sessionId}
|
sessionId={sessionId}
|
||||||
@@ -42,13 +39,6 @@ export default async function Page({
|
|||||||
organization={organization}
|
organization={organization}
|
||||||
method={method}
|
method={method}
|
||||||
></LoginOTP>
|
></LoginOTP>
|
||||||
) : (
|
|
||||||
<VerifyU2F
|
|
||||||
loginName={loginName}
|
|
||||||
sessionId={sessionId}
|
|
||||||
authRequestId={authRequestId}
|
|
||||||
organization={organization}
|
|
||||||
></VerifyU2F>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</DynamicTheme>
|
</DynamicTheme>
|
||||||
|
|||||||
34
apps/login/app/(login)/u2f/page.tsx
Normal file
34
apps/login/app/(login)/u2f/page.tsx
Normal file
@@ -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<string | number | symbol, string | undefined>;
|
||||||
|
params: Record<string | number | symbol, string | undefined>;
|
||||||
|
}) {
|
||||||
|
const { loginName, authRequestId, sessionId, organization, code, submit } =
|
||||||
|
searchParams;
|
||||||
|
|
||||||
|
const branding = await getBrandingSettings(server, organization);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DynamicTheme branding={branding}>
|
||||||
|
<div className="flex flex-col items-center space-y-4">
|
||||||
|
<h1>Verify 2-Factor</h1>
|
||||||
|
|
||||||
|
<p className="ztdl-p">Verify your account with your device.</p>
|
||||||
|
|
||||||
|
<VerifyU2F
|
||||||
|
loginName={loginName}
|
||||||
|
sessionId={sessionId}
|
||||||
|
authRequestId={authRequestId}
|
||||||
|
organization={organization}
|
||||||
|
></VerifyU2F>
|
||||||
|
</div>
|
||||||
|
</DynamicTheme>
|
||||||
|
);
|
||||||
|
}
|
||||||
76
apps/login/app/(login)/u2f/set/page.tsx
Normal file
76
apps/login/app/(login)/u2f/set/page.tsx
Normal file
@@ -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<string | number | symbol, string | undefined>;
|
||||||
|
params: Record<string | number | symbol, string | undefined>;
|
||||||
|
}) {
|
||||||
|
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 (
|
||||||
|
<DynamicTheme branding={branding}>
|
||||||
|
<div className="flex flex-col items-center space-y-4">
|
||||||
|
<h1>Register Device</h1>
|
||||||
|
<p className="ztdl-p">
|
||||||
|
Choose a device to register for 2-Factor Authentication.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{/* {auth && <div>{auth.to}</div>} */}
|
||||||
|
{totpResponse &&
|
||||||
|
"uri" in totpResponse &&
|
||||||
|
"secret" in totpResponse && (
|
||||||
|
<TOTPRegister
|
||||||
|
uri={totpResponse.uri as string}
|
||||||
|
secret={totpResponse.secret as string}
|
||||||
|
></TOTPRegister>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DynamicTheme>
|
||||||
|
);
|
||||||
|
}
|
||||||
141
apps/login/ui/ChooseSecondFactorToSetup.tsx
Normal file
141
apps/login/ui/ChooseSecondFactorToSetup.tsx
Normal file
@@ -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 (
|
||||||
|
<div className="grid grid-cols-1 gap-5 w-full pt-4">
|
||||||
|
{loginSettings.secondFactors.map((factor, i) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={"method-" + i}
|
||||||
|
className="bg-background-light-400 dark:bg-background-dark-400 group block space-y-1.5 rounded-md px-5 py-3 hover:shadow-lg hover:dark:bg-white/10 border border-divider-light dark:border-divider-dark transition-all "
|
||||||
|
>
|
||||||
|
{factor === 1 && (
|
||||||
|
<div className="font-medium flex items-center">
|
||||||
|
<svg
|
||||||
|
className="h-9 w-9 transform -translate-x-[2px] mr-4"
|
||||||
|
version="1.1"
|
||||||
|
baseProfile="basic"
|
||||||
|
id="Layer_1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
xmlSpace="preserve"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="#1A73E8"
|
||||||
|
d="M440,255.99997v0.00006C440,273.12085,426.12085,287,409.00003,287H302l-46-93.01001l49.6507-85.9951
|
||||||
|
c8.56021-14.82629,27.51834-19.9065,42.34518-11.34724l0.00586,0.0034c14.82776,8.55979,19.90875,27.51928,11.34857,42.34682
|
||||||
|
L309.70001,225h99.30002C426.12085,225,440,238.87917,440,255.99997z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#EA4335"
|
||||||
|
d="M348.00174,415.34897l-0.00586,0.00339c-14.82684,8.55927-33.78497,3.47903-42.34518-11.34723L256,318.01001
|
||||||
|
l-49.65065,85.99509c-8.5602,14.82629-27.51834,19.90652-42.34517,11.34729l-0.00591-0.00342
|
||||||
|
c-14.82777-8.55978-19.90875-27.51929-11.34859-42.34683L202.29999,287L256,285l53.70001,2l49.6503,86.00214
|
||||||
|
C367.91049,387.82968,362.8295,406.78918,348.00174,415.34897z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#FBBC04"
|
||||||
|
d="M256,193.98999L242,232l-39.70001-7l-49.6503-86.00212
|
||||||
|
c-8.56017-14.82755-3.47919-33.78705,11.34859-42.34684l0.00591-0.00341c14.82683-8.55925,33.78497-3.47903,42.34517,11.34726
|
||||||
|
L256,193.98999z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#34A853"
|
||||||
|
d="M248,225l-36,62H102.99997C85.87916,287,72,273.12085,72,256.00003v-0.00006
|
||||||
|
C72,238.87917,85.87916,225,102.99997,225H248z"
|
||||||
|
/>
|
||||||
|
<polygon
|
||||||
|
fill="#185DB7"
|
||||||
|
points="309.70001,287 202.29999,287 256,193.98999 "
|
||||||
|
/>
|
||||||
|
</svg>{" "}
|
||||||
|
<span>Authenticator App</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{factor === 2 && (
|
||||||
|
<div className="font-medium flex items-center">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
className="w-8 h-8 mr-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M7.864 4.243A7.5 7.5 0 0119.5 10.5c0 2.92-.556 5.709-1.568 8.268M5.742 6.364A7.465 7.465 0 004.5 10.5a7.464 7.464 0 01-1.15 3.993m1.989 3.559A11.209 11.209 0 008.25 10.5a3.75 3.75 0 117.5 0c0 .527-.021 1.049-.064 1.565M12 10.5a14.94 14.94 0 01-3.6 9.75m6.633-4.596a18.666 18.666 0 01-2.485 5.33"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>Universal Second Factor</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{factor === 3 && (
|
||||||
|
<div className="font-medium flex items-center">
|
||||||
|
<svg
|
||||||
|
className="w-8 h-8 mr-4"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<span>Code via Email</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{factor === 4 && (
|
||||||
|
<div className="font-medium flex items-center">
|
||||||
|
<svg
|
||||||
|
className="w-8 h-8 mr-4"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M10.5 1.5H8.25A2.25 2.25 0 006 3.75v16.5a2.25 2.25 0 002.25 2.25h7.5A2.25 2.25 0 0018 20.25V3.75a2.25 2.25 0 00-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>Code via SMS</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* <div className="line-clamp-3 text-sm text-text-light-secondary-500 dark:text-text-dark-secondary-500">
|
||||||
|
{factor}
|
||||||
|
</div> */}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -71,8 +71,8 @@ export default function PasswordForm({
|
|||||||
|
|
||||||
function submitPasswordAndContinue(value: Inputs): Promise<boolean | void> {
|
function submitPasswordAndContinue(value: Inputs): Promise<boolean | void> {
|
||||||
return submitPassword(value).then((resp) => {
|
return submitPassword(value).then((resp) => {
|
||||||
// if user has mfa -> /totp
|
// if user has mfa -> /otp/[method] or /u2f
|
||||||
// if mfa is forced -> /mfa/set
|
// if mfa is forced and user has no mfa -> /mfa/set
|
||||||
// if no passwordless -> /passkey/add
|
// if no passwordless -> /passkey/add
|
||||||
if (resp.authFactors?.length == 1) {
|
if (resp.authFactors?.length == 1) {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
@@ -88,17 +88,20 @@ export default function PasswordForm({
|
|||||||
}
|
}
|
||||||
|
|
||||||
let method;
|
let method;
|
||||||
if ((resp.authFactors as AuthFactor[])[0].otp) {
|
const factor = (resp.authFactors as AuthFactor[])[0];
|
||||||
|
if (factor.otp) {
|
||||||
method = "time-based";
|
method = "time-based";
|
||||||
} else if ((resp.authFactors as AuthFactor[])[0].otpSms) {
|
return router.push(`/otp/${method}?` + params);
|
||||||
|
} else if (factor.otpSms) {
|
||||||
method = "sms";
|
method = "sms";
|
||||||
} else if ((resp.authFactors as AuthFactor[])[0].otpEmail) {
|
return router.push(`/otp/${method}?` + params);
|
||||||
|
} else if (factor.otpEmail) {
|
||||||
method = "email";
|
method = "email";
|
||||||
} else if ((resp.authFactors as AuthFactor[])[0].u2f) {
|
return router.push(`/otp/${method}?` + params);
|
||||||
|
} else if (factor.u2f) {
|
||||||
method = "u2f";
|
method = "u2f";
|
||||||
|
return router.push(`/u2f?` + params);
|
||||||
}
|
}
|
||||||
|
|
||||||
return router.push(`/otp/${method}?` + params);
|
|
||||||
} else if (resp.authFactors?.length >= 1) {
|
} else if (resp.authFactors?.length >= 1) {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
loginName: resp.factors.user.loginName,
|
loginName: resp.factors.user.loginName,
|
||||||
@@ -113,6 +116,20 @@ export default function PasswordForm({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return router.push(`/mfa?` + params);
|
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 (
|
} else if (
|
||||||
resp.factors &&
|
resp.factors &&
|
||||||
!resp.factors.passwordless && // if session was not verified with a passkey
|
!resp.factors.passwordless && // if session was not verified with a passkey
|
||||||
|
|||||||
Reference in New Issue
Block a user