mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-12 06:42:59 +00:00
@@ -97,6 +97,7 @@ export default async function Page({
|
|||||||
return retrieveIDPIntent(id, token)
|
return retrieveIDPIntent(id, token)
|
||||||
.then((resp) => {
|
.then((resp) => {
|
||||||
const { idpInformation, userId } = resp;
|
const { idpInformation, userId } = resp;
|
||||||
|
|
||||||
if (idpInformation) {
|
if (idpInformation) {
|
||||||
// handle login
|
// handle login
|
||||||
if (userId) {
|
if (userId) {
|
||||||
@@ -166,10 +167,14 @@ export default async function Page({
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center space-y-4">
|
<DynamicTheme branding={branding}>
|
||||||
<h1>Register</h1>
|
<div className="flex flex-col items-center space-y-4">
|
||||||
<p className="ztdl-p">No id and token received!</p>
|
<div className="flex flex-col items-center space-y-4">
|
||||||
</div>
|
<h1>Register</h1>
|
||||||
|
<p className="ztdl-p">No id and token received!</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DynamicTheme>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import { Button, ButtonVariants } from "#/ui/Button";
|
|
||||||
import { TextInput } from "#/ui/Input";
|
|
||||||
import UserAvatar from "#/ui/UserAvatar";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
|
|
||||||
export default function Page() {
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center space-y-4">
|
|
||||||
<h1>Password</h1>
|
|
||||||
<p className="ztdl-p mb-6 block">Enter your password.</p>
|
|
||||||
|
|
||||||
<UserAvatar
|
|
||||||
showDropdown
|
|
||||||
displayName="Max Peintner"
|
|
||||||
loginName="max@zitadel.com"
|
|
||||||
></UserAvatar>
|
|
||||||
|
|
||||||
<div className="w-full">
|
|
||||||
<TextInput type="password" label="Password" />
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full flex-row items-center justify-between">
|
|
||||||
<Button
|
|
||||||
onClick={() => router.back()}
|
|
||||||
variant={ButtonVariants.Secondary}
|
|
||||||
>
|
|
||||||
back
|
|
||||||
</Button>
|
|
||||||
<Button variant={ButtonVariants.Primary}>continue</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,102 @@
|
|||||||
export default function Page() {
|
import {
|
||||||
return <div className="flex flex-col items-center space-y-4">mfa</div>;
|
getBrandingSettings,
|
||||||
|
getSession,
|
||||||
|
listAuthenticationMethodTypes,
|
||||||
|
server,
|
||||||
|
} from "#/lib/zitadel";
|
||||||
|
import Alert from "#/ui/Alert";
|
||||||
|
import ChooseSecondFactor from "#/ui/ChooseSecondFactor";
|
||||||
|
import DynamicTheme from "#/ui/DynamicTheme";
|
||||||
|
import UserAvatar from "#/ui/UserAvatar";
|
||||||
|
import {
|
||||||
|
getMostRecentCookieWithLoginname,
|
||||||
|
getSessionCookieById,
|
||||||
|
} from "#/utils/cookies";
|
||||||
|
|
||||||
|
export default async function Page({
|
||||||
|
searchParams,
|
||||||
|
}: {
|
||||||
|
searchParams: Record<string | number | symbol, string | undefined>;
|
||||||
|
}) {
|
||||||
|
const { loginName, checkAfter, 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 && response.session.factors?.user?.id) {
|
||||||
|
return listAuthenticationMethodTypes(
|
||||||
|
response.session.factors.user.id
|
||||||
|
).then((methods) => {
|
||||||
|
return {
|
||||||
|
factors: response.session?.factors,
|
||||||
|
authMethods: methods.authMethodTypes ?? [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 && response.session.factors?.user?.id) {
|
||||||
|
return listAuthenticationMethodTypes(
|
||||||
|
response.session.factors.user.id
|
||||||
|
).then((methods) => {
|
||||||
|
return {
|
||||||
|
factors: response.session?.factors,
|
||||||
|
authMethods: methods.authMethodTypes ?? [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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">Choose one of the following second factors.</p>
|
||||||
|
|
||||||
|
{sessionFactors && (
|
||||||
|
<UserAvatar
|
||||||
|
loginName={loginName ?? sessionFactors.factors?.user?.loginName}
|
||||||
|
displayName={sessionFactors.factors?.user?.displayName}
|
||||||
|
showDropdown
|
||||||
|
searchParams={searchParams}
|
||||||
|
></UserAvatar>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!(loginName || sessionId) && (
|
||||||
|
<Alert>Provide your active session as loginName param</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{sessionFactors ? (
|
||||||
|
<ChooseSecondFactor
|
||||||
|
loginName={loginName}
|
||||||
|
sessionId={sessionId}
|
||||||
|
authRequestId={authRequestId}
|
||||||
|
organization={organization}
|
||||||
|
userMethods={sessionFactors.authMethods ?? []}
|
||||||
|
></ChooseSecondFactor>
|
||||||
|
) : (
|
||||||
|
<Alert>No second factors available to setup.</Alert>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DynamicTheme>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,116 @@
|
|||||||
"use client";
|
import {
|
||||||
import { Button, ButtonVariants } from "#/ui/Button";
|
getBrandingSettings,
|
||||||
import { TextInput } from "#/ui/Input";
|
getLoginSettings,
|
||||||
|
getSession,
|
||||||
|
getUserByID,
|
||||||
|
listAuthenticationMethodTypes,
|
||||||
|
server,
|
||||||
|
} from "#/lib/zitadel";
|
||||||
|
import Alert from "#/ui/Alert";
|
||||||
|
import ChooseSecondFactorToSetup from "#/ui/ChooseSecondFactorToSetup";
|
||||||
|
import DynamicTheme from "#/ui/DynamicTheme";
|
||||||
import UserAvatar from "#/ui/UserAvatar";
|
import UserAvatar from "#/ui/UserAvatar";
|
||||||
import { useRouter } from "next/navigation";
|
import {
|
||||||
|
getMostRecentCookieWithLoginname,
|
||||||
|
getSessionCookieById,
|
||||||
|
} from "#/utils/cookies";
|
||||||
|
import { user } from "@zitadel/server";
|
||||||
|
|
||||||
export default function Page() {
|
export default async function Page({
|
||||||
const router = useRouter();
|
searchParams,
|
||||||
|
}: {
|
||||||
|
searchParams: Record<string | number | symbol, string | undefined>;
|
||||||
|
}) {
|
||||||
|
const { loginName, checkAfter, authRequestId, organization, sessionId } =
|
||||||
|
searchParams;
|
||||||
|
|
||||||
|
const sessionWithData = 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 && response.session.factors?.user?.id) {
|
||||||
|
const userId = response.session.factors.user.id;
|
||||||
|
return listAuthenticationMethodTypes(userId).then((methods) => {
|
||||||
|
return getUserByID(userId).then((user) => {
|
||||||
|
return {
|
||||||
|
factors: response.session?.factors,
|
||||||
|
authMethods: methods.authMethodTypes ?? [],
|
||||||
|
phoneVerified: user.user?.human?.phone?.isVerified ?? false,
|
||||||
|
emailVerified: user.user?.human?.email?.isVerified ?? false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 && response.session.factors?.user?.id) {
|
||||||
|
const userId = response.session.factors.user.id;
|
||||||
|
return listAuthenticationMethodTypes(userId).then((methods) => {
|
||||||
|
return getUserByID(userId).then((user) => {
|
||||||
|
return {
|
||||||
|
factors: response.session?.factors,
|
||||||
|
authMethods: methods.authMethodTypes ?? [],
|
||||||
|
phoneVerified: user.user?.human?.phone?.isVerified ?? false,
|
||||||
|
emailVerified: user.user?.human?.email?.isVerified ?? false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const branding = await getBrandingSettings(server, organization);
|
||||||
|
const loginSettings = await getLoginSettings(server, organization);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center space-y-4">
|
<DynamicTheme branding={branding}>
|
||||||
<h1>Password</h1>
|
<div className="flex flex-col items-center space-y-4">
|
||||||
<p className="ztdl-p mb-6 block">Enter your password.</p>
|
<h1>Set up 2-Factor</h1>
|
||||||
|
|
||||||
<UserAvatar
|
<p className="ztdl-p">Choose one of the following second factors.</p>
|
||||||
showDropdown
|
|
||||||
displayName="Max Peintner"
|
{sessionWithData && (
|
||||||
loginName="max@zitadel.com"
|
<UserAvatar
|
||||||
></UserAvatar>
|
loginName={loginName ?? sessionWithData.factors?.user?.loginName}
|
||||||
<div className="w-full">
|
displayName={sessionWithData.factors?.user?.displayName}
|
||||||
<TextInput type="password" label="Password" />
|
showDropdown
|
||||||
|
searchParams={searchParams}
|
||||||
|
></UserAvatar>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!(loginName || sessionId) && (
|
||||||
|
<Alert>Provide your active session as loginName param</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{loginSettings && sessionWithData ? (
|
||||||
|
<ChooseSecondFactorToSetup
|
||||||
|
loginName={loginName}
|
||||||
|
sessionId={sessionId}
|
||||||
|
authRequestId={authRequestId}
|
||||||
|
organization={organization}
|
||||||
|
loginSettings={loginSettings}
|
||||||
|
userMethods={sessionWithData.authMethods ?? []}
|
||||||
|
phoneVerified={sessionWithData.phoneVerified ?? false}
|
||||||
|
emailVerified={sessionWithData.emailVerified ?? false}
|
||||||
|
checkAfter={checkAfter === "true"}
|
||||||
|
></ChooseSecondFactorToSetup>
|
||||||
|
) : (
|
||||||
|
<Alert>No second factors available to setup.</Alert>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full flex-row items-center justify-between">
|
</DynamicTheme>
|
||||||
<Button
|
|
||||||
onClick={() => router.back()}
|
|
||||||
variant={ButtonVariants.Secondary}
|
|
||||||
>
|
|
||||||
back
|
|
||||||
</Button>
|
|
||||||
<Button variant={ButtonVariants.Primary}>continue</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
84
apps/login/app/(login)/otp/[method]/page.tsx
Normal file
84
apps/login/app/(login)/otp/[method]/page.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import {
|
||||||
|
getBrandingSettings,
|
||||||
|
getLoginSettings,
|
||||||
|
getSession,
|
||||||
|
server,
|
||||||
|
} from "#/lib/zitadel";
|
||||||
|
import Alert from "#/ui/Alert";
|
||||||
|
import DynamicTheme from "#/ui/DynamicTheme";
|
||||||
|
import LoginOTP from "#/ui/LoginOTP";
|
||||||
|
import UserAvatar from "#/ui/UserAvatar";
|
||||||
|
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, authRequestId, sessionId, organization, code, submit } =
|
||||||
|
searchParams;
|
||||||
|
|
||||||
|
const { method } = params;
|
||||||
|
|
||||||
|
const { session, token } = await loadSession(loginName, organization);
|
||||||
|
|
||||||
|
const branding = await getBrandingSettings(server, organization);
|
||||||
|
|
||||||
|
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>Verify 2-Factor</h1>
|
||||||
|
{method === "time-based" && (
|
||||||
|
<p className="ztdl-p">Enter the code from your authenticator app.</p>
|
||||||
|
)}
|
||||||
|
{method === "sms" && (
|
||||||
|
<p className="ztdl-p">Enter the code you got on your phone.</p>
|
||||||
|
)}
|
||||||
|
{method === "email" && (
|
||||||
|
<p className="ztdl-p">Enter the code you got via your email.</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!session && (
|
||||||
|
<div className="py-4">
|
||||||
|
<Alert>
|
||||||
|
Could not get the context of the user. Make sure to enter the
|
||||||
|
username first or provide a loginName as searchParam.
|
||||||
|
</Alert>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{session && (
|
||||||
|
<UserAvatar
|
||||||
|
loginName={loginName ?? session.factors?.user?.loginName}
|
||||||
|
displayName={session.factors?.user?.displayName}
|
||||||
|
showDropdown
|
||||||
|
searchParams={searchParams}
|
||||||
|
></UserAvatar>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{method && (
|
||||||
|
<LoginOTP
|
||||||
|
loginName={loginName}
|
||||||
|
sessionId={sessionId}
|
||||||
|
authRequestId={authRequestId}
|
||||||
|
organization={organization}
|
||||||
|
method={method}
|
||||||
|
></LoginOTP>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DynamicTheme>
|
||||||
|
);
|
||||||
|
}
|
||||||
179
apps/login/app/(login)/otp/[method]/set/page.tsx
Normal file
179
apps/login/app/(login)/otp/[method]/set/page.tsx
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import {
|
||||||
|
addOTPEmail,
|
||||||
|
addOTPSMS,
|
||||||
|
getBrandingSettings,
|
||||||
|
getSession,
|
||||||
|
registerTOTP,
|
||||||
|
server,
|
||||||
|
} from "#/lib/zitadel";
|
||||||
|
import Alert from "#/ui/Alert";
|
||||||
|
import { Button, ButtonVariants } from "#/ui/Button";
|
||||||
|
import DynamicTheme from "#/ui/DynamicTheme";
|
||||||
|
import { Spinner } from "#/ui/Spinner";
|
||||||
|
import TOTPRegister from "#/ui/TOTPRegister";
|
||||||
|
import UserAvatar from "#/ui/UserAvatar";
|
||||||
|
import { getMostRecentCookieWithLoginname } from "#/utils/cookies";
|
||||||
|
import { RegisterTOTPResponse } from "@zitadel/server";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { ClientError } from "nice-grpc";
|
||||||
|
|
||||||
|
export default async function Page({
|
||||||
|
searchParams,
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
searchParams: Record<string | number | symbol, string | undefined>;
|
||||||
|
params: Record<string | number | symbol, string | undefined>;
|
||||||
|
}) {
|
||||||
|
const { loginName, organization, sessionId, authRequestId, checkAfter } =
|
||||||
|
searchParams;
|
||||||
|
const { method } = params;
|
||||||
|
|
||||||
|
const branding = await getBrandingSettings(server, organization);
|
||||||
|
const { session, token } = await loadSession(loginName, organization);
|
||||||
|
|
||||||
|
let totpResponse: RegisterTOTPResponse | undefined,
|
||||||
|
totpError: ClientError | undefined;
|
||||||
|
if (session && session.factors?.user?.id) {
|
||||||
|
if (method === "time-based") {
|
||||||
|
await registerTOTP(session.factors.user.id)
|
||||||
|
.then((resp) => {
|
||||||
|
if (resp) {
|
||||||
|
totpResponse = resp;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
totpError = error;
|
||||||
|
});
|
||||||
|
} else if (method === "sms") {
|
||||||
|
// does not work
|
||||||
|
await addOTPSMS(session.factors.user.id);
|
||||||
|
} else if (method === "email") {
|
||||||
|
// works
|
||||||
|
await 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 };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const paramsToContinue = new URLSearchParams({});
|
||||||
|
let urlToContinue = "/accounts";
|
||||||
|
|
||||||
|
if (authRequestId && sessionId) {
|
||||||
|
if (sessionId) {
|
||||||
|
paramsToContinue.append("sessionId", sessionId);
|
||||||
|
}
|
||||||
|
if (authRequestId) {
|
||||||
|
paramsToContinue.append("authRequestId", authRequestId);
|
||||||
|
}
|
||||||
|
if (organization) {
|
||||||
|
paramsToContinue.append("organization", organization);
|
||||||
|
}
|
||||||
|
urlToContinue = `/login?` + paramsToContinue;
|
||||||
|
} else if (loginName) {
|
||||||
|
if (loginName) {
|
||||||
|
paramsToContinue.append("loginName", loginName);
|
||||||
|
}
|
||||||
|
if (authRequestId) {
|
||||||
|
paramsToContinue.append("authRequestId", authRequestId);
|
||||||
|
}
|
||||||
|
if (organization) {
|
||||||
|
paramsToContinue.append("organization", organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
urlToContinue = `/signedin?` + paramsToContinue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DynamicTheme branding={branding}>
|
||||||
|
<div className="flex flex-col items-center space-y-4">
|
||||||
|
<h1>Register 2-factor</h1>
|
||||||
|
{!session && (
|
||||||
|
<div className="py-4">
|
||||||
|
<Alert>
|
||||||
|
Could not get the context of the user. Make sure to enter the
|
||||||
|
username first or provide a loginName as searchParam.
|
||||||
|
</Alert>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{totpError && (
|
||||||
|
<div className="py-4">
|
||||||
|
<Alert>{totpError?.details}</Alert>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{session && (
|
||||||
|
<UserAvatar
|
||||||
|
loginName={loginName ?? session.factors?.user?.loginName}
|
||||||
|
displayName={session.factors?.user?.displayName}
|
||||||
|
showDropdown
|
||||||
|
searchParams={searchParams}
|
||||||
|
></UserAvatar>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{totpResponse && "uri" in totpResponse && "secret" in totpResponse ? (
|
||||||
|
<>
|
||||||
|
<p className="ztdl-p">
|
||||||
|
Scan the QR Code or navigate to the URL manually.
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
{/* {auth && <div>{auth.to}</div>} */}
|
||||||
|
|
||||||
|
<TOTPRegister
|
||||||
|
uri={totpResponse.uri as string}
|
||||||
|
secret={totpResponse.secret as string}
|
||||||
|
loginName={loginName}
|
||||||
|
sessionId={sessionId}
|
||||||
|
authRequestId={authRequestId}
|
||||||
|
organization={organization}
|
||||||
|
checkAfter={checkAfter === "true"}
|
||||||
|
></TOTPRegister>
|
||||||
|
</div>{" "}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p className="ztdl-p">
|
||||||
|
{method === "email"
|
||||||
|
? "Code via email was successfully added."
|
||||||
|
: method === "sms"
|
||||||
|
? "Code via SMS was successfully added."
|
||||||
|
: ""}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-8 flex w-full flex-row items-center">
|
||||||
|
<span className="flex-grow"></span>
|
||||||
|
<Link
|
||||||
|
href={
|
||||||
|
checkAfter
|
||||||
|
? `/otp/${method}?` + new URLSearchParams()
|
||||||
|
: urlToContinue
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="self-end"
|
||||||
|
variant={ButtonVariants.Primary}
|
||||||
|
>
|
||||||
|
continue
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DynamicTheme>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -45,6 +45,7 @@ export default async function Page({
|
|||||||
loginName={loginName ?? sessionFactors.factors?.user?.loginName}
|
loginName={loginName ?? sessionFactors.factors?.user?.loginName}
|
||||||
displayName={sessionFactors.factors?.user?.displayName}
|
displayName={sessionFactors.factors?.user?.displayName}
|
||||||
showDropdown
|
showDropdown
|
||||||
|
searchParams={searchParams}
|
||||||
></UserAvatar>
|
></UserAvatar>
|
||||||
)}
|
)}
|
||||||
<p className="ztdl-p mb-6 block">{description}</p>
|
<p className="ztdl-p mb-6 block">{description}</p>
|
||||||
|
|||||||
@@ -60,12 +60,11 @@ export default async function Page({
|
|||||||
loginName={loginName ?? sessionFactors.factors?.user?.loginName}
|
loginName={loginName ?? sessionFactors.factors?.user?.loginName}
|
||||||
displayName={sessionFactors.factors?.user?.displayName}
|
displayName={sessionFactors.factors?.user?.displayName}
|
||||||
showDropdown
|
showDropdown
|
||||||
|
searchParams={searchParams}
|
||||||
></UserAvatar>
|
></UserAvatar>
|
||||||
)}
|
)}
|
||||||
<p className="ztdl-p mb-6 block">{description}</p>
|
<p className="ztdl-p mb-6 block">{description}</p>
|
||||||
|
|
||||||
{!sessionFactors && <div className="py-4"></div>}
|
|
||||||
|
|
||||||
{!(loginName || sessionId) && (
|
{!(loginName || sessionId) && (
|
||||||
<Alert>Provide your active session as loginName param</Alert>
|
<Alert>Provide your active session as loginName param</Alert>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { getBrandingSettings, getSession, server } from "#/lib/zitadel";
|
import {
|
||||||
|
getBrandingSettings,
|
||||||
|
getLoginSettings,
|
||||||
|
getSession,
|
||||||
|
server,
|
||||||
|
} from "#/lib/zitadel";
|
||||||
import Alert from "#/ui/Alert";
|
import Alert from "#/ui/Alert";
|
||||||
import DynamicTheme from "#/ui/DynamicTheme";
|
import DynamicTheme from "#/ui/DynamicTheme";
|
||||||
import PasswordForm from "#/ui/PasswordForm";
|
import PasswordForm from "#/ui/PasswordForm";
|
||||||
@@ -28,6 +33,7 @@ export default async function Page({
|
|||||||
}
|
}
|
||||||
|
|
||||||
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}>
|
||||||
@@ -49,6 +55,7 @@ export default async function Page({
|
|||||||
loginName={loginName ?? sessionFactors.factors?.user?.loginName}
|
loginName={loginName ?? sessionFactors.factors?.user?.loginName}
|
||||||
displayName={sessionFactors.factors?.user?.displayName}
|
displayName={sessionFactors.factors?.user?.displayName}
|
||||||
showDropdown
|
showDropdown
|
||||||
|
searchParams={searchParams}
|
||||||
></UserAvatar>
|
></UserAvatar>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -56,6 +63,7 @@ export default async function Page({
|
|||||||
loginName={loginName}
|
loginName={loginName}
|
||||||
authRequestId={authRequestId}
|
authRequestId={authRequestId}
|
||||||
organization={organization}
|
organization={organization}
|
||||||
|
loginSettings={loginSettings}
|
||||||
promptPasswordless={promptPasswordless === "true"}
|
promptPasswordless={promptPasswordless === "true"}
|
||||||
isAlternative={alt === "true"}
|
isAlternative={alt === "true"}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export default async function Page({ searchParams }: { searchParams: any }) {
|
|||||||
loginName={loginName ?? sessionFactors?.factors?.user?.loginName}
|
loginName={loginName ?? sessionFactors?.factors?.user?.loginName}
|
||||||
displayName={sessionFactors?.factors?.user?.displayName}
|
displayName={sessionFactors?.factors?.user?.displayName}
|
||||||
showDropdown
|
showDropdown
|
||||||
|
searchParams={searchParams}
|
||||||
></UserAvatar>
|
></UserAvatar>
|
||||||
</div>
|
</div>
|
||||||
</DynamicTheme>
|
</DynamicTheme>
|
||||||
|
|||||||
89
apps/login/app/(login)/u2f/page.tsx
Normal file
89
apps/login/app/(login)/u2f/page.tsx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import {
|
||||||
|
getBrandingSettings,
|
||||||
|
getLoginSettings,
|
||||||
|
getSession,
|
||||||
|
server,
|
||||||
|
} from "#/lib/zitadel";
|
||||||
|
import Alert from "#/ui/Alert";
|
||||||
|
import DynamicTheme from "#/ui/DynamicTheme";
|
||||||
|
import LoginPasskey from "#/ui/LoginPasskey";
|
||||||
|
import UserAvatar from "#/ui/UserAvatar";
|
||||||
|
import {
|
||||||
|
getMostRecentCookieWithLoginname,
|
||||||
|
getSessionCookieById,
|
||||||
|
} 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, authRequestId, sessionId, organization } = searchParams;
|
||||||
|
|
||||||
|
const branding = await getBrandingSettings(server, organization);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DynamicTheme branding={branding}>
|
||||||
|
<div className="flex flex-col items-center space-y-4">
|
||||||
|
<h1>Verify 2-Factor</h1>
|
||||||
|
|
||||||
|
{sessionFactors && (
|
||||||
|
<UserAvatar
|
||||||
|
loginName={loginName ?? sessionFactors.factors?.user?.loginName}
|
||||||
|
displayName={sessionFactors.factors?.user?.displayName}
|
||||||
|
showDropdown
|
||||||
|
searchParams={searchParams}
|
||||||
|
></UserAvatar>
|
||||||
|
)}
|
||||||
|
<p className="ztdl-p mb-6 block">
|
||||||
|
Verify your account with your device.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{!(loginName || sessionId) && (
|
||||||
|
<Alert>Provide your active session as loginName param</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(loginName || sessionId) && (
|
||||||
|
<LoginPasskey
|
||||||
|
loginName={loginName}
|
||||||
|
sessionId={sessionId}
|
||||||
|
authRequestId={authRequestId}
|
||||||
|
altPassword={false}
|
||||||
|
organization={organization}
|
||||||
|
login={false} // this sets the userVerificationRequirement to discouraged as its used as second factor
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DynamicTheme>
|
||||||
|
);
|
||||||
|
}
|
||||||
69
apps/login/app/(login)/u2f/set/page.tsx
Normal file
69
apps/login/app/(login)/u2f/set/page.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { getBrandingSettings, getSession, server } from "#/lib/zitadel";
|
||||||
|
import Alert, { AlertType } from "#/ui/Alert";
|
||||||
|
import DynamicTheme from "#/ui/DynamicTheme";
|
||||||
|
import RegisterPasskey from "#/ui/RegisterPasskey";
|
||||||
|
import RegisterU2F from "#/ui/RegisterU2F";
|
||||||
|
import UserAvatar from "#/ui/UserAvatar";
|
||||||
|
import { getMostRecentCookieWithLoginname } from "#/utils/cookies";
|
||||||
|
|
||||||
|
export default async function Page({
|
||||||
|
searchParams,
|
||||||
|
}: {
|
||||||
|
searchParams: Record<string | number | symbol, string | undefined>;
|
||||||
|
}) {
|
||||||
|
const { loginName, organization, authRequestId } = searchParams;
|
||||||
|
|
||||||
|
const sessionFactors = await loadSession(loginName);
|
||||||
|
|
||||||
|
async function loadSession(loginName?: string) {
|
||||||
|
const recent = await getMostRecentCookieWithLoginname(
|
||||||
|
loginName,
|
||||||
|
organization
|
||||||
|
);
|
||||||
|
return getSession(server, recent.id, recent.token).then((response) => {
|
||||||
|
if (response?.session) {
|
||||||
|
return response.session;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const title = "Use your passkey to confirm it's really you";
|
||||||
|
const description =
|
||||||
|
"Your device will ask for your fingerprint, face, or screen lock";
|
||||||
|
|
||||||
|
const branding = await getBrandingSettings(server, organization);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DynamicTheme branding={branding}>
|
||||||
|
<div className="flex flex-col items-center space-y-4">
|
||||||
|
<h1>{title}</h1>
|
||||||
|
|
||||||
|
{sessionFactors && (
|
||||||
|
<UserAvatar
|
||||||
|
loginName={loginName ?? sessionFactors.factors?.user?.loginName}
|
||||||
|
displayName={sessionFactors.factors?.user?.displayName}
|
||||||
|
showDropdown
|
||||||
|
searchParams={searchParams}
|
||||||
|
></UserAvatar>
|
||||||
|
)}
|
||||||
|
<p className="ztdl-p mb-6 block">{description}</p>
|
||||||
|
|
||||||
|
{!sessionFactors && (
|
||||||
|
<div className="py-4">
|
||||||
|
<Alert>
|
||||||
|
Could not get the context of the user. Make sure to enter the
|
||||||
|
username first or provide a loginName as searchParam.
|
||||||
|
</Alert>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{sessionFactors?.id && (
|
||||||
|
<RegisterU2F
|
||||||
|
sessionId={sessionFactors.id}
|
||||||
|
organization={organization}
|
||||||
|
authRequestId={authRequestId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DynamicTheme>
|
||||||
|
);
|
||||||
|
}
|
||||||
70
apps/login/app/api/otp/set/route.ts
Normal file
70
apps/login/app/api/otp/set/route.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import {
|
||||||
|
SessionCookie,
|
||||||
|
getMostRecentSessionCookie,
|
||||||
|
getSessionCookieById,
|
||||||
|
getSessionCookieByLoginName,
|
||||||
|
} from "#/utils/cookies";
|
||||||
|
import { setSessionAndUpdateCookie } from "#/utils/session";
|
||||||
|
import { Checks } from "@zitadel/server";
|
||||||
|
import { NextRequest, NextResponse, userAgent } from "next/server";
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
const body = await request.json();
|
||||||
|
|
||||||
|
if (body) {
|
||||||
|
const { loginName, sessionId, organization, authRequestId, code, method } =
|
||||||
|
body;
|
||||||
|
|
||||||
|
const recentPromise: Promise<SessionCookie> = sessionId
|
||||||
|
? getSessionCookieById(sessionId).catch((error) => {
|
||||||
|
return Promise.reject(error);
|
||||||
|
})
|
||||||
|
: loginName
|
||||||
|
? getSessionCookieByLoginName(loginName, organization).catch((error) => {
|
||||||
|
return Promise.reject(error);
|
||||||
|
})
|
||||||
|
: getMostRecentSessionCookie().catch((error) => {
|
||||||
|
return Promise.reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
return recentPromise
|
||||||
|
.then((recent) => {
|
||||||
|
const checks: Checks = {};
|
||||||
|
|
||||||
|
if (method === "time-based") {
|
||||||
|
checks.totp = {
|
||||||
|
code,
|
||||||
|
};
|
||||||
|
} else if (method === "sms") {
|
||||||
|
checks.otpSms = {
|
||||||
|
code,
|
||||||
|
};
|
||||||
|
} else if (method === "email") {
|
||||||
|
checks.otpEmail = {
|
||||||
|
code,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return setSessionAndUpdateCookie(
|
||||||
|
recent,
|
||||||
|
checks,
|
||||||
|
undefined,
|
||||||
|
authRequestId
|
||||||
|
).then((session) => {
|
||||||
|
return NextResponse.json({
|
||||||
|
sessionId: session.id,
|
||||||
|
factors: session.factors,
|
||||||
|
challenges: session.challenges,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
return NextResponse.json({ details: error }, { status: 500 });
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ details: "Request body is missing" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,10 @@
|
|||||||
import { server, deleteSession } from "#/lib/zitadel";
|
import {
|
||||||
|
server,
|
||||||
|
deleteSession,
|
||||||
|
getSession,
|
||||||
|
getUserByID,
|
||||||
|
listAuthenticationMethodTypes,
|
||||||
|
} from "#/lib/zitadel";
|
||||||
import {
|
import {
|
||||||
SessionCookie,
|
SessionCookie,
|
||||||
getMostRecentSessionCookie,
|
getMostRecentSessionCookie,
|
||||||
@@ -11,7 +17,7 @@ import {
|
|||||||
createSessionForIdpAndUpdateCookie,
|
createSessionForIdpAndUpdateCookie,
|
||||||
setSessionAndUpdateCookie,
|
setSessionAndUpdateCookie,
|
||||||
} from "#/utils/session";
|
} from "#/utils/session";
|
||||||
import { RequestChallenges } from "@zitadel/server";
|
import { Challenges, Checks, RequestChallenges } from "@zitadel/server";
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
@@ -67,11 +73,10 @@ export async function PUT(request: NextRequest) {
|
|||||||
loginName,
|
loginName,
|
||||||
sessionId,
|
sessionId,
|
||||||
organization,
|
organization,
|
||||||
password,
|
checks,
|
||||||
webAuthN,
|
|
||||||
authRequestId,
|
authRequestId,
|
||||||
|
challenges,
|
||||||
} = body;
|
} = body;
|
||||||
const challenges: RequestChallenges = body.challenges;
|
|
||||||
|
|
||||||
const recentPromise: Promise<SessionCookie> = sessionId
|
const recentPromise: Promise<SessionCookie> = sessionId
|
||||||
? getSessionCookieById(sessionId).catch((error) => {
|
? getSessionCookieById(sessionId).catch((error) => {
|
||||||
@@ -92,22 +97,63 @@ export async function PUT(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return recentPromise
|
return recentPromise
|
||||||
.then((recent) => {
|
.then(async (recent) => {
|
||||||
|
if (
|
||||||
|
challenges &&
|
||||||
|
(challenges.otpEmail === "" || challenges.otpSms === "")
|
||||||
|
) {
|
||||||
|
const sessionResponse = await getSession(
|
||||||
|
server,
|
||||||
|
recent.id,
|
||||||
|
recent.token
|
||||||
|
);
|
||||||
|
if (sessionResponse && sessionResponse.session?.factors?.user?.id) {
|
||||||
|
const userResponse = await getUserByID(
|
||||||
|
sessionResponse.session.factors.user.id
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
challenges.otpEmail === "" &&
|
||||||
|
userResponse.user?.human?.email?.email
|
||||||
|
) {
|
||||||
|
challenges.otpEmail = userResponse.user?.human?.email?.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
challenges.otpSms === "" &&
|
||||||
|
userResponse.user?.human?.phone?.phone
|
||||||
|
) {
|
||||||
|
challenges.otpSms = userResponse.user?.human?.phone?.phone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return setSessionAndUpdateCookie(
|
return setSessionAndUpdateCookie(
|
||||||
recent,
|
recent,
|
||||||
password,
|
checks,
|
||||||
webAuthN,
|
|
||||||
challenges,
|
challenges,
|
||||||
authRequestId
|
authRequestId
|
||||||
).then((session) => {
|
).then(async (session) => {
|
||||||
|
// if password, check if user has MFA methods
|
||||||
|
let authMethods;
|
||||||
|
if (checks && checks.password && session.factors?.user?.id) {
|
||||||
|
const response = await listAuthenticationMethodTypes(
|
||||||
|
session.factors?.user?.id
|
||||||
|
);
|
||||||
|
if (response.authMethodTypes && response.authMethodTypes.length) {
|
||||||
|
authMethods = response.authMethodTypes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
sessionId: session.id,
|
sessionId: session.id,
|
||||||
factors: session.factors,
|
factors: session.factors,
|
||||||
challenges: session.challenges,
|
challenges: session.challenges,
|
||||||
|
authMethods,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
return NextResponse.json({ details: error }, { status: 500 });
|
return NextResponse.json({ details: error }, { status: 500 });
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
46
apps/login/app/api/u2f/route.ts
Normal file
46
apps/login/app/api/u2f/route.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import {
|
||||||
|
createPasskeyRegistrationLink,
|
||||||
|
getSession,
|
||||||
|
registerPasskey,
|
||||||
|
registerU2F,
|
||||||
|
server,
|
||||||
|
} from "#/lib/zitadel";
|
||||||
|
import { getSessionCookieById } from "#/utils/cookies";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
const body = await request.json();
|
||||||
|
if (body) {
|
||||||
|
const { sessionId } = body;
|
||||||
|
|
||||||
|
const sessionCookie = await getSessionCookieById(sessionId);
|
||||||
|
|
||||||
|
const session = await getSession(
|
||||||
|
server,
|
||||||
|
sessionCookie.id,
|
||||||
|
sessionCookie.token
|
||||||
|
);
|
||||||
|
|
||||||
|
const domain: string = request.nextUrl.hostname;
|
||||||
|
|
||||||
|
const userId = session?.session?.factors?.user?.id;
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
|
return registerU2F(userId, domain)
|
||||||
|
.then((resp) => {
|
||||||
|
return NextResponse.json(resp);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("error on creating passkey registration link");
|
||||||
|
return NextResponse.json(error, { status: 500 });
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ details: "could not get session" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return NextResponse.json({}, { status: 400 });
|
||||||
|
}
|
||||||
|
}
|
||||||
50
apps/login/app/api/u2f/verify/route.ts
Normal file
50
apps/login/app/api/u2f/verify/route.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { getSession, server, verifyU2FRegistration } from "#/lib/zitadel";
|
||||||
|
import { getSessionCookieById } from "#/utils/cookies";
|
||||||
|
import { VerifyU2FRegistrationRequest } from "@zitadel/server";
|
||||||
|
import { NextRequest, NextResponse, userAgent } from "next/server";
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
const body = await request.json();
|
||||||
|
if (body) {
|
||||||
|
let { u2fId, passkeyName, publicKeyCredential, sessionId } = body;
|
||||||
|
|
||||||
|
if (!!!passkeyName) {
|
||||||
|
const { browser, device, os } = userAgent(request);
|
||||||
|
passkeyName = `${device.vendor ?? ""} ${device.model ?? ""}${
|
||||||
|
device.vendor || device.model ? ", " : ""
|
||||||
|
}${os.name}${os.name ? ", " : ""}${browser.name}`;
|
||||||
|
}
|
||||||
|
const sessionCookie = await getSessionCookieById(sessionId);
|
||||||
|
|
||||||
|
const session = await getSession(
|
||||||
|
server,
|
||||||
|
sessionCookie.id,
|
||||||
|
sessionCookie.token
|
||||||
|
);
|
||||||
|
|
||||||
|
const userId = session?.session?.factors?.user?.id;
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
|
const req: VerifyU2FRegistrationRequest = {
|
||||||
|
publicKeyCredential,
|
||||||
|
u2fId,
|
||||||
|
userId,
|
||||||
|
tokenName: passkeyName,
|
||||||
|
};
|
||||||
|
return verifyU2FRegistration(req)
|
||||||
|
.then((resp) => {
|
||||||
|
return NextResponse.json(resp);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
return NextResponse.json(error, { status: 500 });
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ details: "could not get session" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return NextResponse.json({}, { status: 400 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,6 +53,13 @@ export async function GET(request: NextRequest) {
|
|||||||
sessions = await loadSessions(ids);
|
sessions = await loadSessions(ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: before automatically redirecting to the callbackUrl, check if the session is still valid
|
||||||
|
* possible scenaio:
|
||||||
|
* mfa is required, session is not valid anymore (e.g. session expired, user logged out, etc.)
|
||||||
|
* to check for mfa for automatically selected session -> const response = await listAuthenticationMethodTypes(userId);
|
||||||
|
**/
|
||||||
|
|
||||||
if (authRequestId && sessionId) {
|
if (authRequestId && sessionId) {
|
||||||
console.log(
|
console.log(
|
||||||
`Login with session: ${sessionId} and authRequest: ${authRequestId}`
|
`Login with session: ${sessionId} and authRequest: ${authRequestId}`
|
||||||
@@ -86,7 +93,8 @@ export async function GET(request: NextRequest) {
|
|||||||
if (authRequestId) {
|
if (authRequestId) {
|
||||||
console.log(`Login with authRequest: ${authRequestId}`);
|
console.log(`Login with authRequest: ${authRequestId}`);
|
||||||
const { authRequest } = await getAuthRequest(server, { authRequestId });
|
const { authRequest } = await getAuthRequest(server, { authRequestId });
|
||||||
let organization;
|
|
||||||
|
let organization = "";
|
||||||
|
|
||||||
if (authRequest?.scope) {
|
if (authRequest?.scope) {
|
||||||
const orgScope = authRequest.scope.find((s: string) =>
|
const orgScope = authRequest.scope.find((s: string) =>
|
||||||
@@ -112,6 +120,18 @@ export async function GET(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const gotoAccounts = (): NextResponse<unknown> => {
|
||||||
|
const accountsUrl = new URL("/accounts", request.url);
|
||||||
|
if (authRequest?.id) {
|
||||||
|
accountsUrl.searchParams.set("authRequestId", authRequest?.id);
|
||||||
|
}
|
||||||
|
if (organization) {
|
||||||
|
accountsUrl.searchParams.set("organization", organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.redirect(accountsUrl);
|
||||||
|
};
|
||||||
|
|
||||||
if (authRequest && authRequest.prompt.includes(Prompt.PROMPT_CREATE)) {
|
if (authRequest && authRequest.prompt.includes(Prompt.PROMPT_CREATE)) {
|
||||||
const registerUrl = new URL("/register", request.url);
|
const registerUrl = new URL("/register", request.url);
|
||||||
if (authRequest?.id) {
|
if (authRequest?.id) {
|
||||||
@@ -128,15 +148,7 @@ export async function GET(request: NextRequest) {
|
|||||||
if (authRequest && sessions.length) {
|
if (authRequest && sessions.length) {
|
||||||
// if some accounts are available for selection and select_account is set
|
// if some accounts are available for selection and select_account is set
|
||||||
if (authRequest.prompt.includes(Prompt.PROMPT_SELECT_ACCOUNT)) {
|
if (authRequest.prompt.includes(Prompt.PROMPT_SELECT_ACCOUNT)) {
|
||||||
const accountsUrl = new URL("/accounts", request.url);
|
return gotoAccounts();
|
||||||
if (authRequest?.id) {
|
|
||||||
accountsUrl.searchParams.set("authRequestId", authRequest?.id);
|
|
||||||
}
|
|
||||||
if (organization) {
|
|
||||||
accountsUrl.searchParams.set("organization", organization);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.redirect(accountsUrl);
|
|
||||||
} else if (authRequest.prompt.includes(Prompt.PROMPT_LOGIN)) {
|
} else if (authRequest.prompt.includes(Prompt.PROMPT_LOGIN)) {
|
||||||
// if prompt is login
|
// if prompt is login
|
||||||
const loginNameUrl = new URL("/loginname", request.url);
|
const loginNameUrl = new URL("/loginname", request.url);
|
||||||
@@ -196,26 +208,25 @@ export async function GET(request: NextRequest) {
|
|||||||
sessionId: cookie?.id,
|
sessionId: cookie?.id,
|
||||||
sessionToken: cookie?.token,
|
sessionToken: cookie?.token,
|
||||||
};
|
};
|
||||||
const { callbackUrl } = await createCallback(server, {
|
try {
|
||||||
authRequestId,
|
const { callbackUrl } = await createCallback(server, {
|
||||||
session,
|
authRequestId,
|
||||||
});
|
session,
|
||||||
return NextResponse.redirect(callbackUrl);
|
});
|
||||||
} else {
|
if (callbackUrl) {
|
||||||
const accountsUrl = new URL("/accounts", request.url);
|
return NextResponse.redirect(callbackUrl);
|
||||||
accountsUrl.searchParams.set("authRequestId", authRequestId);
|
} else {
|
||||||
if (organization) {
|
return gotoAccounts();
|
||||||
accountsUrl.searchParams.set("organization", organization);
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return gotoAccounts();
|
||||||
}
|
}
|
||||||
return NextResponse.redirect(accountsUrl);
|
} else {
|
||||||
|
return gotoAccounts();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const accountsUrl = new URL("/accounts", request.url);
|
return gotoAccounts();
|
||||||
accountsUrl.searchParams.set("authRequestId", authRequestId);
|
|
||||||
if (organization) {
|
|
||||||
accountsUrl.searchParams.set("organization", organization);
|
|
||||||
}
|
|
||||||
return NextResponse.redirect(accountsUrl);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
24
apps/login/lib/server-actions.ts
Normal file
24
apps/login/lib/server-actions.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { getMostRecentCookieWithLoginname } from "#/utils/cookies";
|
||||||
|
import { getSession, server, verifyTOTPRegistration } from "./zitadel";
|
||||||
|
|
||||||
|
export async function verifyTOTP(
|
||||||
|
code: string,
|
||||||
|
loginName?: string,
|
||||||
|
organization?: string
|
||||||
|
) {
|
||||||
|
return getMostRecentCookieWithLoginname(loginName, organization)
|
||||||
|
.then((recent) => {
|
||||||
|
return getSession(server, recent.id, recent.token).then((response) => {
|
||||||
|
return { session: response?.session, token: recent.token };
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(({ session, token }) => {
|
||||||
|
if (session?.factors?.user?.id) {
|
||||||
|
return verifyTOTPRegistration(code, session.factors.user.id, token);
|
||||||
|
} else {
|
||||||
|
throw Error("No user id found in session.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,24 +1,34 @@
|
|||||||
|
import { VerifyU2FRegistrationRequest } from "@zitadel/server";
|
||||||
import {
|
import {
|
||||||
|
GetUserByIDResponse,
|
||||||
|
RegisterTOTPResponse,
|
||||||
|
VerifyTOTPRegistrationResponse,
|
||||||
|
} from "@zitadel/server";
|
||||||
|
import {
|
||||||
|
LegalAndSupportSettings,
|
||||||
|
PasswordComplexitySettings,
|
||||||
ZitadelServer,
|
ZitadelServer,
|
||||||
|
VerifyMyAuthFactorOTPResponse,
|
||||||
ZitadelServerOptions,
|
ZitadelServerOptions,
|
||||||
user,
|
user,
|
||||||
oidc,
|
oidc,
|
||||||
settings,
|
settings,
|
||||||
getServers,
|
getServers,
|
||||||
|
auth,
|
||||||
initializeServer,
|
initializeServer,
|
||||||
session,
|
session,
|
||||||
GetGeneralSettingsResponse,
|
GetGeneralSettingsResponse,
|
||||||
CreateSessionResponse,
|
CreateSessionResponse,
|
||||||
GetBrandingSettingsResponse,
|
GetBrandingSettingsResponse,
|
||||||
GetPasswordComplexitySettingsResponse,
|
GetPasswordComplexitySettingsResponse,
|
||||||
|
RegisterU2FResponse,
|
||||||
GetLegalAndSupportSettingsResponse,
|
GetLegalAndSupportSettingsResponse,
|
||||||
AddHumanUserResponse,
|
AddHumanUserResponse,
|
||||||
BrandingSettings,
|
BrandingSettings,
|
||||||
ListSessionsResponse,
|
ListSessionsResponse,
|
||||||
LegalAndSupportSettings,
|
|
||||||
PasswordComplexitySettings,
|
|
||||||
GetSessionResponse,
|
GetSessionResponse,
|
||||||
VerifyEmailResponse,
|
VerifyEmailResponse,
|
||||||
|
Checks,
|
||||||
SetSessionResponse,
|
SetSessionResponse,
|
||||||
SetSessionRequest,
|
SetSessionRequest,
|
||||||
ListUsersResponse,
|
ListUsersResponse,
|
||||||
@@ -39,9 +49,14 @@ import {
|
|||||||
CreateCallbackResponse,
|
CreateCallbackResponse,
|
||||||
RequestChallenges,
|
RequestChallenges,
|
||||||
TextQueryMethod,
|
TextQueryMethod,
|
||||||
|
ListHumanAuthFactorsResponse,
|
||||||
AddHumanUserRequest,
|
AddHumanUserRequest,
|
||||||
|
AddOTPEmailResponse,
|
||||||
|
AddOTPSMSResponse,
|
||||||
} from "@zitadel/server";
|
} from "@zitadel/server";
|
||||||
|
|
||||||
|
const SESSION_LIFETIME_S = 3000;
|
||||||
|
|
||||||
export const zitadelConfig: ZitadelServerOptions = {
|
export const zitadelConfig: ZitadelServerOptions = {
|
||||||
name: "zitadel login",
|
name: "zitadel login",
|
||||||
apiUrl: process.env.ZITADEL_API_URL ?? "",
|
apiUrl: process.env.ZITADEL_API_URL ?? "",
|
||||||
@@ -78,6 +93,65 @@ export async function getLoginSettings(
|
|||||||
.then((resp: GetLoginSettingsResponse) => resp.settings);
|
.then((resp: GetLoginSettingsResponse) => resp.settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function verifyMyAuthFactorOTP(
|
||||||
|
code: string
|
||||||
|
): Promise<VerifyMyAuthFactorOTPResponse> {
|
||||||
|
const authService = auth.getAuth(server);
|
||||||
|
return authService.verifyMyAuthFactorOTP({ code }, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addOTPEmail(
|
||||||
|
userId: string
|
||||||
|
): Promise<AddOTPEmailResponse | undefined> {
|
||||||
|
const userService = user.getUser(server);
|
||||||
|
return userService.addOTPEmail(
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addOTPSMS(
|
||||||
|
userId: string,
|
||||||
|
token?: string
|
||||||
|
): Promise<AddOTPSMSResponse | undefined> {
|
||||||
|
let userService;
|
||||||
|
if (token) {
|
||||||
|
const authConfig: ZitadelServerOptions = {
|
||||||
|
name: "zitadel login",
|
||||||
|
apiUrl: process.env.ZITADEL_API_URL ?? "",
|
||||||
|
token: token,
|
||||||
|
};
|
||||||
|
|
||||||
|
const sessionUser = initializeServer(authConfig);
|
||||||
|
userService = user.getUser(sessionUser);
|
||||||
|
} else {
|
||||||
|
userService = user.getUser(server);
|
||||||
|
}
|
||||||
|
return userService.addOTPSMS({ userId }, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function registerTOTP(
|
||||||
|
userId: string,
|
||||||
|
token?: string
|
||||||
|
): Promise<RegisterTOTPResponse | undefined> {
|
||||||
|
let userService;
|
||||||
|
if (token) {
|
||||||
|
const authConfig: ZitadelServerOptions = {
|
||||||
|
name: "zitadel login",
|
||||||
|
apiUrl: process.env.ZITADEL_API_URL ?? "",
|
||||||
|
token: token,
|
||||||
|
};
|
||||||
|
|
||||||
|
const sessionUser = initializeServer(authConfig);
|
||||||
|
userService = user.getUser(sessionUser);
|
||||||
|
} else {
|
||||||
|
userService = user.getUser(server);
|
||||||
|
}
|
||||||
|
return userService.registerTOTP({ userId }, {});
|
||||||
|
}
|
||||||
|
|
||||||
export async function getGeneralSettings(
|
export async function getGeneralSettings(
|
||||||
server: ZitadelServer
|
server: ZitadelServer
|
||||||
): Promise<string[] | undefined> {
|
): Promise<string[] | undefined> {
|
||||||
@@ -118,68 +192,23 @@ export async function getPasswordComplexitySettings(
|
|||||||
.then((resp: GetPasswordComplexitySettingsResponse) => resp.settings);
|
.then((resp: GetPasswordComplexitySettingsResponse) => resp.settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createSessionForLoginname(
|
export async function createSessionFromChecks(
|
||||||
server: ZitadelServer,
|
server: ZitadelServer,
|
||||||
loginName: string,
|
checks: Checks,
|
||||||
password: string | undefined,
|
|
||||||
challenges: RequestChallenges | undefined
|
challenges: RequestChallenges | undefined
|
||||||
): Promise<CreateSessionResponse | undefined> {
|
): Promise<CreateSessionResponse | undefined> {
|
||||||
const sessionService = session.getSession(server);
|
const sessionService = session.getSession(server);
|
||||||
return password
|
return sessionService.createSession(
|
||||||
? sessionService.createSession(
|
{
|
||||||
{
|
checks: checks,
|
||||||
checks: { user: { loginName }, password: { password } },
|
challenges,
|
||||||
challenges,
|
lifetime: {
|
||||||
lifetime: {
|
seconds: SESSION_LIFETIME_S,
|
||||||
seconds: 300,
|
nanos: 0,
|
||||||
nanos: 0,
|
},
|
||||||
},
|
},
|
||||||
},
|
{}
|
||||||
{}
|
);
|
||||||
)
|
|
||||||
: sessionService.createSession(
|
|
||||||
{
|
|
||||||
checks: { user: { loginName } },
|
|
||||||
challenges,
|
|
||||||
lifetime: {
|
|
||||||
seconds: 300,
|
|
||||||
nanos: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createSessionForUserId(
|
|
||||||
server: ZitadelServer,
|
|
||||||
userId: string,
|
|
||||||
password: string | undefined,
|
|
||||||
challenges: RequestChallenges | undefined
|
|
||||||
): Promise<CreateSessionResponse | undefined> {
|
|
||||||
const sessionService = session.getSession(server);
|
|
||||||
return password
|
|
||||||
? sessionService.createSession(
|
|
||||||
{
|
|
||||||
checks: { user: { userId }, password: { password } },
|
|
||||||
challenges,
|
|
||||||
lifetime: {
|
|
||||||
seconds: 300,
|
|
||||||
nanos: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
: sessionService.createSession(
|
|
||||||
{
|
|
||||||
checks: { user: { userId } },
|
|
||||||
challenges,
|
|
||||||
lifetime: {
|
|
||||||
seconds: 300,
|
|
||||||
nanos: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createSessionForUserIdAndIdpIntent(
|
export async function createSessionForUserIdAndIdpIntent(
|
||||||
@@ -208,9 +237,8 @@ export async function setSession(
|
|||||||
server: ZitadelServer,
|
server: ZitadelServer,
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
sessionToken: string,
|
sessionToken: string,
|
||||||
password: string | undefined,
|
challenges: RequestChallenges | undefined,
|
||||||
webAuthN: { credentialAssertionData: any } | undefined,
|
checks: Checks
|
||||||
challenges: RequestChallenges | undefined
|
|
||||||
): Promise<SetSessionResponse | undefined> {
|
): Promise<SetSessionResponse | undefined> {
|
||||||
const sessionService = session.getSession(server);
|
const sessionService = session.getSession(server);
|
||||||
|
|
||||||
@@ -222,12 +250,8 @@ export async function setSession(
|
|||||||
metadata: {},
|
metadata: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (password && payload.checks) {
|
if (checks && payload.checks) {
|
||||||
payload.checks.password = { password };
|
payload.checks = checks;
|
||||||
}
|
|
||||||
|
|
||||||
if (webAuthN && payload.checks) {
|
|
||||||
payload.checks.webAuthN = webAuthN;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return sessionService.setSession(payload, {});
|
return sessionService.setSession(payload, {});
|
||||||
@@ -296,6 +320,35 @@ export async function addHumanUser(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function verifyTOTPRegistration(
|
||||||
|
code: string,
|
||||||
|
userId: string,
|
||||||
|
token?: string
|
||||||
|
): Promise<VerifyTOTPRegistrationResponse> {
|
||||||
|
let userService;
|
||||||
|
if (token) {
|
||||||
|
const authConfig: ZitadelServerOptions = {
|
||||||
|
name: "zitadel login",
|
||||||
|
apiUrl: process.env.ZITADEL_API_URL ?? "",
|
||||||
|
token: token,
|
||||||
|
};
|
||||||
|
|
||||||
|
const sessionUser = initializeServer(authConfig);
|
||||||
|
userService = user.getUser(sessionUser);
|
||||||
|
} else {
|
||||||
|
userService = user.getUser(server);
|
||||||
|
}
|
||||||
|
return userService.verifyTOTPRegistration({ code, userId }, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUserByID(
|
||||||
|
userId: string
|
||||||
|
): Promise<GetUserByIDResponse> {
|
||||||
|
const userService = user.getUser(server);
|
||||||
|
|
||||||
|
return userService.getUserByID({ userId }, {});
|
||||||
|
}
|
||||||
|
|
||||||
export async function listUsers(
|
export async function listUsers(
|
||||||
userName: string,
|
userName: string,
|
||||||
organizationId: string
|
organizationId: string
|
||||||
@@ -423,16 +476,63 @@ export async function setEmail(
|
|||||||
* @returns the newly set email
|
* @returns the newly set email
|
||||||
*/
|
*/
|
||||||
export async function createPasskeyRegistrationLink(
|
export async function createPasskeyRegistrationLink(
|
||||||
userId: string
|
userId: string,
|
||||||
|
token?: string
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const userservice = user.getUser(server);
|
let userService;
|
||||||
|
if (token) {
|
||||||
|
const authConfig: ZitadelServerOptions = {
|
||||||
|
name: "zitadel login",
|
||||||
|
apiUrl: process.env.ZITADEL_API_URL ?? "",
|
||||||
|
token: token,
|
||||||
|
};
|
||||||
|
|
||||||
return userservice.createPasskeyRegistrationLink({
|
const sessionUser = initializeServer(authConfig);
|
||||||
|
userService = user.getUser(sessionUser);
|
||||||
|
} else {
|
||||||
|
userService = user.getUser(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
return userService.createPasskeyRegistrationLink({
|
||||||
userId,
|
userId,
|
||||||
returnCode: {},
|
returnCode: {},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param server
|
||||||
|
* @param userId the id of the user where the email should be set
|
||||||
|
* @param domain the domain on which the factor is registered
|
||||||
|
* @returns the newly set email
|
||||||
|
*/
|
||||||
|
export async function registerU2F(
|
||||||
|
userId: string,
|
||||||
|
domain: string
|
||||||
|
): Promise<RegisterU2FResponse> {
|
||||||
|
const userservice = user.getUser(server);
|
||||||
|
|
||||||
|
return userservice.registerU2F({
|
||||||
|
userId,
|
||||||
|
domain,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param server
|
||||||
|
* @param userId the id of the user where the email should be set
|
||||||
|
* @param domain the domain on which the factor is registered
|
||||||
|
* @returns the newly set email
|
||||||
|
*/
|
||||||
|
export async function verifyU2FRegistration(
|
||||||
|
request: VerifyU2FRegistrationRequest
|
||||||
|
): Promise<any> {
|
||||||
|
const userservice = user.getUser(server);
|
||||||
|
|
||||||
|
return userservice.verifyU2FRegistration(request, {});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param server
|
* @param server
|
||||||
|
|||||||
@@ -40,10 +40,12 @@
|
|||||||
"@zitadel/react": "workspace:*",
|
"@zitadel/react": "workspace:*",
|
||||||
"@zitadel/server": "workspace:*",
|
"@zitadel/server": "workspace:*",
|
||||||
"clsx": "1.2.1",
|
"clsx": "1.2.1",
|
||||||
|
"copy-to-clipboard": "^3.3.3",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"next": "13.4.12",
|
"next": "13.4.12",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"nice-grpc": "2.0.1",
|
"nice-grpc": "2.0.1",
|
||||||
|
"qrcode.react": "^3.1.0",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-hook-form": "7.39.5",
|
"react-hook-form": "7.39.5",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ let colors = {
|
|||||||
text: { light: { contrast: {} }, dark: { contrast: {} } },
|
text: { light: { contrast: {} }, dark: { contrast: {} } },
|
||||||
link: { light: { contrast: {} }, dark: { contrast: {} } },
|
link: { light: { contrast: {} }, dark: { contrast: {} } },
|
||||||
};
|
};
|
||||||
|
|
||||||
const shades = [
|
const shades = [
|
||||||
"50",
|
"50",
|
||||||
"100",
|
"100",
|
||||||
@@ -49,7 +50,51 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
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: {
|
animation: {
|
||||||
shake: "shake .8s cubic-bezier(.36,.07,.19,.97) both;",
|
shake: "shake .8s cubic-bezier(.36,.07,.19,.97) both;",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export default function Alert({ children, type = AlertType.ALERT }: Props) {
|
|||||||
{type === AlertType.INFO && (
|
{type === AlertType.INFO && (
|
||||||
<InformationCircleIcon className="flex-shrink-0 h-5 w-5 mr-2 ml-2" />
|
<InformationCircleIcon className="flex-shrink-0 h-5 w-5 mr-2 ml-2" />
|
||||||
)}
|
)}
|
||||||
<span className="text-sm">{children}</span>
|
<span className="text-sm w-full ">{children}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
205
apps/login/ui/AuthMethods.tsx
Normal file
205
apps/login/ui/AuthMethods.tsx
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { BadgeState, StateBadge } from "./StateBadge";
|
||||||
|
import { CheckIcon } from "@heroicons/react/24/solid";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
const cardClasses = (alreadyAdded: boolean) =>
|
||||||
|
clsx(
|
||||||
|
"relative 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 cursor-default"
|
||||||
|
: "hover:shadow-lg hover:dark:bg-white/10"
|
||||||
|
);
|
||||||
|
|
||||||
|
const LinkWrapper = ({
|
||||||
|
alreadyAdded,
|
||||||
|
children,
|
||||||
|
link,
|
||||||
|
}: {
|
||||||
|
alreadyAdded: boolean;
|
||||||
|
children: ReactNode;
|
||||||
|
link: string;
|
||||||
|
}) => {
|
||||||
|
return !alreadyAdded ? (
|
||||||
|
<Link href={link} className={cardClasses(alreadyAdded)}>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<div className={cardClasses(alreadyAdded)}>{children}</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TOTP = (alreadyAdded: boolean, link: string) => {
|
||||||
|
return (
|
||||||
|
<LinkWrapper alreadyAdded={alreadyAdded} link={link}>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"font-medium flex items-center",
|
||||||
|
alreadyAdded ? "opacity-50" : ""
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
{alreadyAdded && (
|
||||||
|
<>
|
||||||
|
<Setup />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</LinkWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const U2F = (alreadyAdded: boolean, link: string) => {
|
||||||
|
return (
|
||||||
|
<LinkWrapper alreadyAdded={alreadyAdded} link={link}>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"font-medium flex items-center",
|
||||||
|
alreadyAdded ? "" : ""
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
{alreadyAdded && (
|
||||||
|
<>
|
||||||
|
<Setup />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</LinkWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EMAIL = (alreadyAdded: boolean, link: string) => {
|
||||||
|
return (
|
||||||
|
<LinkWrapper alreadyAdded={alreadyAdded} link={link}>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"font-medium flex items-center",
|
||||||
|
alreadyAdded ? "" : ""
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
{alreadyAdded && (
|
||||||
|
<>
|
||||||
|
<Setup />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</LinkWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SMS = (alreadyAdded: boolean, link: string) => {
|
||||||
|
return (
|
||||||
|
<LinkWrapper alreadyAdded={alreadyAdded} link={link}>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"font-medium flex items-center",
|
||||||
|
alreadyAdded ? "" : ""
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
{alreadyAdded && (
|
||||||
|
<>
|
||||||
|
<Setup />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</LinkWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function Setup() {
|
||||||
|
return (
|
||||||
|
<div className="transform absolute right-2 top-0">
|
||||||
|
<StateBadge evenPadding={true} state={BadgeState.Success}>
|
||||||
|
<CheckIcon className="w-4 h-4" />
|
||||||
|
</StateBadge>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
58
apps/login/ui/ChooseSecondFactor.tsx
Normal file
58
apps/login/ui/ChooseSecondFactor.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AuthenticationMethodType,
|
||||||
|
LoginSettings,
|
||||||
|
login,
|
||||||
|
} from "@zitadel/server";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { BadgeState, StateBadge } from "./StateBadge";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { CheckIcon } from "@heroicons/react/24/outline";
|
||||||
|
import { EMAIL, SMS, TOTP, U2F } from "./AuthMethods";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
loginName?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
authRequestId?: string;
|
||||||
|
organization?: string;
|
||||||
|
userMethods: AuthenticationMethodType[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ChooseSecondFactor({
|
||||||
|
loginName,
|
||||||
|
sessionId,
|
||||||
|
authRequestId,
|
||||||
|
organization,
|
||||||
|
userMethods,
|
||||||
|
}: Props) {
|
||||||
|
const params = new URLSearchParams({});
|
||||||
|
|
||||||
|
if (loginName) {
|
||||||
|
params.append("loginName", loginName);
|
||||||
|
}
|
||||||
|
if (sessionId) {
|
||||||
|
params.append("sessionId", sessionId);
|
||||||
|
}
|
||||||
|
if (authRequestId) {
|
||||||
|
params.append("authRequestId", authRequestId);
|
||||||
|
}
|
||||||
|
if (organization) {
|
||||||
|
params.append("organization", organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 gap-5 w-full pt-4">
|
||||||
|
{userMethods.map((method, i) => {
|
||||||
|
return (
|
||||||
|
<div key={"method-" + i}>
|
||||||
|
{method === 4 && TOTP(false, "/otp/time-based?" + params)}
|
||||||
|
{method === 5 && U2F(false, "/u2f?" + params)}
|
||||||
|
{method === 7 && EMAIL(false, "/otp/email?" + params)}
|
||||||
|
{method === 6 && SMS(false, "/otp/sms?" + params)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
62
apps/login/ui/ChooseSecondFactorToSetup.tsx
Normal file
62
apps/login/ui/ChooseSecondFactorToSetup.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { AuthenticationMethodType, LoginSettings } from "@zitadel/server";
|
||||||
|
import { EMAIL, SMS, TOTP, U2F } from "./AuthMethods";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
loginName?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
authRequestId?: string;
|
||||||
|
organization?: string;
|
||||||
|
loginSettings: LoginSettings;
|
||||||
|
userMethods: AuthenticationMethodType[];
|
||||||
|
checkAfter: boolean;
|
||||||
|
phoneVerified: boolean;
|
||||||
|
emailVerified: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ChooseSecondFactorToSetup({
|
||||||
|
loginName,
|
||||||
|
sessionId,
|
||||||
|
authRequestId,
|
||||||
|
organization,
|
||||||
|
loginSettings,
|
||||||
|
userMethods,
|
||||||
|
checkAfter,
|
||||||
|
phoneVerified,
|
||||||
|
emailVerified,
|
||||||
|
}: Props) {
|
||||||
|
const params = new URLSearchParams({});
|
||||||
|
|
||||||
|
if (loginName) {
|
||||||
|
params.append("loginName", loginName);
|
||||||
|
}
|
||||||
|
if (sessionId) {
|
||||||
|
params.append("sessionId", sessionId);
|
||||||
|
}
|
||||||
|
if (authRequestId) {
|
||||||
|
params.append("authRequestId", authRequestId);
|
||||||
|
}
|
||||||
|
if (organization) {
|
||||||
|
params.append("organization", organization);
|
||||||
|
}
|
||||||
|
if (checkAfter) {
|
||||||
|
params.append("checkAfter", "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 gap-5 w-full pt-4">
|
||||||
|
{loginSettings.secondFactors.map((factor, i) => {
|
||||||
|
return factor === 1
|
||||||
|
? TOTP(userMethods.includes(4), "/otp/time-based/set?" + params)
|
||||||
|
: factor === 2
|
||||||
|
? U2F(userMethods.includes(5), "/u2f/set?" + params)
|
||||||
|
: factor === 3 && emailVerified
|
||||||
|
? EMAIL(userMethods.includes(7), "/otp/email/set?" + params)
|
||||||
|
: factor === 4 && phoneVerified
|
||||||
|
? SMS(userMethods.includes(6), "/otp/sms/set?" + params)
|
||||||
|
: null;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
41
apps/login/ui/CopyToClipboard.tsx
Normal file
41
apps/login/ui/CopyToClipboard.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ClipboardDocumentCheckIcon,
|
||||||
|
ClipboardIcon,
|
||||||
|
} from "@heroicons/react/20/solid";
|
||||||
|
import copy from "copy-to-clipboard";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function CopyToClipboard({ value }: Props) {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (copied) {
|
||||||
|
copy(value);
|
||||||
|
const to = setTimeout(setCopied, 1000, false);
|
||||||
|
return () => clearTimeout(to);
|
||||||
|
}
|
||||||
|
}, [copied]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-row items-center px-2">
|
||||||
|
<button
|
||||||
|
id="tooltip-ctc"
|
||||||
|
type="button"
|
||||||
|
className=" text-primary-light-500 dark:text-primary-dark-500"
|
||||||
|
onClick={() => setCopied(true)}
|
||||||
|
>
|
||||||
|
{!copied ? (
|
||||||
|
<ClipboardIcon className="h-5 w-5" />
|
||||||
|
) : (
|
||||||
|
<ClipboardDocumentCheckIcon className="h-5 w-5" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
245
apps/login/ui/LoginOTP.tsx
Normal file
245
apps/login/ui/LoginOTP.tsx
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
"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, { AlertType } from "./Alert";
|
||||||
|
import { Spinner } from "./Spinner";
|
||||||
|
import { Checks } from "@zitadel/server";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { TextInput } from "./Input";
|
||||||
|
import { Challenges } from "@zitadel/server";
|
||||||
|
|
||||||
|
// either loginName or sessionId must be provided
|
||||||
|
type Props = {
|
||||||
|
loginName?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
authRequestId?: string;
|
||||||
|
organization?: string;
|
||||||
|
method: string;
|
||||||
|
code?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Inputs = {
|
||||||
|
code: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function LoginOTP({
|
||||||
|
loginName,
|
||||||
|
sessionId,
|
||||||
|
authRequestId,
|
||||||
|
organization,
|
||||||
|
method,
|
||||||
|
code,
|
||||||
|
}: Props) {
|
||||||
|
const [error, setError] = useState<string>("");
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const initialized = useRef(false);
|
||||||
|
|
||||||
|
const { register, handleSubmit, formState } = useForm<Inputs>({
|
||||||
|
mode: "onBlur",
|
||||||
|
defaultValues: {
|
||||||
|
code: code ? code : "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!initialized.current && ["email", "sms"].includes(method)) {
|
||||||
|
initialized.current = true;
|
||||||
|
setLoading(true);
|
||||||
|
updateSessionForOTPChallenge()
|
||||||
|
.then((response) => {
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
setError(error);
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function updateSessionForOTPChallenge() {
|
||||||
|
const challenges: Challenges = {};
|
||||||
|
|
||||||
|
if (method === "email") {
|
||||||
|
challenges.otpEmail = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === "sms") {
|
||||||
|
challenges.otpSms = "";
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
const res = await fetch("/api/session", {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
loginName,
|
||||||
|
sessionId,
|
||||||
|
organization,
|
||||||
|
challenges,
|
||||||
|
authRequestId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
if (!res.ok) {
|
||||||
|
const error = await res.json();
|
||||||
|
throw error.details.details;
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitCode(values: Inputs, organization?: string) {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
let body: any = {
|
||||||
|
code: values.code,
|
||||||
|
method,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (organization) {
|
||||||
|
body.organization = organization;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authRequestId) {
|
||||||
|
body.authRequestId = authRequestId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const checks: Checks = {};
|
||||||
|
if (method === "sms") {
|
||||||
|
checks.otpSms = { code: values.code };
|
||||||
|
}
|
||||||
|
if (method === "email") {
|
||||||
|
checks.otpEmail = { code: values.code };
|
||||||
|
}
|
||||||
|
if (method === "time-based") {
|
||||||
|
checks.totp = { code: values.code };
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch("/api/session", {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
loginName,
|
||||||
|
sessionId,
|
||||||
|
organization,
|
||||||
|
checks,
|
||||||
|
authRequestId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
if (!res.ok) {
|
||||||
|
const response = await res.json();
|
||||||
|
|
||||||
|
setError(response.details.details ?? "An internal error occurred");
|
||||||
|
return Promise.reject(
|
||||||
|
response.details.details ?? "An internal error occurred"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCodeAndContinue(values: Inputs, organization?: string) {
|
||||||
|
return submitCode(values, organization).then((response) => {
|
||||||
|
if (authRequestId && response && response.sessionId) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
sessionId: response.sessionId,
|
||||||
|
authRequest: authRequestId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (organization) {
|
||||||
|
params.append("organization", organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
return router.push(`/login?` + params);
|
||||||
|
} else {
|
||||||
|
const params = new URLSearchParams(
|
||||||
|
authRequestId
|
||||||
|
? {
|
||||||
|
loginName: response.factors.user.loginName,
|
||||||
|
authRequestId,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
loginName: response.factors.user.loginName,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (organization) {
|
||||||
|
params.append("organization", organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
return router.push(`/signedin?` + params);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { errors } = formState;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className="w-full">
|
||||||
|
{["email", "sms"].includes(method) && (
|
||||||
|
<Alert type={AlertType.INFO}>
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<span className="flex-1 mr-auto text-left">
|
||||||
|
Did not get the Code?
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
aria-label="Resend OTP Code"
|
||||||
|
disabled={loading}
|
||||||
|
className="ml-4 text-primary-light-500 dark:text-primary-dark-500 hover:dark:text-primary-dark-400 hover:text-primary-light-400 cursor-pointer disabled:cursor-default disabled:text-gray-400 dark:disabled:text-gray-700"
|
||||||
|
onClick={() => {
|
||||||
|
setLoading(true);
|
||||||
|
updateSessionForOTPChallenge()
|
||||||
|
.then((response) => {
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
setError(error);
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Resend
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
<div className="mt-4">
|
||||||
|
<TextInput
|
||||||
|
type="text"
|
||||||
|
{...register("code", { required: "This field is required" })}
|
||||||
|
label="Code"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="py-4">
|
||||||
|
<Alert>{error}</Alert>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-8 flex w-full flex-row items-center">
|
||||||
|
<span className="flex-grow"></span>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="self-end"
|
||||||
|
variant={ButtonVariants.Primary}
|
||||||
|
disabled={loading || !formState.isValid}
|
||||||
|
onClick={handleSubmit((e) => setCodeAndContinue(e, organization))}
|
||||||
|
>
|
||||||
|
{loading && <Spinner className="h-5 w-5 mr-2" />}
|
||||||
|
continue
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { coerceToArrayBuffer, coerceToBase64Url } from "#/utils/base64";
|
|||||||
import { Button, ButtonVariants } from "./Button";
|
import { Button, ButtonVariants } from "./Button";
|
||||||
import Alert from "./Alert";
|
import Alert from "./Alert";
|
||||||
import { Spinner } from "./Spinner";
|
import { Spinner } from "./Spinner";
|
||||||
|
import { Checks } from "@zitadel/server";
|
||||||
|
|
||||||
// either loginName or sessionId must be provided
|
// either loginName or sessionId must be provided
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -13,6 +14,7 @@ type Props = {
|
|||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
authRequestId?: string;
|
authRequestId?: string;
|
||||||
altPassword: boolean;
|
altPassword: boolean;
|
||||||
|
login?: boolean;
|
||||||
organization?: string;
|
organization?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -22,6 +24,7 @@ export default function LoginPasskey({
|
|||||||
authRequestId,
|
authRequestId,
|
||||||
altPassword,
|
altPassword,
|
||||||
organization,
|
organization,
|
||||||
|
login = true,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [error, setError] = useState<string>("");
|
const [error, setError] = useState<string>("");
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
@@ -30,6 +33,7 @@ export default function LoginPasskey({
|
|||||||
|
|
||||||
const initialized = useRef(false);
|
const initialized = useRef(false);
|
||||||
|
|
||||||
|
// TODO: move this to server side
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!initialized.current) {
|
if (!initialized.current) {
|
||||||
initialized.current = true;
|
initialized.current = true;
|
||||||
@@ -60,7 +64,9 @@ export default function LoginPasskey({
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
async function updateSessionForChallenge() {
|
async function updateSessionForChallenge(
|
||||||
|
userVerificationRequirement: number = login ? 1 : 3
|
||||||
|
) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const res = await fetch("/api/session", {
|
const res = await fetch("/api/session", {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
@@ -74,7 +80,11 @@ export default function LoginPasskey({
|
|||||||
challenges: {
|
challenges: {
|
||||||
webAuthN: {
|
webAuthN: {
|
||||||
domain: "",
|
domain: "",
|
||||||
userVerificationRequirement: 1,
|
// USER_VERIFICATION_REQUIREMENT_UNSPECIFIED = 0;
|
||||||
|
// USER_VERIFICATION_REQUIREMENT_REQUIRED = 1; - passkey login
|
||||||
|
// USER_VERIFICATION_REQUIREMENT_PREFERRED = 2;
|
||||||
|
// USER_VERIFICATION_REQUIREMENT_DISCOURAGED = 3; - mfa
|
||||||
|
userVerificationRequirement: userVerificationRequirement,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
authRequestId,
|
authRequestId,
|
||||||
@@ -100,7 +110,9 @@ export default function LoginPasskey({
|
|||||||
loginName,
|
loginName,
|
||||||
sessionId,
|
sessionId,
|
||||||
organization,
|
organization,
|
||||||
webAuthN: { credentialAssertionData: data },
|
checks: {
|
||||||
|
webAuthN: { credentialAssertionData: data },
|
||||||
|
} as Checks,
|
||||||
authRequestId,
|
authRequestId,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,12 +7,19 @@ import { useForm } from "react-hook-form";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Spinner } from "./Spinner";
|
import { Spinner } from "./Spinner";
|
||||||
import Alert from "./Alert";
|
import Alert from "./Alert";
|
||||||
|
import {
|
||||||
|
LoginSettings,
|
||||||
|
AuthFactor,
|
||||||
|
Checks,
|
||||||
|
AuthenticationMethodType,
|
||||||
|
} from "@zitadel/server";
|
||||||
|
|
||||||
type Inputs = {
|
type Inputs = {
|
||||||
password: string;
|
password: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
loginSettings: LoginSettings | undefined;
|
||||||
loginName?: string;
|
loginName?: string;
|
||||||
organization?: string;
|
organization?: string;
|
||||||
authRequestId?: string;
|
authRequestId?: string;
|
||||||
@@ -21,6 +28,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function PasswordForm({
|
export default function PasswordForm({
|
||||||
|
loginSettings,
|
||||||
loginName,
|
loginName,
|
||||||
organization,
|
organization,
|
||||||
authRequestId,
|
authRequestId,
|
||||||
@@ -49,7 +57,9 @@ export default function PasswordForm({
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
loginName,
|
loginName,
|
||||||
organization,
|
organization,
|
||||||
password: values.password,
|
checks: {
|
||||||
|
password: { password: values.password },
|
||||||
|
} as Checks,
|
||||||
authRequestId,
|
authRequestId,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -58,15 +68,76 @@ export default function PasswordForm({
|
|||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
setError(response.details);
|
console.log(response.details.details);
|
||||||
|
setError(response.details?.details ?? "Could not verify password");
|
||||||
return Promise.reject(response.details);
|
return Promise.reject(response.details);
|
||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
function submitPasswordAndContinue(value: Inputs): Promise<boolean | void> {
|
function submitPasswordAndContinue(value: Inputs): Promise<boolean | void> {
|
||||||
return submitPassword(value).then((resp: any) => {
|
return submitPassword(value).then((resp) => {
|
||||||
if (
|
// if user has mfa -> /otp/[method] or /u2f
|
||||||
|
// if mfa is forced and user has no mfa -> /mfa/set
|
||||||
|
// if no passwordless -> /passkey/add
|
||||||
|
|
||||||
|
// exclude password
|
||||||
|
const availableSecondFactors = resp.authMethods?.filter(
|
||||||
|
(m: AuthenticationMethodType) => m !== 1
|
||||||
|
);
|
||||||
|
if (availableSecondFactors.length == 1) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
loginName: resp.factors.user.loginName,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (authRequestId) {
|
||||||
|
params.append("authRequestId", authRequestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (organization) {
|
||||||
|
params.append("organization", organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
const factor = availableSecondFactors[0];
|
||||||
|
if (factor === 4) {
|
||||||
|
return router.push(`/otp/time-based?` + params);
|
||||||
|
} else if (factor === 6) {
|
||||||
|
return router.push(`/otp/sms?` + params);
|
||||||
|
} else if (factor === 7) {
|
||||||
|
return router.push(`/otp/email?` + params);
|
||||||
|
} else if (factor === 5) {
|
||||||
|
return router.push(`/u2f?` + params);
|
||||||
|
}
|
||||||
|
} else if (availableSecondFactors.length >= 1) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
loginName: resp.factors.user.loginName,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (authRequestId) {
|
||||||
|
params.append("authRequestId", authRequestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (organization) {
|
||||||
|
params.append("organization", organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
return router.push(`/mfa?` + params);
|
||||||
|
} else if (loginSettings?.forceMfa && !availableSecondFactors.length) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
loginName: resp.factors.user.loginName,
|
||||||
|
checkAfter: "true", // this defines if the check is directly made after the setup
|
||||||
|
});
|
||||||
|
|
||||||
|
if (authRequestId) {
|
||||||
|
params.append("authRequestId", authRequestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (organization) {
|
||||||
|
params.append("organization", organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
return router.push(`/mfa/set?` + params);
|
||||||
|
} 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
|
||||||
promptPasswordless && // if explicitly prompted due policy
|
promptPasswordless && // if explicitly prompted due policy
|
||||||
@@ -77,47 +148,48 @@ export default function PasswordForm({
|
|||||||
promptPasswordless: "true",
|
promptPasswordless: "true",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (authRequestId) {
|
||||||
|
params.append("authRequestId", authRequestId);
|
||||||
|
}
|
||||||
|
|
||||||
if (organization) {
|
if (organization) {
|
||||||
params.append("organization", organization);
|
params.append("organization", organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
return router.push(`/passkey/add?` + params);
|
return router.push(`/passkey/add?` + params);
|
||||||
} else {
|
} else if (authRequestId && resp && resp.sessionId) {
|
||||||
if (authRequestId && resp && resp.sessionId) {
|
const params = new URLSearchParams({
|
||||||
const params = new URLSearchParams({
|
sessionId: resp.sessionId,
|
||||||
sessionId: resp.sessionId,
|
authRequest: authRequestId,
|
||||||
authRequest: authRequestId,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
if (organization) {
|
if (organization) {
|
||||||
params.append("organization", organization);
|
params.append("organization", organization);
|
||||||
}
|
|
||||||
|
|
||||||
return router.push(`/login?` + params);
|
|
||||||
} else {
|
|
||||||
const params = new URLSearchParams(
|
|
||||||
authRequestId
|
|
||||||
? {
|
|
||||||
loginName: resp.factors.user.loginName,
|
|
||||||
authRequestId,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
loginName: resp.factors.user.loginName,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (organization) {
|
|
||||||
params.append("organization", organization);
|
|
||||||
}
|
|
||||||
|
|
||||||
return router.push(`/signedin?` + params);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return router.push(`/login?` + params);
|
||||||
|
} else {
|
||||||
|
// without OIDC flow
|
||||||
|
const params = new URLSearchParams(
|
||||||
|
authRequestId
|
||||||
|
? {
|
||||||
|
loginName: resp.factors.user.loginName,
|
||||||
|
authRequestId,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
loginName: resp.factors.user.loginName,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (organization) {
|
||||||
|
params.append("organization", organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
return router.push(`/signedin?` + params);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { errors } = formState;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="w-full">
|
<form className="w-full">
|
||||||
<div className={`${error && "transform-gpu animate-shake"}`}>
|
<div className={`${error && "transform-gpu animate-shake"}`}>
|
||||||
|
|||||||
@@ -200,14 +200,17 @@ export default function RegisterPasskey({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (authRequestId) {
|
if (authRequestId) {
|
||||||
params.set("authRequestId", authRequestId);
|
params.set("authRequest", authRequestId);
|
||||||
|
}
|
||||||
|
if (sessionId) {
|
||||||
|
params.set("sessionId", sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (organization) {
|
if (organization) {
|
||||||
params.set("organization", organization);
|
params.set("organization", organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
router.push("/accounts?" + params);
|
router.push("/login?" + params);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
skip
|
skip
|
||||||
|
|||||||
216
apps/login/ui/RegisterU2F.tsx
Normal file
216
apps/login/ui/RegisterU2F.tsx
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Button, ButtonVariants } from "./Button";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { Spinner } from "./Spinner";
|
||||||
|
import Alert from "./Alert";
|
||||||
|
import { RegisterU2FResponse } from "@zitadel/server";
|
||||||
|
import { coerceToArrayBuffer, coerceToBase64Url } from "#/utils/base64";
|
||||||
|
type Inputs = {};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
sessionId: string;
|
||||||
|
authRequestId?: string;
|
||||||
|
organization?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RegisterU2F({
|
||||||
|
sessionId,
|
||||||
|
organization,
|
||||||
|
authRequestId,
|
||||||
|
}: Props) {
|
||||||
|
const { register, handleSubmit, formState } = useForm<Inputs>({
|
||||||
|
mode: "onBlur",
|
||||||
|
});
|
||||||
|
|
||||||
|
const [error, setError] = useState<string>("");
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
async function submitRegister() {
|
||||||
|
setError("");
|
||||||
|
setLoading(true);
|
||||||
|
const res = await fetch("/api/u2f", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
sessionId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await res.json();
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
if (!res.ok) {
|
||||||
|
setError(response.details);
|
||||||
|
return Promise.reject(response.details);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitVerify(
|
||||||
|
u2fId: string,
|
||||||
|
passkeyName: string,
|
||||||
|
publicKeyCredential: any,
|
||||||
|
sessionId: string
|
||||||
|
) {
|
||||||
|
setLoading(true);
|
||||||
|
const res = await fetch("/api/u2f/verify", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
u2fId,
|
||||||
|
passkeyName,
|
||||||
|
publicKeyCredential,
|
||||||
|
sessionId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await res.json();
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
if (!res.ok) {
|
||||||
|
setError(response.details);
|
||||||
|
return Promise.reject(response.details);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitRegisterAndContinue(value: Inputs): Promise<boolean | void> {
|
||||||
|
return submitRegister().then((resp: RegisterU2FResponse) => {
|
||||||
|
const u2fId = resp.u2fId;
|
||||||
|
|
||||||
|
if (
|
||||||
|
resp.publicKeyCredentialCreationOptions &&
|
||||||
|
resp.publicKeyCredentialCreationOptions.publicKey
|
||||||
|
) {
|
||||||
|
resp.publicKeyCredentialCreationOptions.publicKey.challenge =
|
||||||
|
coerceToArrayBuffer(
|
||||||
|
resp.publicKeyCredentialCreationOptions.publicKey.challenge,
|
||||||
|
"challenge"
|
||||||
|
);
|
||||||
|
resp.publicKeyCredentialCreationOptions.publicKey.user.id =
|
||||||
|
coerceToArrayBuffer(
|
||||||
|
resp.publicKeyCredentialCreationOptions.publicKey.user.id,
|
||||||
|
"userid"
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
resp.publicKeyCredentialCreationOptions.publicKey.excludeCredentials
|
||||||
|
) {
|
||||||
|
resp.publicKeyCredentialCreationOptions.publicKey.excludeCredentials.map(
|
||||||
|
(cred: any) => {
|
||||||
|
cred.id = coerceToArrayBuffer(
|
||||||
|
cred.id as string,
|
||||||
|
"excludeCredentials.id"
|
||||||
|
);
|
||||||
|
return cred;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.credentials
|
||||||
|
.create(resp.publicKeyCredentialCreationOptions)
|
||||||
|
.then((resp) => {
|
||||||
|
if (
|
||||||
|
resp &&
|
||||||
|
(resp as any).response.attestationObject &&
|
||||||
|
(resp as any).response.clientDataJSON &&
|
||||||
|
(resp as any).rawId
|
||||||
|
) {
|
||||||
|
const attestationObject = (resp as any).response
|
||||||
|
.attestationObject;
|
||||||
|
const clientDataJSON = (resp as any).response.clientDataJSON;
|
||||||
|
const rawId = (resp as any).rawId;
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
id: resp.id,
|
||||||
|
rawId: coerceToBase64Url(rawId, "rawId"),
|
||||||
|
type: resp.type,
|
||||||
|
response: {
|
||||||
|
attestationObject: coerceToBase64Url(
|
||||||
|
attestationObject,
|
||||||
|
"attestationObject"
|
||||||
|
),
|
||||||
|
clientDataJSON: coerceToBase64Url(
|
||||||
|
clientDataJSON,
|
||||||
|
"clientDataJSON"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return submitVerify(u2fId, "", data, sessionId).then(() => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
if (organization) {
|
||||||
|
params.set("organization", organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authRequestId) {
|
||||||
|
params.set("authRequestId", authRequestId);
|
||||||
|
params.set("sessionId", sessionId);
|
||||||
|
// params.set("altPassword", ${false}); // without setting altPassword this does not allow password
|
||||||
|
// params.set("loginName", resp.loginName);
|
||||||
|
|
||||||
|
router.push("/u2f?" + params);
|
||||||
|
} else {
|
||||||
|
router.push("/accounts?" + params);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setLoading(false);
|
||||||
|
setError("An error on registering passkey");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
setLoading(false);
|
||||||
|
setError(error);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { errors } = formState;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className="w-full">
|
||||||
|
{error && (
|
||||||
|
<div className="py-4">
|
||||||
|
<Alert>{error}</Alert>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-8 flex w-full flex-row items-center">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant={ButtonVariants.Secondary}
|
||||||
|
onClick={() => router.back()}
|
||||||
|
>
|
||||||
|
back
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<span className="flex-grow"></span>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="self-end"
|
||||||
|
variant={ButtonVariants.Primary}
|
||||||
|
disabled={loading || !formState.isValid}
|
||||||
|
onClick={handleSubmit(submitRegisterAndContinue)}
|
||||||
|
>
|
||||||
|
{loading && <Spinner className="h-5 w-5 mr-2" />}
|
||||||
|
continue
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
40
apps/login/ui/StateBadge.tsx
Normal file
40
apps/login/ui/StateBadge.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
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;
|
||||||
|
evenPadding?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBadgeClasses = (state: BadgeState, evenPadding: boolean) =>
|
||||||
|
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,
|
||||||
|
"p-[2px]": evenPadding,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function StateBadge({
|
||||||
|
state = BadgeState.Success,
|
||||||
|
evenPadding = false,
|
||||||
|
children,
|
||||||
|
}: StateBadgeProps) {
|
||||||
|
return (
|
||||||
|
<span className={`${getBadgeClasses(state, evenPadding)}`}>{children}</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
150
apps/login/ui/TOTPRegister.tsx
Normal file
150
apps/login/ui/TOTPRegister.tsx
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
"use client";
|
||||||
|
import { QRCodeSVG } from "qrcode.react";
|
||||||
|
import Alert, { AlertType } from "./Alert";
|
||||||
|
import Link from "next/link";
|
||||||
|
import CopyToClipboard from "./CopyToClipboard";
|
||||||
|
import { TextInput } from "./Input";
|
||||||
|
import { Button, ButtonVariants } from "./Button";
|
||||||
|
import { Spinner } from "./Spinner";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { verifyTOTP } from "#/lib/server-actions";
|
||||||
|
import { login } from "@zitadel/server";
|
||||||
|
|
||||||
|
type Inputs = {
|
||||||
|
code: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
uri: string;
|
||||||
|
secret: string;
|
||||||
|
loginName?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
authRequestId?: string;
|
||||||
|
organization?: string;
|
||||||
|
checkAfter?: boolean;
|
||||||
|
};
|
||||||
|
export default function TOTPRegister({
|
||||||
|
uri,
|
||||||
|
secret,
|
||||||
|
loginName,
|
||||||
|
sessionId,
|
||||||
|
authRequestId,
|
||||||
|
organization,
|
||||||
|
checkAfter,
|
||||||
|
}: Props) {
|
||||||
|
const [error, setError] = useState<string>("");
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { register, handleSubmit, formState } = useForm<Inputs>({
|
||||||
|
mode: "onBlur",
|
||||||
|
defaultValues: {
|
||||||
|
code: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
async function continueWithCode(values: Inputs) {
|
||||||
|
setLoading(true);
|
||||||
|
return verifyTOTP(values.code, loginName, organization)
|
||||||
|
.then((response) => {
|
||||||
|
setLoading(false);
|
||||||
|
// if attribute is set, validate MFA after it is setup, otherwise proceed as usual (when mfa is enforced to login)
|
||||||
|
if (checkAfter) {
|
||||||
|
const params = new URLSearchParams({});
|
||||||
|
|
||||||
|
if (loginName) {
|
||||||
|
params.append("loginName", loginName);
|
||||||
|
}
|
||||||
|
if (authRequestId) {
|
||||||
|
params.append("authRequestId", authRequestId);
|
||||||
|
}
|
||||||
|
if (organization) {
|
||||||
|
params.append("organization", organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
return router.push(`/otp/time-based?` + params);
|
||||||
|
} else {
|
||||||
|
if (authRequestId && sessionId) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
sessionId: sessionId,
|
||||||
|
authRequest: authRequestId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (organization) {
|
||||||
|
params.append("organization", organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
return router.push(`/login?` + params);
|
||||||
|
} else if (loginName) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
loginName,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (authRequestId) {
|
||||||
|
params.append("authRequestId", authRequestId);
|
||||||
|
}
|
||||||
|
if (organization) {
|
||||||
|
params.append("organization", organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
return router.push(`/signedin?` + params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
setLoading(false);
|
||||||
|
setError(e.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center ">
|
||||||
|
{uri && (
|
||||||
|
<>
|
||||||
|
<QRCodeSVG
|
||||||
|
className="rounded-md w-40 h-40 p-2 bg-white my-4"
|
||||||
|
value={uri}
|
||||||
|
/>
|
||||||
|
<div className="mb-4 w-96 flex text-sm my-2 border rounded-lg px-4 py-2 pr-2 border-divider-light dark:border-divider-dark">
|
||||||
|
<Link href={uri} target="_blank" className="flex-1 overflow-x-auto">
|
||||||
|
{uri}
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<CopyToClipboard value={uri}></CopyToClipboard>
|
||||||
|
</div>
|
||||||
|
<form className="w-full">
|
||||||
|
<div className="">
|
||||||
|
<TextInput
|
||||||
|
type="text"
|
||||||
|
{...register("code", { required: "This field is required" })}
|
||||||
|
label="Code"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="py-4">
|
||||||
|
<Alert>{error}</Alert>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-8 flex w-full flex-row items-center">
|
||||||
|
<span className="flex-grow"></span>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="self-end"
|
||||||
|
variant={ButtonVariants.Primary}
|
||||||
|
disabled={loading || !formState.isValid}
|
||||||
|
onClick={handleSubmit(continueWithCode)}
|
||||||
|
>
|
||||||
|
{loading && <Spinner className="h-5 w-5 mr-2" />}
|
||||||
|
continue
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,13 +6,33 @@ type Props = {
|
|||||||
loginName?: string;
|
loginName?: string;
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
showDropdown: boolean;
|
showDropdown: boolean;
|
||||||
|
searchParams?: Record<string | number | symbol, string | undefined>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function UserAvatar({
|
export default function UserAvatar({
|
||||||
loginName,
|
loginName,
|
||||||
displayName,
|
displayName,
|
||||||
showDropdown,
|
showDropdown,
|
||||||
|
searchParams,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const params = new URLSearchParams({});
|
||||||
|
|
||||||
|
if (searchParams?.sessionId) {
|
||||||
|
params.set("sessionId", searchParams.sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchParams?.organization) {
|
||||||
|
params.set("organization", searchParams.organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchParams?.authRequestId) {
|
||||||
|
params.set("authRequestId", searchParams.authRequestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchParams?.loginName) {
|
||||||
|
params.set("loginName", searchParams.loginName);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-row items-center rounded-full border p-[1px] dark:border-white/20">
|
<div className="flex h-full flex-row items-center rounded-full border p-[1px] dark:border-white/20">
|
||||||
<div>
|
<div>
|
||||||
@@ -26,7 +46,7 @@ export default function UserAvatar({
|
|||||||
<span className="flex-grow"></span>
|
<span className="flex-grow"></span>
|
||||||
{showDropdown && (
|
{showDropdown && (
|
||||||
<Link
|
<Link
|
||||||
href="/accounts"
|
href={"/accounts?" + params}
|
||||||
className="ml-4 flex items-center justify-center p-1 hover:bg-black/10 dark:hover:bg-white/10 rounded-full mr-1 transition-all"
|
className="ml-4 flex items-center justify-center p-1 hover:bg-black/10 dark:hover:bg-white/10 rounded-full mr-1 transition-all"
|
||||||
>
|
>
|
||||||
<ChevronDownIcon className="h-4 w-4" />
|
<ChevronDownIcon className="h-4 w-4" />
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createSessionForLoginname,
|
createSessionFromChecks,
|
||||||
createSessionForUserId,
|
|
||||||
createSessionForUserIdAndIdpIntent,
|
createSessionForUserIdAndIdpIntent,
|
||||||
getSession,
|
getSession,
|
||||||
server,
|
server,
|
||||||
@@ -13,7 +12,12 @@ import {
|
|||||||
addSessionToCookie,
|
addSessionToCookie,
|
||||||
updateSessionCookie,
|
updateSessionCookie,
|
||||||
} from "./cookies";
|
} from "./cookies";
|
||||||
import { Session, Challenges, RequestChallenges } from "@zitadel/server";
|
import {
|
||||||
|
Session,
|
||||||
|
Challenges,
|
||||||
|
RequestChallenges,
|
||||||
|
Checks,
|
||||||
|
} from "@zitadel/server";
|
||||||
|
|
||||||
export async function createSessionAndUpdateCookie(
|
export async function createSessionAndUpdateCookie(
|
||||||
loginName: string,
|
loginName: string,
|
||||||
@@ -22,10 +26,15 @@ export async function createSessionAndUpdateCookie(
|
|||||||
organization?: string,
|
organization?: string,
|
||||||
authRequestId?: string
|
authRequestId?: string
|
||||||
): Promise<Session> {
|
): Promise<Session> {
|
||||||
const createdSession = await createSessionForLoginname(
|
const createdSession = await createSessionFromChecks(
|
||||||
server,
|
server,
|
||||||
loginName,
|
password
|
||||||
password,
|
? {
|
||||||
|
user: { loginName },
|
||||||
|
password: { password },
|
||||||
|
// totp: { code: totpCode },
|
||||||
|
}
|
||||||
|
: { user: { loginName } },
|
||||||
challenges
|
challenges
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -72,10 +81,15 @@ export async function createSessionForUserIdAndUpdateCookie(
|
|||||||
challenges: RequestChallenges | undefined,
|
challenges: RequestChallenges | undefined,
|
||||||
authRequestId: string | undefined
|
authRequestId: string | undefined
|
||||||
): Promise<Session> {
|
): Promise<Session> {
|
||||||
const createdSession = await createSessionForUserId(
|
const createdSession = await createSessionFromChecks(
|
||||||
server,
|
server,
|
||||||
userId,
|
password
|
||||||
password,
|
? {
|
||||||
|
user: { userId },
|
||||||
|
password: { password },
|
||||||
|
// totp: { code: totpCode },
|
||||||
|
}
|
||||||
|
: { user: { userId } },
|
||||||
challenges
|
challenges
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -174,8 +188,7 @@ export type SessionWithChallenges = Session & {
|
|||||||
|
|
||||||
export async function setSessionAndUpdateCookie(
|
export async function setSessionAndUpdateCookie(
|
||||||
recentCookie: SessionCookie,
|
recentCookie: SessionCookie,
|
||||||
password: string | undefined,
|
checks: Checks,
|
||||||
webAuthN: { credentialAssertionData: any } | undefined,
|
|
||||||
challenges: RequestChallenges | undefined,
|
challenges: RequestChallenges | undefined,
|
||||||
authRequestId: string | undefined
|
authRequestId: string | undefined
|
||||||
): Promise<SessionWithChallenges> {
|
): Promise<SessionWithChallenges> {
|
||||||
@@ -183,9 +196,8 @@ export async function setSessionAndUpdateCookie(
|
|||||||
server,
|
server,
|
||||||
recentCookie.id,
|
recentCookie.id,
|
||||||
recentCookie.token,
|
recentCookie.token,
|
||||||
password,
|
challenges,
|
||||||
webAuthN,
|
checks
|
||||||
challenges
|
|
||||||
).then((updatedSession) => {
|
).then((updatedSession) => {
|
||||||
if (updatedSession) {
|
if (updatedSession) {
|
||||||
const sessionCookie: SessionCookie = {
|
const sessionCookie: SessionCookie = {
|
||||||
@@ -202,39 +214,31 @@ export async function setSessionAndUpdateCookie(
|
|||||||
sessionCookie.authRequestId = authRequestId;
|
sessionCookie.authRequestId = authRequestId;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve) => setTimeout(resolve, 1000)).then(() =>
|
return getSession(server, sessionCookie.id, sessionCookie.token).then(
|
||||||
// TODO: remove
|
(response) => {
|
||||||
getSession(server, sessionCookie.id, sessionCookie.token).then(
|
if (response?.session && response.session.factors?.user?.loginName) {
|
||||||
(response) => {
|
const { session } = response;
|
||||||
if (
|
const newCookie: SessionCookie = {
|
||||||
response?.session &&
|
id: sessionCookie.id,
|
||||||
response.session.factors?.user?.loginName
|
token: updatedSession.sessionToken,
|
||||||
) {
|
creationDate: sessionCookie.creationDate,
|
||||||
const { session } = response;
|
expirationDate: sessionCookie.expirationDate,
|
||||||
const newCookie: SessionCookie = {
|
changeDate: `${session.changeDate?.getTime() ?? ""}`,
|
||||||
id: sessionCookie.id,
|
loginName: session.factors?.user?.loginName ?? "",
|
||||||
token: updatedSession.sessionToken,
|
organization: session.factors?.user?.organizationId ?? "",
|
||||||
creationDate: sessionCookie.creationDate,
|
};
|
||||||
expirationDate: sessionCookie.expirationDate,
|
|
||||||
changeDate: `${session.changeDate?.getTime() ?? ""}`,
|
|
||||||
loginName: session.factors?.user?.loginName ?? "",
|
|
||||||
organization: session.factors?.user?.organizationId ?? "",
|
|
||||||
};
|
|
||||||
|
|
||||||
if (sessionCookie.authRequestId) {
|
if (sessionCookie.authRequestId) {
|
||||||
newCookie.authRequestId = sessionCookie.authRequestId;
|
newCookie.authRequestId = sessionCookie.authRequestId;
|
||||||
}
|
|
||||||
|
|
||||||
return updateSessionCookie(sessionCookie.id, newCookie).then(
|
|
||||||
() => {
|
|
||||||
return { challenges: updatedSession.challenges, ...session };
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw "could not get session or session does not have loginName";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return updateSessionCookie(sessionCookie.id, newCookie).then(() => {
|
||||||
|
return { challenges: updatedSession.challenges, ...session };
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw "could not get session or session does not have loginName";
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
throw "Session not be set";
|
throw "Session not be set";
|
||||||
|
|||||||
@@ -5,25 +5,43 @@ import {
|
|||||||
AuthServiceDefinition,
|
AuthServiceDefinition,
|
||||||
GetMyUserResponse,
|
GetMyUserResponse,
|
||||||
} from "../proto/server/zitadel/auth";
|
} from "../proto/server/zitadel/auth";
|
||||||
import { ZitadelServer } from "../server";
|
import { ZitadelServer, getServers } from "../server";
|
||||||
import { authMiddleware } from "../middleware";
|
import { authMiddleware } from "../middleware";
|
||||||
|
|
||||||
const createClient = <Client>(
|
const createClient = <Client>(
|
||||||
definition: CompatServiceDefinition,
|
definition: CompatServiceDefinition,
|
||||||
accessToken: string
|
apiUrl: string,
|
||||||
|
token: string
|
||||||
) => {
|
) => {
|
||||||
|
if (!apiUrl) {
|
||||||
|
throw Error("ZITADEL_API_URL not set");
|
||||||
|
}
|
||||||
|
|
||||||
const channel = createChannel(process.env.ZITADEL_API_URL ?? "");
|
const channel = createChannel(process.env.ZITADEL_API_URL ?? "");
|
||||||
return createClientFactory()
|
return createClientFactory()
|
||||||
.use(authMiddleware(accessToken))
|
.use(authMiddleware(token))
|
||||||
.create(definition, channel) as Client;
|
.create(definition, channel) as Client;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getAuth(app?: ZitadelServer): Promise<AuthServiceClient> {
|
export const getAuth = (app?: string | ZitadelServer) => {
|
||||||
|
let config;
|
||||||
|
if (app && typeof app === "string") {
|
||||||
|
const apps = getServers();
|
||||||
|
config = apps.find((a) => a.name === app)?.config;
|
||||||
|
} else if (app && typeof app === "object") {
|
||||||
|
config = app.config;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
throw Error("No ZITADEL app found");
|
||||||
|
}
|
||||||
|
|
||||||
return createClient<AuthServiceClient>(
|
return createClient<AuthServiceClient>(
|
||||||
AuthServiceDefinition as CompatServiceDefinition,
|
AuthServiceDefinition as CompatServiceDefinition,
|
||||||
""
|
config.apiUrl,
|
||||||
|
config.token
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export async function getMyUser(): Promise<GetMyUserResponse> {
|
export async function getMyUser(): Promise<GetMyUserResponse> {
|
||||||
const auth = await getAuth();
|
const auth = await getAuth();
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export * from "../proto/server/zitadel/auth";
|
export * from "./auth";
|
||||||
export { getAuth } from "./auth";
|
export * as auth from "../proto/server/zitadel/auth";
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import * as session from "./v2/session";
|
|||||||
import * as user from "./v2/user";
|
import * as user from "./v2/user";
|
||||||
import * as oidc from "./v2/oidc";
|
import * as oidc from "./v2/oidc";
|
||||||
import * as management from "./management";
|
import * as management from "./management";
|
||||||
|
import * as auth from "./auth";
|
||||||
|
|
||||||
import * as login from "./proto/server/zitadel/settings/v2beta/login_settings";
|
import * as login from "./proto/server/zitadel/settings/v2beta/login_settings";
|
||||||
import * as password from "./proto/server/zitadel/settings/v2beta/password_settings";
|
import * as password from "./proto/server/zitadel/settings/v2beta/password_settings";
|
||||||
@@ -51,6 +52,7 @@ export {
|
|||||||
CreateSessionResponse,
|
CreateSessionResponse,
|
||||||
SetSessionResponse,
|
SetSessionResponse,
|
||||||
SetSessionRequest,
|
SetSessionRequest,
|
||||||
|
Checks,
|
||||||
DeleteSessionResponse,
|
DeleteSessionResponse,
|
||||||
} from "./proto/server/zitadel/session/v2beta/session_service";
|
} from "./proto/server/zitadel/session/v2beta/session_service";
|
||||||
export {
|
export {
|
||||||
@@ -67,6 +69,7 @@ export { TextQueryMethod } from "./proto/server/zitadel/object/v2beta/object";
|
|||||||
export {
|
export {
|
||||||
AddHumanUserResponse,
|
AddHumanUserResponse,
|
||||||
AddHumanUserRequest,
|
AddHumanUserRequest,
|
||||||
|
GetUserByIDResponse,
|
||||||
VerifyEmailResponse,
|
VerifyEmailResponse,
|
||||||
VerifyPasskeyRegistrationRequest,
|
VerifyPasskeyRegistrationRequest,
|
||||||
VerifyPasskeyRegistrationResponse,
|
VerifyPasskeyRegistrationResponse,
|
||||||
@@ -83,17 +86,37 @@ export {
|
|||||||
RetrieveIdentityProviderIntentResponse,
|
RetrieveIdentityProviderIntentResponse,
|
||||||
ListUsersRequest,
|
ListUsersRequest,
|
||||||
ListUsersResponse,
|
ListUsersResponse,
|
||||||
|
AddOTPEmailResponse,
|
||||||
|
AddOTPEmailRequest,
|
||||||
|
AddOTPSMSResponse,
|
||||||
|
AddOTPSMSRequest,
|
||||||
|
RegisterTOTPRequest,
|
||||||
|
RegisterTOTPResponse,
|
||||||
|
VerifyTOTPRegistrationRequest,
|
||||||
|
VerifyTOTPRegistrationResponse,
|
||||||
|
VerifyU2FRegistrationRequest,
|
||||||
|
VerifyU2FRegistrationResponse,
|
||||||
|
RegisterU2FResponse,
|
||||||
|
RegisterU2FRequest,
|
||||||
} from "./proto/server/zitadel/user/v2beta/user_service";
|
} from "./proto/server/zitadel/user/v2beta/user_service";
|
||||||
|
|
||||||
|
export { AuthFactor } from "./proto/server/zitadel/user";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
SetHumanPasswordResponse,
|
SetHumanPasswordResponse,
|
||||||
SetHumanPasswordRequest,
|
SetHumanPasswordRequest,
|
||||||
GetOrgByDomainGlobalResponse,
|
GetOrgByDomainGlobalResponse,
|
||||||
|
ListHumanAuthFactorsResponse,
|
||||||
} from "./proto/server/zitadel/management";
|
} from "./proto/server/zitadel/management";
|
||||||
|
|
||||||
export * from "./proto/server/zitadel/idp";
|
export * from "./proto/server/zitadel/idp";
|
||||||
export { type LegalAndSupportSettings } from "./proto/server/zitadel/settings/v2beta/legal_settings";
|
export { type LegalAndSupportSettings } from "./proto/server/zitadel/settings/v2beta/legal_settings";
|
||||||
export { type PasswordComplexitySettings } from "./proto/server/zitadel/settings/v2beta/password_settings";
|
export { type PasswordComplexitySettings } from "./proto/server/zitadel/settings/v2beta/password_settings";
|
||||||
export { type ResourceOwnerType } from "./proto/server/zitadel/settings/v2beta/settings";
|
export { type ResourceOwnerType } from "./proto/server/zitadel/settings/v2beta/settings";
|
||||||
|
export {
|
||||||
|
type VerifyMyAuthFactorOTPResponse,
|
||||||
|
AddMyAuthFactorOTPResponse,
|
||||||
|
} from "./proto/server/zitadel/auth";
|
||||||
import {
|
import {
|
||||||
getServers,
|
getServers,
|
||||||
initializeServer,
|
initializeServer,
|
||||||
@@ -115,4 +138,5 @@ export {
|
|||||||
password,
|
password,
|
||||||
legal,
|
legal,
|
||||||
oidc,
|
oidc,
|
||||||
|
auth,
|
||||||
};
|
};
|
||||||
|
|||||||
8331
pnpm-lock.yaml
generated
8331
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user