mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-25 10:12:51 +00:00
@@ -1,3 +1,3 @@
|
|||||||
ZITADEL_API_URL=http://localhost:22222
|
ZITADEL_API_URL=http://localhost:22222
|
||||||
CACHE_REVALIDATION_INTERVAL_IN_SECONDS=3600
|
EMAIL_VERIFICATION=true
|
||||||
DEBUG=true
|
DEBUG=true
|
@@ -49,6 +49,16 @@ export default async function Page(props: {
|
|||||||
organization ?? defaultOrganization,
|
organization ?? defaultOrganization,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
if (authRequestId) {
|
||||||
|
params.append("authRequestId", authRequestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (organization) {
|
||||||
|
params.append("organization", organization);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DynamicTheme branding={branding}>
|
<DynamicTheme branding={branding}>
|
||||||
<div className="flex flex-col items-center space-y-4">
|
<div className="flex flex-col items-center space-y-4">
|
||||||
@@ -57,16 +67,7 @@ export default async function Page(props: {
|
|||||||
|
|
||||||
<div className="flex flex-col w-full space-y-2">
|
<div className="flex flex-col w-full space-y-2">
|
||||||
<SessionsList sessions={sessions} authRequestId={authRequestId} />
|
<SessionsList sessions={sessions} authRequestId={authRequestId} />
|
||||||
<Link
|
<Link href={`/loginname?` + params}>
|
||||||
href={
|
|
||||||
authRequestId
|
|
||||||
? `/loginname?` +
|
|
||||||
new URLSearchParams({
|
|
||||||
authRequestId,
|
|
||||||
})
|
|
||||||
: "/loginname"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="flex flex-row items-center py-3 px-4 hover:bg-black/10 dark:hover:bg-white/10 rounded-md transition-all">
|
<div className="flex flex-row items-center py-3 px-4 hover:bg-black/10 dark:hover:bg-white/10 rounded-md transition-all">
|
||||||
<div className="w-8 h-8 mr-4 flex flex-row justify-center items-center rounded-full bg-black/5 dark:bg-white/5">
|
<div className="w-8 h-8 mr-4 flex flex-row justify-center items-center rounded-full bg-black/5 dark:bg-white/5">
|
||||||
<UserPlusIcon className="h-5 w-5" />
|
<UserPlusIcon className="h-5 w-5" />
|
||||||
|
@@ -3,6 +3,8 @@ import { DynamicTheme } from "@/components/dynamic-theme";
|
|||||||
import { UserAvatar } from "@/components/user-avatar";
|
import { UserAvatar } from "@/components/user-avatar";
|
||||||
import { VerifyForm } from "@/components/verify-form";
|
import { VerifyForm } from "@/components/verify-form";
|
||||||
import { VerifyRedirectButton } from "@/components/verify-redirect-button";
|
import { VerifyRedirectButton } from "@/components/verify-redirect-button";
|
||||||
|
import { resendVerification } from "@/lib/server/verify";
|
||||||
|
import { loadMostRecentSession } from "@/lib/session";
|
||||||
import {
|
import {
|
||||||
getBrandingSettings,
|
getBrandingSettings,
|
||||||
getUserByID,
|
getUserByID,
|
||||||
@@ -18,14 +20,49 @@ export default async function Page(props: { searchParams: Promise<any> }) {
|
|||||||
const t = await getTranslations({ locale, namespace: "verify" });
|
const t = await getTranslations({ locale, namespace: "verify" });
|
||||||
const tError = await getTranslations({ locale, namespace: "error" });
|
const tError = await getTranslations({ locale, namespace: "error" });
|
||||||
|
|
||||||
const { userId, loginName, code, organization, authRequestId, invite } =
|
const {
|
||||||
searchParams;
|
userId,
|
||||||
|
loginName,
|
||||||
|
code,
|
||||||
|
organization,
|
||||||
|
authRequestId,
|
||||||
|
invite,
|
||||||
|
skipsend,
|
||||||
|
} = searchParams;
|
||||||
|
|
||||||
const branding = await getBrandingSettings(organization);
|
const branding = await getBrandingSettings(organization);
|
||||||
|
|
||||||
|
let sessionFactors;
|
||||||
let user: User | undefined;
|
let user: User | undefined;
|
||||||
let human: HumanUser | undefined;
|
let human: HumanUser | undefined;
|
||||||
if (userId) {
|
let id: string | undefined;
|
||||||
|
|
||||||
|
if ("loginName" in searchParams) {
|
||||||
|
sessionFactors = await loadMostRecentSession({
|
||||||
|
loginName,
|
||||||
|
organization,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!skipsend && sessionFactors?.factors?.user?.id) {
|
||||||
|
await resendVerification({
|
||||||
|
userId: sessionFactors?.factors?.user?.id,
|
||||||
|
isInvite: invite === "true",
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error("Could not resend verification email", error);
|
||||||
|
throw Error("Could not request email");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if ("userId" in searchParams && userId) {
|
||||||
|
if (!skipsend) {
|
||||||
|
await resendVerification({
|
||||||
|
userId,
|
||||||
|
isInvite: invite === "true",
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error("Could not resend verification email", error);
|
||||||
|
throw Error("Could not request email");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const userResponse = await getUserByID(userId);
|
const userResponse = await getUserByID(userId);
|
||||||
if (userResponse) {
|
if (userResponse) {
|
||||||
user = userResponse.user;
|
user = userResponse.user;
|
||||||
@@ -35,6 +72,8 @@ export default async function Page(props: { searchParams: Promise<any> }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
id = userId ?? sessionFactors?.factors?.user?.id;
|
||||||
|
|
||||||
let authMethods: AuthenticationMethodType[] | null = null;
|
let authMethods: AuthenticationMethodType[] | null = null;
|
||||||
if (human?.email?.isVerified) {
|
if (human?.email?.isVerified) {
|
||||||
const authMethodsResponse = await listAuthenticationMethodTypes(userId);
|
const authMethodsResponse = await listAuthenticationMethodTypes(userId);
|
||||||
@@ -66,7 +105,7 @@ export default async function Page(props: { searchParams: Promise<any> }) {
|
|||||||
<h1>{t("verify.title")}</h1>
|
<h1>{t("verify.title")}</h1>
|
||||||
<p className="ztdl-p mb-6 block">{t("verify.description")}</p>
|
<p className="ztdl-p mb-6 block">{t("verify.description")}</p>
|
||||||
|
|
||||||
{!userId && (
|
{!id && (
|
||||||
<>
|
<>
|
||||||
<h1>{t("verify.title")}</h1>
|
<h1>{t("verify.title")}</h1>
|
||||||
<p className="ztdl-p mb-6 block">{t("verify.description")}</p>
|
<p className="ztdl-p mb-6 block">{t("verify.description")}</p>
|
||||||
@@ -77,29 +116,44 @@ export default async function Page(props: { searchParams: Promise<any> }) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{user && (
|
{sessionFactors ? (
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
loginName={user.preferredLoginName}
|
loginName={loginName ?? sessionFactors.factors?.user?.loginName}
|
||||||
displayName={human?.profile?.displayName}
|
displayName={sessionFactors.factors?.user?.displayName}
|
||||||
showDropdown={false}
|
showDropdown
|
||||||
/>
|
searchParams={searchParams}
|
||||||
|
></UserAvatar>
|
||||||
|
) : (
|
||||||
|
user && (
|
||||||
|
<UserAvatar
|
||||||
|
loginName={user.preferredLoginName}
|
||||||
|
displayName={human?.profile?.displayName}
|
||||||
|
showDropdown={false}
|
||||||
|
/>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{human?.email?.isVerified ? (
|
{id &&
|
||||||
<VerifyRedirectButton
|
(human?.email?.isVerified ? (
|
||||||
userId={userId}
|
// show page for already verified users
|
||||||
authRequestId={authRequestId}
|
<VerifyRedirectButton
|
||||||
authMethods={authMethods}
|
userId={id}
|
||||||
/>
|
loginName={loginName}
|
||||||
) : (
|
organization={organization}
|
||||||
// check if auth methods are set
|
authRequestId={authRequestId}
|
||||||
<VerifyForm
|
authMethods={authMethods}
|
||||||
userId={userId}
|
/>
|
||||||
code={code}
|
) : (
|
||||||
isInvite={invite === "true"}
|
// check if auth methods are set
|
||||||
params={params}
|
<VerifyForm
|
||||||
/>
|
loginName={loginName}
|
||||||
)}
|
organization={organization}
|
||||||
|
userId={id}
|
||||||
|
code={code}
|
||||||
|
isInvite={invite === "true"}
|
||||||
|
authRequestId={authRequestId}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</DynamicTheme>
|
</DynamicTheme>
|
||||||
);
|
);
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { createNewSessionForIdp } from "@/lib/server/session";
|
import { createNewSessionFromIdpIntent } from "@/lib/server/idp";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Alert } from "./alert";
|
import { Alert } from "./alert";
|
||||||
@@ -27,7 +27,7 @@ export function IdpSignin({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
createNewSessionForIdp({
|
createNewSessionFromIdpIntent({
|
||||||
userId,
|
userId,
|
||||||
idpIntent: {
|
idpIntent: {
|
||||||
idpIntentId,
|
idpIntentId,
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { coerceToArrayBuffer, coerceToBase64Url } from "@/helpers/base64";
|
import { coerceToArrayBuffer, coerceToBase64Url } from "@/helpers/base64";
|
||||||
import { getNextUrl } from "@/lib/client";
|
import { sendPasskey } from "@/lib/server/passkeys";
|
||||||
import { updateSession } from "@/lib/server/session";
|
import { updateSession } from "@/lib/server/session";
|
||||||
import { create } from "@zitadel/client";
|
import { create, JsonObject } from "@zitadel/client";
|
||||||
import {
|
import {
|
||||||
RequestChallengesSchema,
|
RequestChallengesSchema,
|
||||||
UserVerificationRequirement,
|
UserVerificationRequirement,
|
||||||
@@ -118,9 +118,9 @@ export function LoginPasskey({
|
|||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitLogin(data: any) {
|
async function submitLogin(data: JsonObject) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await updateSession({
|
const response = await sendPasskey({
|
||||||
loginName,
|
loginName,
|
||||||
sessionId,
|
sessionId,
|
||||||
organization,
|
organization,
|
||||||
@@ -142,7 +142,9 @@ export function LoginPasskey({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
if (response && "redirect" in response && response.redirect) {
|
||||||
|
return router.push(response.redirect);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitLoginAndContinue(
|
async function submitLoginAndContinue(
|
||||||
@@ -192,31 +194,7 @@ export function LoginPasskey({
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return submitLogin(data).then(async (resp) => {
|
return submitLogin(data);
|
||||||
const url =
|
|
||||||
authRequestId && resp?.sessionId
|
|
||||||
? await getNextUrl(
|
|
||||||
{
|
|
||||||
sessionId: resp.sessionId,
|
|
||||||
authRequestId: authRequestId,
|
|
||||||
organization: organization,
|
|
||||||
},
|
|
||||||
loginSettings?.defaultRedirectUri,
|
|
||||||
)
|
|
||||||
: resp?.factors?.user?.loginName
|
|
||||||
? await getNextUrl(
|
|
||||||
{
|
|
||||||
loginName: resp.factors.user.loginName,
|
|
||||||
organization: organization,
|
|
||||||
},
|
|
||||||
loginSettings?.defaultRedirectUri,
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (url) {
|
|
||||||
router.push(url);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
@@ -1,7 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { coerceToArrayBuffer, coerceToBase64Url } from "@/helpers/base64";
|
import { coerceToArrayBuffer, coerceToBase64Url } from "@/helpers/base64";
|
||||||
import { registerPasskeyLink, verifyPasskey } from "@/lib/server/passkeys";
|
import {
|
||||||
|
registerPasskeyLink,
|
||||||
|
verifyPasskeyRegistration,
|
||||||
|
} from "@/lib/server/passkeys";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@@ -45,7 +48,7 @@ export function RegisterPasskey({
|
|||||||
sessionId: string,
|
sessionId: string,
|
||||||
) {
|
) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await verifyPasskey({
|
const response = await verifyPasskeyRegistration({
|
||||||
passkeyId,
|
passkeyId,
|
||||||
passkeyName,
|
passkeyName,
|
||||||
publicKeyCredential,
|
publicKeyCredential,
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Alert, AlertType } from "@/components/alert";
|
import { Alert, AlertType } from "@/components/alert";
|
||||||
import { resendVerification, sendVerification } from "@/lib/server/email";
|
import { resendVerification, sendVerification } from "@/lib/server/verify";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
@@ -17,12 +17,21 @@ type Inputs = {
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
userId: string;
|
userId: string;
|
||||||
|
loginName?: string;
|
||||||
|
organization?: string;
|
||||||
code?: string;
|
code?: string;
|
||||||
isInvite: boolean;
|
isInvite: boolean;
|
||||||
params: URLSearchParams;
|
authRequestId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function VerifyForm({ userId, code, isInvite, params }: Props) {
|
export function VerifyForm({
|
||||||
|
userId,
|
||||||
|
loginName,
|
||||||
|
organization,
|
||||||
|
authRequestId,
|
||||||
|
code,
|
||||||
|
isInvite,
|
||||||
|
}: Props) {
|
||||||
const t = useTranslations("verify");
|
const t = useTranslations("verify");
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -67,6 +76,9 @@ export function VerifyForm({ userId, code, isInvite, params }: Props) {
|
|||||||
code: value.code,
|
code: value.code,
|
||||||
userId,
|
userId,
|
||||||
isInvite: isInvite,
|
isInvite: isInvite,
|
||||||
|
loginName: loginName,
|
||||||
|
organization: organization,
|
||||||
|
authRequestId: authRequestId,
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setError("Could not verify user");
|
setError("Could not verify user");
|
||||||
@@ -76,12 +88,12 @@ export function VerifyForm({ userId, code, isInvite, params }: Props) {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response?.error) {
|
if (response && "error" in response && response?.error) {
|
||||||
setError(response.error);
|
setError(response.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response?.redirect) {
|
if (response && "redirect" in response && response?.redirect) {
|
||||||
return router.push(response?.redirect);
|
return router.push(response?.redirect);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -1,6 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { sendVerificationRedirectWithoutCheck } from "@/lib/server/email";
|
import {
|
||||||
|
sendVerificationRedirectWithoutCheck,
|
||||||
|
SendVerificationRedirectWithoutCheckCommand,
|
||||||
|
} from "@/lib/server/verify";
|
||||||
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@@ -11,12 +14,16 @@ import { Spinner } from "./spinner";
|
|||||||
|
|
||||||
export function VerifyRedirectButton({
|
export function VerifyRedirectButton({
|
||||||
userId,
|
userId,
|
||||||
|
loginName,
|
||||||
authRequestId,
|
authRequestId,
|
||||||
authMethods,
|
authMethods,
|
||||||
|
organization,
|
||||||
}: {
|
}: {
|
||||||
userId: string;
|
userId?: string;
|
||||||
|
loginName?: string;
|
||||||
authRequestId: string;
|
authRequestId: string;
|
||||||
authMethods: AuthenticationMethodType[] | null;
|
authMethods: AuthenticationMethodType[] | null;
|
||||||
|
organization?: string;
|
||||||
}) {
|
}) {
|
||||||
const t = useTranslations("verify");
|
const t = useTranslations("verify");
|
||||||
const [error, setError] = useState<string>("");
|
const [error, setError] = useState<string>("");
|
||||||
@@ -26,10 +33,24 @@ export function VerifyRedirectButton({
|
|||||||
async function submitAndContinue(): Promise<boolean | void> {
|
async function submitAndContinue(): Promise<boolean | void> {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
await sendVerificationRedirectWithoutCheck({
|
let command = {
|
||||||
userId,
|
organization,
|
||||||
authRequestId,
|
authRequestId,
|
||||||
})
|
} as SendVerificationRedirectWithoutCheckCommand;
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
|
command = {
|
||||||
|
...command,
|
||||||
|
userId,
|
||||||
|
} as SendVerificationRedirectWithoutCheckCommand;
|
||||||
|
} else if (loginName) {
|
||||||
|
command = {
|
||||||
|
...command,
|
||||||
|
loginName,
|
||||||
|
} as SendVerificationRedirectWithoutCheckCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendVerificationRedirectWithoutCheck(command)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
setError("Could not verify user");
|
setError("Could not verify user");
|
||||||
return;
|
return;
|
||||||
|
@@ -1,138 +0,0 @@
|
|||||||
"use server";
|
|
||||||
|
|
||||||
import {
|
|
||||||
getUserByID,
|
|
||||||
listAuthenticationMethodTypes,
|
|
||||||
resendEmailCode,
|
|
||||||
resendInviteCode,
|
|
||||||
verifyEmail,
|
|
||||||
verifyInviteCode,
|
|
||||||
} from "@/lib/zitadel";
|
|
||||||
import { create } from "@zitadel/client";
|
|
||||||
import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
|
||||||
import { createSessionAndUpdateCookie } from "./cookie";
|
|
||||||
|
|
||||||
type VerifyUserByEmailCommand = {
|
|
||||||
userId: string;
|
|
||||||
code: string;
|
|
||||||
isInvite: boolean;
|
|
||||||
authRequestId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function sendVerification(command: VerifyUserByEmailCommand) {
|
|
||||||
const verifyResponse = command.isInvite
|
|
||||||
? await verifyInviteCode(command.userId, command.code).catch(() => {
|
|
||||||
return { error: "Could not verify invite" };
|
|
||||||
})
|
|
||||||
: await verifyEmail(command.userId, command.code).catch(() => {
|
|
||||||
return { error: "Could not verify email" };
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!verifyResponse) {
|
|
||||||
return { error: "Could not verify user" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const userResponse = await getUserByID(command.userId);
|
|
||||||
|
|
||||||
if (!userResponse || !userResponse.user) {
|
|
||||||
return { error: "Could not load user" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const checks = create(ChecksSchema, {
|
|
||||||
user: {
|
|
||||||
search: {
|
|
||||||
case: "loginName",
|
|
||||||
value: userResponse.user.preferredLoginName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const session = await createSessionAndUpdateCookie(
|
|
||||||
checks,
|
|
||||||
undefined,
|
|
||||||
command.authRequestId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const authMethodResponse = await listAuthenticationMethodTypes(
|
|
||||||
command.userId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!authMethodResponse || !authMethodResponse.authMethodTypes) {
|
|
||||||
return { error: "Could not load possible authenticators" };
|
|
||||||
}
|
|
||||||
// if no authmethods are found on the user, redirect to set one up
|
|
||||||
if (
|
|
||||||
authMethodResponse &&
|
|
||||||
authMethodResponse.authMethodTypes &&
|
|
||||||
authMethodResponse.authMethodTypes.length == 0
|
|
||||||
) {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
sessionId: session.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (session.factors?.user?.loginName) {
|
|
||||||
params.set("loginName", session.factors?.user?.loginName);
|
|
||||||
}
|
|
||||||
return { redirect: `/authenticator/set?${params}` };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type resendVerifyEmailCommand = {
|
|
||||||
userId: string;
|
|
||||||
isInvite: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function resendVerification(command: resendVerifyEmailCommand) {
|
|
||||||
return command.isInvite
|
|
||||||
? resendInviteCode(command.userId)
|
|
||||||
: resendEmailCode(command.userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function sendVerificationRedirectWithoutCheck(command: {
|
|
||||||
userId: string;
|
|
||||||
authRequestId?: string;
|
|
||||||
}) {
|
|
||||||
const userResponse = await getUserByID(command.userId);
|
|
||||||
|
|
||||||
if (!userResponse || !userResponse.user) {
|
|
||||||
return { error: "Could not load user" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const checks = create(ChecksSchema, {
|
|
||||||
user: {
|
|
||||||
search: {
|
|
||||||
case: "loginName",
|
|
||||||
value: userResponse.user.preferredLoginName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const session = await createSessionAndUpdateCookie(
|
|
||||||
checks,
|
|
||||||
undefined,
|
|
||||||
command.authRequestId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const authMethodResponse = await listAuthenticationMethodTypes(
|
|
||||||
command.userId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!authMethodResponse || !authMethodResponse.authMethodTypes) {
|
|
||||||
return { error: "Could not load possible authenticators" };
|
|
||||||
}
|
|
||||||
// if no authmethods are found on the user, redirect to set one up
|
|
||||||
if (
|
|
||||||
authMethodResponse &&
|
|
||||||
authMethodResponse.authMethodTypes &&
|
|
||||||
authMethodResponse.authMethodTypes.length == 0
|
|
||||||
) {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
sessionId: session.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (session.factors?.user?.loginName) {
|
|
||||||
params.set("loginName", session.factors?.user?.loginName);
|
|
||||||
}
|
|
||||||
return { redirect: `/authenticator/set?${params}` };
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,7 +1,14 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { startIdentityProviderFlow } from "@/lib/zitadel";
|
import {
|
||||||
|
getLoginSettings,
|
||||||
|
getUserByID,
|
||||||
|
startIdentityProviderFlow,
|
||||||
|
} from "@/lib/zitadel";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
|
import { getNextUrl } from "../client";
|
||||||
|
import { checkEmailVerification } from "../verify-helper";
|
||||||
|
import { createSessionForIdpAndUpdateCookie } from "./cookie";
|
||||||
|
|
||||||
export type StartIDPFlowCommand = {
|
export type StartIDPFlowCommand = {
|
||||||
idpId: string;
|
idpId: string;
|
||||||
@@ -32,3 +39,85 @@ export async function startIDPFlow(command: StartIDPFlowCommand) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CreateNewSessionCommand = {
|
||||||
|
userId: string;
|
||||||
|
idpIntent: {
|
||||||
|
idpIntentId: string;
|
||||||
|
idpIntentToken: string;
|
||||||
|
};
|
||||||
|
loginName?: string;
|
||||||
|
password?: string;
|
||||||
|
organization?: string;
|
||||||
|
authRequestId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function createNewSessionFromIdpIntent(
|
||||||
|
command: CreateNewSessionCommand,
|
||||||
|
) {
|
||||||
|
if (!command.userId || !command.idpIntent) {
|
||||||
|
throw new Error("No userId or loginName provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
const userResponse = await getUserByID(command.userId);
|
||||||
|
|
||||||
|
if (!userResponse || !userResponse.user) {
|
||||||
|
return { error: "Could not find user" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginSettings = await getLoginSettings(
|
||||||
|
userResponse.user.details?.resourceOwner,
|
||||||
|
);
|
||||||
|
|
||||||
|
const session = await createSessionForIdpAndUpdateCookie(
|
||||||
|
command.userId,
|
||||||
|
command.idpIntent,
|
||||||
|
command.authRequestId,
|
||||||
|
loginSettings?.externalLoginCheckLifetime,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!session || !session.factors?.user) {
|
||||||
|
return { error: "Could not create session" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const humanUser =
|
||||||
|
userResponse.user.type.case === "human"
|
||||||
|
? userResponse.user.type.value
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
// check to see if user was verified
|
||||||
|
const emailVerificationCheck = checkEmailVerification(
|
||||||
|
session,
|
||||||
|
humanUser,
|
||||||
|
command.organization,
|
||||||
|
command.authRequestId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (emailVerificationCheck?.redirect) {
|
||||||
|
return emailVerificationCheck;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: check if user has MFA methods
|
||||||
|
// const mfaFactorCheck = checkMFAFactors(session, loginSettings, authMethods, organization, authRequestId);
|
||||||
|
// if (mfaFactorCheck?.redirect) {
|
||||||
|
// return mfaFactorCheck;
|
||||||
|
// }
|
||||||
|
|
||||||
|
const url = await getNextUrl(
|
||||||
|
command.authRequestId && session.id
|
||||||
|
? {
|
||||||
|
sessionId: session.id,
|
||||||
|
authRequestId: command.authRequestId,
|
||||||
|
organization: session.factors.user.organizationId,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
loginName: session.factors.user.loginName,
|
||||||
|
organization: session.factors.user.organizationId,
|
||||||
|
},
|
||||||
|
loginSettings?.defaultRedirectUri,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
return { redirect: url };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -8,6 +8,7 @@ import { idpTypeToIdentityProviderType, idpTypeToSlug } from "../idp";
|
|||||||
|
|
||||||
import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
||||||
import { UserState } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
import { UserState } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
||||||
|
import { checkInvite } from "../verify-helper";
|
||||||
import {
|
import {
|
||||||
getActiveIdentityProviders,
|
getActiveIdentityProviders,
|
||||||
getIDPByID,
|
getIDPByID,
|
||||||
@@ -171,29 +172,21 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!methods.authMethodTypes || !methods.authMethodTypes.length) {
|
if (!methods.authMethodTypes || !methods.authMethodTypes.length) {
|
||||||
if (
|
const humanUser =
|
||||||
potentialUsers[0].type.case === "human" &&
|
potentialUsers[0].type.case === "human"
|
||||||
potentialUsers[0].type.value.email &&
|
? potentialUsers[0].type.value
|
||||||
!potentialUsers[0].type.value.email.isVerified
|
: undefined;
|
||||||
) {
|
|
||||||
const paramsVerify = new URLSearchParams({
|
|
||||||
loginName: session.factors?.user?.loginName,
|
|
||||||
userId: session.factors?.user?.id, // verify needs user id
|
|
||||||
invite: "true", // TODO: check - set this to true as we dont expect old email verification method here
|
|
||||||
});
|
|
||||||
|
|
||||||
if (command.organization || session.factors?.user?.organizationId) {
|
// redirect to /verify invite if no auth method is set and email is not verified
|
||||||
paramsVerify.append(
|
const inviteCheck = checkInvite(
|
||||||
"organization",
|
session,
|
||||||
command.organization ?? session.factors?.user?.organizationId,
|
humanUser,
|
||||||
);
|
session.factors.user.organizationId,
|
||||||
}
|
command.authRequestId,
|
||||||
|
);
|
||||||
|
|
||||||
if (command.authRequestId) {
|
if (inviteCheck?.redirect) {
|
||||||
paramsVerify.append("authRequestId", command.authRequestId);
|
return inviteCheck;
|
||||||
}
|
|
||||||
|
|
||||||
return { redirect: "/verify?" + paramsVerify };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const paramsAuthenticatorSetup = new URLSearchParams({
|
const paramsAuthenticatorSetup = new URLSearchParams({
|
||||||
@@ -350,8 +343,9 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
|||||||
if (command.authRequestId) {
|
if (command.authRequestId) {
|
||||||
params.set("authRequestId", command.authRequestId);
|
params.set("authRequestId", command.authRequestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (command.loginName) {
|
if (command.loginName) {
|
||||||
params.set("loginName", command.loginName);
|
params.set("email", command.loginName);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { redirect: "/register?" + params };
|
return { redirect: "/register?" + params };
|
||||||
|
@@ -2,18 +2,28 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
createPasskeyRegistrationLink,
|
createPasskeyRegistrationLink,
|
||||||
|
getLoginSettings,
|
||||||
getSession,
|
getSession,
|
||||||
|
getUserByID,
|
||||||
registerPasskey,
|
registerPasskey,
|
||||||
verifyPasskeyRegistration,
|
verifyPasskeyRegistration as zitadelVerifyPasskeyRegistration,
|
||||||
} from "@/lib/zitadel";
|
} from "@/lib/zitadel";
|
||||||
import { create } from "@zitadel/client";
|
import { create, Duration } from "@zitadel/client";
|
||||||
|
import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
||||||
import {
|
import {
|
||||||
RegisterPasskeyResponse,
|
RegisterPasskeyResponse,
|
||||||
VerifyPasskeyRegistrationRequestSchema,
|
VerifyPasskeyRegistrationRequestSchema,
|
||||||
} from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
} from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { userAgent } from "next/server";
|
import { userAgent } from "next/server";
|
||||||
import { getSessionCookieById } from "../cookies";
|
import { getNextUrl } from "../client";
|
||||||
|
import {
|
||||||
|
getMostRecentSessionCookie,
|
||||||
|
getSessionCookieById,
|
||||||
|
getSessionCookieByLoginName,
|
||||||
|
} from "../cookies";
|
||||||
|
import { checkEmailVerification } from "../verify-helper";
|
||||||
|
import { setSessionAndUpdateCookie } from "./cookie";
|
||||||
|
|
||||||
type VerifyPasskeyCommand = {
|
type VerifyPasskeyCommand = {
|
||||||
passkeyId: string;
|
passkeyId: string;
|
||||||
@@ -69,7 +79,7 @@ export async function registerPasskeyLink(
|
|||||||
return registerPasskey(userId, registerLink.code, hostname);
|
return registerPasskey(userId, registerLink.code, hostname);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function verifyPasskey(command: VerifyPasskeyCommand) {
|
export async function verifyPasskeyRegistration(command: VerifyPasskeyCommand) {
|
||||||
// if no name is provided, try to generate one from the user agent
|
// if no name is provided, try to generate one from the user agent
|
||||||
let passkeyName = command.passkeyName;
|
let passkeyName = command.passkeyName;
|
||||||
if (!!!passkeyName) {
|
if (!!!passkeyName) {
|
||||||
@@ -95,7 +105,7 @@ export async function verifyPasskey(command: VerifyPasskeyCommand) {
|
|||||||
throw new Error("Could not get session");
|
throw new Error("Could not get session");
|
||||||
}
|
}
|
||||||
|
|
||||||
return verifyPasskeyRegistration(
|
return zitadelVerifyPasskeyRegistration(
|
||||||
create(VerifyPasskeyRegistrationRequestSchema, {
|
create(VerifyPasskeyRegistrationRequestSchema, {
|
||||||
passkeyId: command.passkeyId,
|
passkeyId: command.passkeyId,
|
||||||
publicKeyCredential: command.publicKeyCredential,
|
publicKeyCredential: command.publicKeyCredential,
|
||||||
@@ -104,3 +114,97 @@ export async function verifyPasskey(command: VerifyPasskeyCommand) {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SendPasskeyCommand = {
|
||||||
|
loginName?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
organization?: string;
|
||||||
|
checks?: Checks;
|
||||||
|
authRequestId?: string;
|
||||||
|
lifetime?: Duration;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function sendPasskey(command: SendPasskeyCommand) {
|
||||||
|
let { loginName, sessionId, organization, checks, authRequestId } = command;
|
||||||
|
const recentSession = sessionId
|
||||||
|
? await getSessionCookieById({ sessionId })
|
||||||
|
: loginName
|
||||||
|
? await getSessionCookieByLoginName({ loginName, organization })
|
||||||
|
: await getMostRecentSessionCookie();
|
||||||
|
|
||||||
|
if (!recentSession) {
|
||||||
|
return {
|
||||||
|
error: "Could not find session",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const host = (await headers()).get("host");
|
||||||
|
|
||||||
|
if (!host) {
|
||||||
|
return { error: "Could not get host" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginSettings = await getLoginSettings(organization);
|
||||||
|
|
||||||
|
const lifetime = checks?.webAuthN
|
||||||
|
? loginSettings?.multiFactorCheckLifetime // TODO different lifetime for webauthn u2f/passkey
|
||||||
|
: checks?.otpEmail || checks?.otpSms
|
||||||
|
? loginSettings?.secondFactorCheckLifetime
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const session = await setSessionAndUpdateCookie(
|
||||||
|
recentSession,
|
||||||
|
checks,
|
||||||
|
undefined,
|
||||||
|
authRequestId,
|
||||||
|
lifetime,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!session || !session?.factors?.user?.id) {
|
||||||
|
return { error: "Could not update session" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const userResponse = await getUserByID(session?.factors?.user?.id);
|
||||||
|
|
||||||
|
if (!userResponse.user) {
|
||||||
|
return { error: "Could not find user" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const humanUser =
|
||||||
|
userResponse.user.type.case === "human"
|
||||||
|
? userResponse.user.type.value
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const emailVerificationCheck = checkEmailVerification(
|
||||||
|
session,
|
||||||
|
humanUser,
|
||||||
|
organization,
|
||||||
|
authRequestId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (emailVerificationCheck?.redirect) {
|
||||||
|
return emailVerificationCheck;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url =
|
||||||
|
authRequestId && session.id
|
||||||
|
? await getNextUrl(
|
||||||
|
{
|
||||||
|
sessionId: session.id,
|
||||||
|
authRequestId: authRequestId,
|
||||||
|
organization: organization,
|
||||||
|
},
|
||||||
|
loginSettings?.defaultRedirectUri,
|
||||||
|
)
|
||||||
|
: session?.factors?.user?.loginName
|
||||||
|
? await getNextUrl(
|
||||||
|
{
|
||||||
|
loginName: session.factors.user.loginName,
|
||||||
|
organization: organization,
|
||||||
|
},
|
||||||
|
loginSettings?.defaultRedirectUri,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return { redirect: url };
|
||||||
|
}
|
||||||
|
@@ -30,6 +30,11 @@ import {
|
|||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { getNextUrl } from "../client";
|
import { getNextUrl } from "../client";
|
||||||
import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies";
|
import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies";
|
||||||
|
import {
|
||||||
|
checkEmailVerification,
|
||||||
|
checkMFAFactors,
|
||||||
|
checkPasswordChangeRequired,
|
||||||
|
} from "../verify-helper";
|
||||||
|
|
||||||
type ResetPasswordCommand = {
|
type ResetPasswordCommand = {
|
||||||
loginName: string;
|
loginName: string;
|
||||||
@@ -134,6 +139,37 @@ export async function sendPassword(command: UpdateSessionCommand) {
|
|||||||
return { error: "Could not create session for user" };
|
return { error: "Could not create session for user" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const humanUser = user.type.case === "human" ? user.type.value : undefined;
|
||||||
|
|
||||||
|
// check if the user has to change password first
|
||||||
|
const passwordChangedCheck = checkPasswordChangeRequired(
|
||||||
|
session,
|
||||||
|
humanUser,
|
||||||
|
command.organization,
|
||||||
|
command.authRequestId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (passwordChangedCheck?.redirect) {
|
||||||
|
return passwordChangedCheck;
|
||||||
|
}
|
||||||
|
|
||||||
|
// throw error if user is in initial state here and do not continue
|
||||||
|
if (user.state === UserState.INITIAL) {
|
||||||
|
return { error: "Initial User not supported" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// check to see if user was verified
|
||||||
|
const emailVerificationCheck = checkEmailVerification(
|
||||||
|
session,
|
||||||
|
humanUser,
|
||||||
|
command.organization,
|
||||||
|
command.authRequestId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (emailVerificationCheck?.redirect) {
|
||||||
|
return emailVerificationCheck;
|
||||||
|
}
|
||||||
|
|
||||||
// if password, check if user has MFA methods
|
// if password, check if user has MFA methods
|
||||||
let authMethods;
|
let authMethods;
|
||||||
if (command.checks && command.checks.password && session.factors?.user?.id) {
|
if (command.checks && command.checks.password && session.factors?.user?.id) {
|
||||||
@@ -145,131 +181,23 @@ export async function sendPassword(command: UpdateSessionCommand) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!authMethods || !session.factors?.user?.loginName) {
|
if (!authMethods) {
|
||||||
return { error: "Could not verify password!" };
|
return { error: "Could not verify password!" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const humanUser = user.type.case === "human" ? user.type.value : undefined;
|
const mfaFactorCheck = checkMFAFactors(
|
||||||
|
session,
|
||||||
// check if the user has to change password first
|
loginSettings,
|
||||||
if (humanUser?.passwordChangeRequired) {
|
authMethods,
|
||||||
const params = new URLSearchParams({
|
command.organization,
|
||||||
loginName: session.factors?.user?.loginName,
|
command.authRequestId,
|
||||||
});
|
|
||||||
|
|
||||||
if (command.organization || session.factors?.user?.organizationId) {
|
|
||||||
params.append("organization", session.factors?.user?.organizationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command.authRequestId) {
|
|
||||||
params.append("authRequestId", command.authRequestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { redirect: "/password/change?" + params };
|
|
||||||
}
|
|
||||||
|
|
||||||
const availableMultiFactors = authMethods?.filter(
|
|
||||||
(m: AuthenticationMethodType) =>
|
|
||||||
m !== AuthenticationMethodType.PASSWORD &&
|
|
||||||
m !== AuthenticationMethodType.PASSKEY,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (availableMultiFactors?.length == 1) {
|
if (mfaFactorCheck?.redirect) {
|
||||||
const params = new URLSearchParams({
|
return mfaFactorCheck;
|
||||||
loginName: session.factors?.user.loginName,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (command.authRequestId) {
|
|
||||||
params.append("authRequestId", command.authRequestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command.organization || session.factors?.user?.organizationId) {
|
|
||||||
params.append(
|
|
||||||
"organization",
|
|
||||||
command.organization ?? session.factors?.user?.organizationId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const factor = availableMultiFactors[0];
|
|
||||||
// if passwordless is other method, but user selected password as alternative, perform a login
|
|
||||||
if (factor === AuthenticationMethodType.TOTP) {
|
|
||||||
return { redirect: `/otp/time-based?` + params };
|
|
||||||
} else if (factor === AuthenticationMethodType.OTP_SMS) {
|
|
||||||
return { redirect: `/otp/sms?` + params };
|
|
||||||
} else if (factor === AuthenticationMethodType.OTP_EMAIL) {
|
|
||||||
return { redirect: `/otp/email?` + params };
|
|
||||||
} else if (factor === AuthenticationMethodType.U2F) {
|
|
||||||
return { redirect: `/u2f?` + params };
|
|
||||||
}
|
|
||||||
} else if (availableMultiFactors?.length >= 1) {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
loginName: session.factors.user.loginName,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (command.authRequestId) {
|
|
||||||
params.append("authRequestId", command.authRequestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command.organization || session.factors?.user?.organizationId) {
|
|
||||||
params.append(
|
|
||||||
"organization",
|
|
||||||
command.organization ?? session.factors?.user?.organizationId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { redirect: `/mfa?` + params };
|
|
||||||
}
|
}
|
||||||
// TODO: check if handling of userstate INITIAL is needed
|
|
||||||
else if (user.state === UserState.INITIAL) {
|
|
||||||
return { error: "Initial User not supported" };
|
|
||||||
} else if (
|
|
||||||
(loginSettings?.forceMfa || loginSettings?.forceMfaLocalOnly) &&
|
|
||||||
!availableMultiFactors.length
|
|
||||||
) {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
loginName: session.factors.user.loginName,
|
|
||||||
force: "true", // this defines if the mfa is forced in the settings
|
|
||||||
checkAfter: "true", // this defines if the check is directly made after the setup
|
|
||||||
});
|
|
||||||
|
|
||||||
if (command.authRequestId) {
|
if (command.authRequestId && session.id) {
|
||||||
params.append("authRequestId", command.authRequestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command.organization || session.factors?.user?.organizationId) {
|
|
||||||
params.append(
|
|
||||||
"organization",
|
|
||||||
command.organization ?? session.factors?.user?.organizationId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: provide a way to setup passkeys on mfa page?
|
|
||||||
return { redirect: `/mfa/set?` + params };
|
|
||||||
}
|
|
||||||
// TODO: implement passkey setup
|
|
||||||
|
|
||||||
// else if (
|
|
||||||
// submitted.factors &&
|
|
||||||
// !submitted.factors.webAuthN && // if session was not verified with a passkey
|
|
||||||
// promptPasswordless && // if explicitly prompted due policy
|
|
||||||
// !isAlternative // escaped if password was used as an alternative method
|
|
||||||
// ) {
|
|
||||||
// const params = new URLSearchParams({
|
|
||||||
// loginName: submitted.factors.user.loginName,
|
|
||||||
// prompt: "true",
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (authRequestId) {
|
|
||||||
// params.append("authRequestId", authRequestId);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (organization) {
|
|
||||||
// params.append("organization", organization);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return router.push(`/passkey/set?` + params);
|
|
||||||
// }
|
|
||||||
else if (command.authRequestId && session.id) {
|
|
||||||
const nextUrl = await getNextUrl(
|
const nextUrl = await getNextUrl(
|
||||||
{
|
{
|
||||||
sessionId: session.id,
|
sessionId: session.id,
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { createSessionAndUpdateCookie } from "@/lib/server/cookie";
|
import { createSessionAndUpdateCookie } from "@/lib/server/cookie";
|
||||||
import { addHumanUser, getLoginSettings } from "@/lib/zitadel";
|
import { addHumanUser, getLoginSettings, getUserByID } from "@/lib/zitadel";
|
||||||
import { create } from "@zitadel/client";
|
import { create } from "@zitadel/client";
|
||||||
import { Factors } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
import { Factors } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
||||||
import {
|
import {
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
ChecksSchema,
|
ChecksSchema,
|
||||||
} from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
} from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
||||||
import { getNextUrl } from "../client";
|
import { getNextUrl } from "../client";
|
||||||
|
import { checkEmailVerification } from "../verify-helper";
|
||||||
|
|
||||||
type RegisterUserCommand = {
|
type RegisterUserCommand = {
|
||||||
email: string;
|
email: string;
|
||||||
@@ -25,7 +26,7 @@ export type RegisterUserResponse = {
|
|||||||
factors: Factors | undefined;
|
factors: Factors | undefined;
|
||||||
};
|
};
|
||||||
export async function registerUser(command: RegisterUserCommand) {
|
export async function registerUser(command: RegisterUserCommand) {
|
||||||
const human = await addHumanUser({
|
const addResponse = await addHumanUser({
|
||||||
email: command.email,
|
email: command.email,
|
||||||
firstName: command.firstName,
|
firstName: command.firstName,
|
||||||
lastName: command.lastName,
|
lastName: command.lastName,
|
||||||
@@ -33,14 +34,14 @@ export async function registerUser(command: RegisterUserCommand) {
|
|||||||
organization: command.organization,
|
organization: command.organization,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!human) {
|
if (!addResponse) {
|
||||||
return { error: "Could not create user" };
|
return { error: "Could not create user" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const loginSettings = await getLoginSettings(command.organization);
|
const loginSettings = await getLoginSettings(command.organization);
|
||||||
|
|
||||||
let checkPayload: any = {
|
let checkPayload: any = {
|
||||||
user: { search: { case: "userId", value: human.userId } },
|
user: { search: { case: "userId", value: addResponse.userId } },
|
||||||
};
|
};
|
||||||
|
|
||||||
if (command.password) {
|
if (command.password) {
|
||||||
@@ -75,6 +76,29 @@ export async function registerUser(command: RegisterUserCommand) {
|
|||||||
|
|
||||||
return { redirect: "/passkey/set?" + params };
|
return { redirect: "/passkey/set?" + params };
|
||||||
} else {
|
} else {
|
||||||
|
const userResponse = await getUserByID(session?.factors?.user?.id);
|
||||||
|
|
||||||
|
if (!userResponse.user) {
|
||||||
|
return { error: "Could not find user" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const humanUser =
|
||||||
|
userResponse.user.type.case === "human"
|
||||||
|
? userResponse.user.type.value
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const emailVerificationCheck = checkEmailVerification(
|
||||||
|
session,
|
||||||
|
humanUser,
|
||||||
|
session.factors.user.organizationId,
|
||||||
|
command.authRequestId,
|
||||||
|
//true, // skip send as a mail was send during registration
|
||||||
|
);
|
||||||
|
|
||||||
|
if (emailVerificationCheck?.redirect) {
|
||||||
|
return emailVerificationCheck;
|
||||||
|
}
|
||||||
|
|
||||||
const url = await getNextUrl(
|
const url = await getNextUrl(
|
||||||
command.authRequestId && session.id
|
command.authRequestId && session.id
|
||||||
? {
|
? {
|
||||||
|
@@ -1,13 +1,9 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import {
|
import { setSessionAndUpdateCookie } from "@/lib/server/cookie";
|
||||||
createSessionForIdpAndUpdateCookie,
|
|
||||||
setSessionAndUpdateCookie,
|
|
||||||
} from "@/lib/server/cookie";
|
|
||||||
import {
|
import {
|
||||||
deleteSession,
|
deleteSession,
|
||||||
getLoginSettings,
|
getLoginSettings,
|
||||||
getUserByID,
|
|
||||||
listAuthenticationMethodTypes,
|
listAuthenticationMethodTypes,
|
||||||
} from "@/lib/zitadel";
|
} from "@/lib/zitadel";
|
||||||
import { Duration } from "@zitadel/client";
|
import { Duration } from "@zitadel/client";
|
||||||
@@ -23,62 +19,6 @@ import {
|
|||||||
removeSessionFromCookie,
|
removeSessionFromCookie,
|
||||||
} from "../cookies";
|
} from "../cookies";
|
||||||
|
|
||||||
type CreateNewSessionCommand = {
|
|
||||||
userId: string;
|
|
||||||
idpIntent: {
|
|
||||||
idpIntentId: string;
|
|
||||||
idpIntentToken: string;
|
|
||||||
};
|
|
||||||
loginName?: string;
|
|
||||||
password?: string;
|
|
||||||
authRequestId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function createNewSessionForIdp(options: CreateNewSessionCommand) {
|
|
||||||
const { userId, idpIntent, authRequestId } = options;
|
|
||||||
|
|
||||||
if (!userId || !idpIntent) {
|
|
||||||
throw new Error("No userId or loginName provided");
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await getUserByID(userId);
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return { error: "Could not find user" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginSettings = await getLoginSettings(user.details?.resourceOwner);
|
|
||||||
|
|
||||||
const session = await createSessionForIdpAndUpdateCookie(
|
|
||||||
userId,
|
|
||||||
idpIntent,
|
|
||||||
authRequestId,
|
|
||||||
loginSettings?.externalLoginCheckLifetime,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!session || !session.factors?.user) {
|
|
||||||
return { error: "Could not create session" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = await getNextUrl(
|
|
||||||
authRequestId && session.id
|
|
||||||
? {
|
|
||||||
sessionId: session.id,
|
|
||||||
authRequestId: authRequestId,
|
|
||||||
organization: session.factors.user.organizationId,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
loginName: session.factors.user.loginName,
|
|
||||||
organization: session.factors.user.organizationId,
|
|
||||||
},
|
|
||||||
loginSettings?.defaultRedirectUri,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (url) {
|
|
||||||
return { redirect: url };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function continueWithSession({
|
export async function continueWithSession({
|
||||||
authRequestId,
|
authRequestId,
|
||||||
...session
|
...session
|
||||||
|
342
apps/login/src/lib/server/verify.ts
Normal file
342
apps/login/src/lib/server/verify.ts
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getLoginSettings,
|
||||||
|
getSession,
|
||||||
|
getUserByID,
|
||||||
|
listAuthenticationMethodTypes,
|
||||||
|
resendEmailCode,
|
||||||
|
resendInviteCode,
|
||||||
|
verifyEmail,
|
||||||
|
verifyInviteCode,
|
||||||
|
} from "@/lib/zitadel";
|
||||||
|
import { create } from "@zitadel/client";
|
||||||
|
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
||||||
|
import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
||||||
|
import { User } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
||||||
|
import { headers } from "next/headers";
|
||||||
|
import { getNextUrl } from "../client";
|
||||||
|
import { getSessionCookieByLoginName } from "../cookies";
|
||||||
|
import { checkMFAFactors } from "../verify-helper";
|
||||||
|
import { createSessionAndUpdateCookie } from "./cookie";
|
||||||
|
|
||||||
|
type VerifyUserByEmailCommand = {
|
||||||
|
userId: string;
|
||||||
|
loginName?: string; // to determine already existing session
|
||||||
|
organization?: string;
|
||||||
|
code: string;
|
||||||
|
isInvite: boolean;
|
||||||
|
authRequestId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function sendVerification(command: VerifyUserByEmailCommand) {
|
||||||
|
const verifyResponse = command.isInvite
|
||||||
|
? await verifyInviteCode(command.userId, command.code).catch(() => {
|
||||||
|
return { error: "Could not verify invite" };
|
||||||
|
})
|
||||||
|
: await verifyEmail(command.userId, command.code).catch(() => {
|
||||||
|
return { error: "Could not verify email" };
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!verifyResponse) {
|
||||||
|
return { error: "Could not verify user" };
|
||||||
|
}
|
||||||
|
|
||||||
|
let session: Session | undefined;
|
||||||
|
let user: User | undefined;
|
||||||
|
|
||||||
|
if ("loginName" in command) {
|
||||||
|
const sessionCookie = await getSessionCookieByLoginName({
|
||||||
|
loginName: command.loginName,
|
||||||
|
organization: command.organization,
|
||||||
|
}).catch((error) => {
|
||||||
|
console.warn("Ignored error:", error);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!sessionCookie) {
|
||||||
|
return { error: "Could not load session cookie" };
|
||||||
|
}
|
||||||
|
|
||||||
|
session = await getSession({
|
||||||
|
sessionId: sessionCookie.id,
|
||||||
|
sessionToken: sessionCookie.token,
|
||||||
|
}).then((response) => {
|
||||||
|
if (response?.session) {
|
||||||
|
return response.session;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session?.factors?.user?.id) {
|
||||||
|
return { error: "Could not create session for user" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const userResponse = await getUserByID(session?.factors?.user?.id);
|
||||||
|
|
||||||
|
if (!userResponse?.user) {
|
||||||
|
return { error: "Could not load user" };
|
||||||
|
}
|
||||||
|
|
||||||
|
user = userResponse.user;
|
||||||
|
} else {
|
||||||
|
const userResponse = await getUserByID(command.userId);
|
||||||
|
|
||||||
|
if (!userResponse || !userResponse.user) {
|
||||||
|
return { error: "Could not load user" };
|
||||||
|
}
|
||||||
|
|
||||||
|
user = userResponse.user;
|
||||||
|
|
||||||
|
const checks = create(ChecksSchema, {
|
||||||
|
user: {
|
||||||
|
search: {
|
||||||
|
case: "loginName",
|
||||||
|
value: userResponse.user.preferredLoginName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
session = await createSessionAndUpdateCookie(
|
||||||
|
checks,
|
||||||
|
undefined,
|
||||||
|
command.authRequestId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!session?.factors?.user?.id) {
|
||||||
|
return { error: "Could not create session for user" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!session?.factors?.user?.id) {
|
||||||
|
return { error: "Could not create session for user" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return { error: "Could not load user" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginSettings = await getLoginSettings(user.details?.resourceOwner);
|
||||||
|
|
||||||
|
const authMethodResponse = await listAuthenticationMethodTypes(user.userId);
|
||||||
|
|
||||||
|
if (!authMethodResponse || !authMethodResponse.authMethodTypes) {
|
||||||
|
return { error: "Could not load possible authenticators" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// if no authmethods are found on the user, redirect to set one up
|
||||||
|
if (
|
||||||
|
authMethodResponse &&
|
||||||
|
authMethodResponse.authMethodTypes &&
|
||||||
|
authMethodResponse.authMethodTypes.length == 0
|
||||||
|
) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
sessionId: session.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (session.factors?.user?.loginName) {
|
||||||
|
params.set("loginName", session.factors?.user?.loginName);
|
||||||
|
}
|
||||||
|
return { redirect: `/authenticator/set?${params}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// redirect to mfa factor if user has one, or redirect to set one up
|
||||||
|
const mfaFactorCheck = checkMFAFactors(
|
||||||
|
session,
|
||||||
|
loginSettings,
|
||||||
|
authMethodResponse.authMethodTypes,
|
||||||
|
command.organization,
|
||||||
|
command.authRequestId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mfaFactorCheck?.redirect) {
|
||||||
|
return mfaFactorCheck;
|
||||||
|
}
|
||||||
|
|
||||||
|
// login user if no additional steps are required
|
||||||
|
if (command.authRequestId && session.id) {
|
||||||
|
const nextUrl = await getNextUrl(
|
||||||
|
{
|
||||||
|
sessionId: session.id,
|
||||||
|
authRequestId: command.authRequestId,
|
||||||
|
organization:
|
||||||
|
command.organization ?? session.factors?.user?.organizationId,
|
||||||
|
},
|
||||||
|
loginSettings?.defaultRedirectUri,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { redirect: nextUrl };
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = await getNextUrl(
|
||||||
|
{
|
||||||
|
loginName: session.factors.user.loginName,
|
||||||
|
organization: session.factors?.user?.organizationId,
|
||||||
|
},
|
||||||
|
loginSettings?.defaultRedirectUri,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { redirect: url };
|
||||||
|
}
|
||||||
|
|
||||||
|
type resendVerifyEmailCommand = {
|
||||||
|
userId: string;
|
||||||
|
isInvite: boolean;
|
||||||
|
authRequestId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function resendVerification(command: resendVerifyEmailCommand) {
|
||||||
|
const host = (await headers()).get("host");
|
||||||
|
|
||||||
|
return command.isInvite
|
||||||
|
? resendInviteCode(command.userId)
|
||||||
|
: resendEmailCode(command.userId, host, command.authRequestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SendVerificationRedirectWithoutCheckCommand = {
|
||||||
|
organization?: string;
|
||||||
|
authRequestId?: string;
|
||||||
|
} & (
|
||||||
|
| { userId: string; loginName?: never }
|
||||||
|
| { userId?: never; loginName: string }
|
||||||
|
);
|
||||||
|
|
||||||
|
export async function sendVerificationRedirectWithoutCheck(
|
||||||
|
command: SendVerificationRedirectWithoutCheckCommand,
|
||||||
|
) {
|
||||||
|
if (!("loginName" in command || "userId" in command)) {
|
||||||
|
return { error: "No userId, nor loginname provided" };
|
||||||
|
}
|
||||||
|
|
||||||
|
let session: Session | undefined;
|
||||||
|
let user: User | undefined;
|
||||||
|
|
||||||
|
if ("loginName" in command) {
|
||||||
|
const sessionCookie = await getSessionCookieByLoginName({
|
||||||
|
loginName: command.loginName,
|
||||||
|
organization: command.organization,
|
||||||
|
}).catch((error) => {
|
||||||
|
console.warn("Ignored error:", error);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!sessionCookie) {
|
||||||
|
return { error: "Could not load session cookie" };
|
||||||
|
}
|
||||||
|
|
||||||
|
session = await getSession({
|
||||||
|
sessionId: sessionCookie.id,
|
||||||
|
sessionToken: sessionCookie.token,
|
||||||
|
}).then((response) => {
|
||||||
|
if (response?.session) {
|
||||||
|
return response.session;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session?.factors?.user?.id) {
|
||||||
|
return { error: "Could not create session for user" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const userResponse = await getUserByID(session?.factors?.user?.id);
|
||||||
|
|
||||||
|
if (!userResponse?.user) {
|
||||||
|
return { error: "Could not load user" };
|
||||||
|
}
|
||||||
|
|
||||||
|
user = userResponse.user;
|
||||||
|
} else if ("userId" in command) {
|
||||||
|
const userResponse = await getUserByID(command.userId);
|
||||||
|
|
||||||
|
if (!userResponse?.user) {
|
||||||
|
return { error: "Could not load user" };
|
||||||
|
}
|
||||||
|
|
||||||
|
user = userResponse.user;
|
||||||
|
|
||||||
|
const checks = create(ChecksSchema, {
|
||||||
|
user: {
|
||||||
|
search: {
|
||||||
|
case: "loginName",
|
||||||
|
value: userResponse.user.preferredLoginName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
session = await createSessionAndUpdateCookie(
|
||||||
|
checks,
|
||||||
|
undefined,
|
||||||
|
command.authRequestId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!session?.factors?.user?.id) {
|
||||||
|
return { error: "Could not create session for user" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!session?.factors?.user?.id) {
|
||||||
|
return { error: "Could not create session for user" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return { error: "Could not load user" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const authMethodResponse = await listAuthenticationMethodTypes(user.userId);
|
||||||
|
|
||||||
|
if (!authMethodResponse || !authMethodResponse.authMethodTypes) {
|
||||||
|
return { error: "Could not load possible authenticators" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// if no authmethods are found on the user, redirect to set one up
|
||||||
|
if (
|
||||||
|
authMethodResponse &&
|
||||||
|
authMethodResponse.authMethodTypes &&
|
||||||
|
authMethodResponse.authMethodTypes.length == 0
|
||||||
|
) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
sessionId: session.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (session.factors?.user?.loginName) {
|
||||||
|
params.set("loginName", session.factors?.user?.loginName);
|
||||||
|
}
|
||||||
|
return { redirect: `/authenticator/set?${params}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginSettings = await getLoginSettings(user.details?.resourceOwner);
|
||||||
|
|
||||||
|
// redirect to mfa factor if user has one, or redirect to set one up
|
||||||
|
const mfaFactorCheck = checkMFAFactors(
|
||||||
|
session,
|
||||||
|
loginSettings,
|
||||||
|
authMethodResponse.authMethodTypes,
|
||||||
|
command.organization,
|
||||||
|
command.authRequestId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mfaFactorCheck?.redirect) {
|
||||||
|
return mfaFactorCheck;
|
||||||
|
}
|
||||||
|
|
||||||
|
// login user if no additional steps are required
|
||||||
|
if (command.authRequestId && session.id) {
|
||||||
|
const nextUrl = await getNextUrl(
|
||||||
|
{
|
||||||
|
sessionId: session.id,
|
||||||
|
authRequestId: command.authRequestId,
|
||||||
|
organization:
|
||||||
|
command.organization ?? session.factors?.user?.organizationId,
|
||||||
|
},
|
||||||
|
loginSettings?.defaultRedirectUri,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { redirect: nextUrl };
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = await getNextUrl(
|
||||||
|
{
|
||||||
|
loginName: session.factors.user.loginName,
|
||||||
|
organization: session.factors?.user?.organizationId,
|
||||||
|
},
|
||||||
|
loginSettings?.defaultRedirectUri,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { redirect: url };
|
||||||
|
}
|
199
apps/login/src/lib/verify-helper.ts
Normal file
199
apps/login/src/lib/verify-helper.ts
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
||||||
|
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
||||||
|
import { HumanUser } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
||||||
|
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
||||||
|
|
||||||
|
export function checkPasswordChangeRequired(
|
||||||
|
session: Session,
|
||||||
|
humanUser: HumanUser | undefined,
|
||||||
|
organization?: string,
|
||||||
|
authRequestId?: string,
|
||||||
|
) {
|
||||||
|
if (humanUser?.passwordChangeRequired) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
loginName: session.factors?.user?.loginName as string,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (organization || session.factors?.user?.organizationId) {
|
||||||
|
params.append(
|
||||||
|
"organization",
|
||||||
|
session.factors?.user?.organizationId as string,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authRequestId) {
|
||||||
|
params.append("authRequestId", authRequestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { redirect: "/password/change?" + params };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkInvite(
|
||||||
|
session: Session,
|
||||||
|
humanUser?: HumanUser,
|
||||||
|
organization?: string,
|
||||||
|
authRequestId?: string,
|
||||||
|
) {
|
||||||
|
if (humanUser?.email && humanUser.email.isVerified) {
|
||||||
|
const paramsVerify = new URLSearchParams({
|
||||||
|
loginName: session.factors?.user?.loginName as string,
|
||||||
|
userId: session.factors?.user?.id as string, // verify needs user id
|
||||||
|
invite: "true", // TODO: check - set this to true as we dont expect old email verification method here
|
||||||
|
});
|
||||||
|
|
||||||
|
if (organization || session.factors?.user?.organizationId) {
|
||||||
|
paramsVerify.append(
|
||||||
|
"organization",
|
||||||
|
organization ?? (session.factors?.user?.organizationId as string),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authRequestId) {
|
||||||
|
paramsVerify.append("authRequestId", authRequestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { redirect: "/verify?" + paramsVerify };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkEmailVerification(
|
||||||
|
session: Session,
|
||||||
|
humanUser?: HumanUser,
|
||||||
|
organization?: string,
|
||||||
|
authRequestId?: string,
|
||||||
|
skipSend?: boolean,
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
!humanUser?.email?.isVerified &&
|
||||||
|
process.env.EMAIL_VERIFICATION === "true"
|
||||||
|
) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
loginName: session.factors?.user?.loginName as string,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (authRequestId) {
|
||||||
|
params.append("authRequestId", authRequestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (organization || session.factors?.user?.organizationId) {
|
||||||
|
params.append(
|
||||||
|
"organization",
|
||||||
|
organization ?? (session.factors?.user?.organizationId as string),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skipSend) {
|
||||||
|
params.append("skipsend", "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
return { redirect: `/verify?` + params };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkMFAFactors(
|
||||||
|
session: Session,
|
||||||
|
loginSettings: LoginSettings | undefined,
|
||||||
|
authMethods: AuthenticationMethodType[],
|
||||||
|
organization?: string,
|
||||||
|
authRequestId?: string,
|
||||||
|
) {
|
||||||
|
const availableMultiFactors = authMethods?.filter(
|
||||||
|
(m: AuthenticationMethodType) =>
|
||||||
|
m !== AuthenticationMethodType.PASSWORD &&
|
||||||
|
m !== AuthenticationMethodType.PASSKEY,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (availableMultiFactors?.length == 1) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
loginName: session.factors?.user?.loginName as string,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (authRequestId) {
|
||||||
|
params.append("authRequestId", authRequestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (organization || session.factors?.user?.organizationId) {
|
||||||
|
params.append(
|
||||||
|
"organization",
|
||||||
|
organization ?? (session.factors?.user?.organizationId as string),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const factor = availableMultiFactors[0];
|
||||||
|
// if passwordless is other method, but user selected password as alternative, perform a login
|
||||||
|
if (factor === AuthenticationMethodType.TOTP) {
|
||||||
|
return { redirect: `/otp/time-based?` + params };
|
||||||
|
} else if (factor === AuthenticationMethodType.OTP_SMS) {
|
||||||
|
return { redirect: `/otp/sms?` + params };
|
||||||
|
} else if (factor === AuthenticationMethodType.OTP_EMAIL) {
|
||||||
|
return { redirect: `/otp/email?` + params };
|
||||||
|
} else if (factor === AuthenticationMethodType.U2F) {
|
||||||
|
return { redirect: `/u2f?` + params };
|
||||||
|
}
|
||||||
|
} else if (availableMultiFactors?.length >= 1) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
loginName: session.factors?.user?.loginName as string,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (authRequestId) {
|
||||||
|
params.append("authRequestId", authRequestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (organization || session.factors?.user?.organizationId) {
|
||||||
|
params.append(
|
||||||
|
"organization",
|
||||||
|
organization ?? (session.factors?.user?.organizationId as string),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { redirect: `/mfa?` + params };
|
||||||
|
} else if (
|
||||||
|
(loginSettings?.forceMfa || loginSettings?.forceMfaLocalOnly) &&
|
||||||
|
!availableMultiFactors.length
|
||||||
|
) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
loginName: session.factors?.user?.loginName as string,
|
||||||
|
force: "true", // this defines if the mfa is forced in the settings
|
||||||
|
checkAfter: "true", // this defines if the check is directly made after the setup
|
||||||
|
});
|
||||||
|
|
||||||
|
if (authRequestId) {
|
||||||
|
params.append("authRequestId", authRequestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (organization || session.factors?.user?.organizationId) {
|
||||||
|
params.append(
|
||||||
|
"organization",
|
||||||
|
organization ?? (session.factors?.user?.organizationId as string),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: provide a way to setup passkeys on mfa page?
|
||||||
|
return { redirect: `/mfa/set?` + params };
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: implement passkey setup
|
||||||
|
|
||||||
|
// else if (
|
||||||
|
// submitted.factors &&
|
||||||
|
// !submitted.factors.webAuthN && // if session was not verified with a passkey
|
||||||
|
// promptPasswordless && // if explicitly prompted due policy
|
||||||
|
// !isAlternative // escaped if password was used as an alternative method
|
||||||
|
// ) {
|
||||||
|
// const params = new URLSearchParams({
|
||||||
|
// loginName: submitted.factors.user.loginName,
|
||||||
|
// prompt: "true",
|
||||||
|
// });
|
||||||
|
|
||||||
|
// if (authRequestId) {
|
||||||
|
// params.append("authRequestId", authRequestId);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (organization) {
|
||||||
|
// params.append("organization", organization);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return router.push(`/passkey/set?` + params);
|
||||||
|
// }
|
||||||
|
}
|
@@ -12,6 +12,8 @@ import { RequestChallenges } from "@zitadel/proto/zitadel/session/v2/challenge_p
|
|||||||
import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
||||||
import {
|
import {
|
||||||
AddHumanUserRequest,
|
AddHumanUserRequest,
|
||||||
|
ResendEmailCodeRequest,
|
||||||
|
ResendEmailCodeRequestSchema,
|
||||||
RetrieveIdentityProviderIntentRequest,
|
RetrieveIdentityProviderIntentRequest,
|
||||||
SetPasswordRequest,
|
SetPasswordRequest,
|
||||||
SetPasswordRequestSchema,
|
SetPasswordRequestSchema,
|
||||||
@@ -23,6 +25,7 @@ import { create, Duration } from "@zitadel/client";
|
|||||||
import { TextQueryMethod } from "@zitadel/proto/zitadel/object/v2/object_pb";
|
import { TextQueryMethod } from "@zitadel/proto/zitadel/object/v2/object_pb";
|
||||||
import { CreateCallbackRequest } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
|
import { CreateCallbackRequest } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
|
||||||
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
|
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
|
||||||
|
import { SendEmailVerificationCodeSchema } from "@zitadel/proto/zitadel/user/v2/email_pb";
|
||||||
import type { RedirectURLsJson } from "@zitadel/proto/zitadel/user/v2/idp_pb";
|
import type { RedirectURLsJson } from "@zitadel/proto/zitadel/user/v2/idp_pb";
|
||||||
import {
|
import {
|
||||||
NotificationType,
|
NotificationType,
|
||||||
@@ -448,13 +451,28 @@ export async function verifyEmail(userId: string, verificationCode: string) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resendEmailCode(userId: string) {
|
export async function resendEmailCode(
|
||||||
return userService.resendEmailCode(
|
userId: string,
|
||||||
{
|
host: string | null,
|
||||||
userId,
|
authRequestId?: string,
|
||||||
},
|
) {
|
||||||
{},
|
let request: ResendEmailCodeRequest = create(ResendEmailCodeRequestSchema, {
|
||||||
);
|
userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (host) {
|
||||||
|
const medium = create(SendEmailVerificationCodeSchema, {
|
||||||
|
urlTemplate:
|
||||||
|
`${host.includes("localhost") ? "http://" : "https://"}${host}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` +
|
||||||
|
(authRequestId ? `&authRequestId=${authRequestId}` : ""),
|
||||||
|
});
|
||||||
|
|
||||||
|
request = { ...request, verification: { case: "sendCode", value: medium } };
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(request);
|
||||||
|
|
||||||
|
return userService.resendEmailCode(request, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function retrieveIDPIntent(id: string, token: string) {
|
export function retrieveIDPIntent(id: string, token: string) {
|
||||||
|
@@ -3,5 +3,6 @@ export { NewAuthorizationBearerInterceptor } from "./interceptors";
|
|||||||
|
|
||||||
// TODO: Move this to `./protobuf.ts` and export it from there
|
// TODO: Move this to `./protobuf.ts` and export it from there
|
||||||
export { create, fromJson, toJson } from "@bufbuild/protobuf";
|
export { create, fromJson, toJson } from "@bufbuild/protobuf";
|
||||||
|
export type { JsonObject } from "@bufbuild/protobuf";
|
||||||
export { TimestampSchema, timestampDate, timestampFromDate, timestampFromMs, timestampMs } from "@bufbuild/protobuf/wkt";
|
export { TimestampSchema, timestampDate, timestampFromDate, timestampFromMs, timestampMs } from "@bufbuild/protobuf/wkt";
|
||||||
export type { Duration, Timestamp } from "@bufbuild/protobuf/wkt";
|
export type { Duration, Timestamp } from "@bufbuild/protobuf/wkt";
|
||||||
|
@@ -2,7 +2,6 @@
|
|||||||
"name": "@zitadel/tsconfig",
|
"name": "@zitadel/tsconfig",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
|
@@ -12,7 +12,7 @@
|
|||||||
"ZITADEL_SYSTEM_API_KEY",
|
"ZITADEL_SYSTEM_API_KEY",
|
||||||
"ZITADEL_ISSUER",
|
"ZITADEL_ISSUER",
|
||||||
"ZITADEL_ADMIN_TOKEN",
|
"ZITADEL_ADMIN_TOKEN",
|
||||||
"CACHE_REVALIDATION_INTERVAL_IN_SECONDS",
|
"EMAIL_VERIFICATION",
|
||||||
"VERCEL_URL"
|
"VERCEL_URL"
|
||||||
],
|
],
|
||||||
"tasks": {
|
"tasks": {
|
||||||
|
Reference in New Issue
Block a user