Merge pull request #369 from zitadel/qa

Promote qa to prod: SAML, MFA init prompt
This commit is contained in:
Max Peintner
2025-03-03 09:31:24 +01:00
committed by GitHub
69 changed files with 1204 additions and 826 deletions

View File

@@ -16,6 +16,26 @@ FirstInstance:
ExpirationDate: 2099-01-01T00:00:00Z
DefaultInstance:
LoginPolicy:
AllowUsernamePassword: true # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_ALLOWUSERNAMEPASSWORD
AllowRegister: true # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_ALLOWREGISTER
AllowExternalIDP: true # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_ALLOWEXTERNALIDP
ForceMFA: false # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_FORCEMFA
HidePasswordReset: false # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_HIDEPASSWORDRESET
IgnoreUnknownUsernames: false # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_IGNOREUNKNOWNUSERNAMES
AllowDomainDiscovery: true # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_ALLOWDOMAINDISCOVERY
# 1 is allowed, 0 is not allowed
PasswordlessType: 1 # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_PASSWORDLESSTYPE
# DefaultRedirectURL is empty by default because we use the Console UI
DefaultRedirectURI: # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_DEFAULTREDIRECTURI
# 240h = 10d
PasswordCheckLifetime: 240h # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_PASSWORDCHECKLIFETIME
# 240h = 10d
ExternalLoginCheckLifetime: 240h # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_EXTERNALLOGINCHECKLIFETIME
# 720h = 30d
MfaInitSkipLifetime: 0h # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_MFAINITSKIPLIFETIME
SecondFactorCheckLifetime: 18h # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_SECONDFACTORCHECKLIFETIME
MultiFactorCheckLifetime: 12h # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_MULTIFACTORCHECKLIFETIME
PrivacyPolicy:
TOSLink: "https://zitadel.com/docs/legal/terms-of-service"
PrivacyLink: "https://zitadel.com/docs/legal/policies/privacy-policy"

View File

@@ -71,7 +71,8 @@
},
"set": {
"title": "2-Faktor einrichten",
"description": "Wählen Sie einen der folgenden zweiten Faktoren."
"description": "Wählen Sie einen der folgenden zweiten Faktoren.",
"skip": "Überspringen"
}
},
"otp": {

View File

@@ -71,7 +71,8 @@
},
"set": {
"title": "Set up 2-Factor",
"description": "Choose one of the following second factors."
"description": "Choose one of the following second factors.",
"skip": "Skip"
}
},
"otp": {

View File

@@ -71,7 +71,8 @@
},
"set": {
"title": "Configurar autenticación de 2 factores",
"description": "Elige uno de los siguientes factores secundarios."
"description": "Elige uno de los siguientes factores secundarios.",
"skip": "Omitir"
}
},
"otp": {

View File

@@ -71,7 +71,8 @@
},
"set": {
"title": "Configura l'autenticazione a 2 fattori",
"description": "Scegli uno dei seguenti secondi fattori."
"description": "Scegli uno dei seguenti secondi fattori.",
"skip": "Salta"
}
},
"otp": {

View File

@@ -71,7 +71,8 @@
},
"set": {
"title": "设置双因素认证",
"description": "选择以下的一个第二因素。"
"description": "选择以下的一个第二因素。",
"skip": "跳过"
}
},
"otp": {

View File

@@ -389,7 +389,6 @@ In future, self service options to jump to are shown below, like:
## Currently NOT Supported
- Login Settings: multifactor init prompt
- forceMFA on login settings is not checked for IDPs
Also note that IDP logins are considered as valid MFA. An additional MFA check will be implemented in future if enforced.

View File

@@ -36,7 +36,7 @@ export default async function Page(props: {
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "accounts" });
const authRequestId = searchParams?.authRequestId;
const requestId = searchParams?.requestId;
const organization = searchParams?.organization;
const _headers = await headers();
@@ -62,8 +62,8 @@ export default async function Page(props: {
const params = new URLSearchParams();
if (authRequestId) {
params.append("authRequestId", authRequestId);
if (requestId) {
params.append("requestId", requestId);
}
if (organization) {
@@ -77,7 +77,7 @@ export default async function Page(props: {
<p className="ztdl-p mb-6 block">{t("description")}</p>
<div className="flex flex-col w-full space-y-2">
<SessionsList sessions={sessions} authRequestId={authRequestId} />
<SessionsList sessions={sessions} requestId={requestId} />
<Link href={`/loginname?` + params}>
<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">

View File

@@ -27,7 +27,7 @@ export default async function Page(props: {
const t = await getTranslations({ locale, namespace: "authenticator" });
const tError = await getTranslations({ locale, namespace: "error" });
const { loginName, authRequestId, organization, sessionId } = searchParams;
const { loginName, requestId, organization, sessionId } = searchParams;
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
@@ -141,8 +141,8 @@ export default async function Page(props: {
params.set("organization", sessionWithData.factors?.user?.organizationId);
}
if (authRequestId) {
params.set("authRequestId", authRequestId);
if (requestId) {
params.set("requestId", requestId);
}
return (
@@ -174,7 +174,7 @@ export default async function Page(props: {
{loginSettings?.allowExternalIdp && identityProviders && (
<SignInWithIdp
identityProviders={identityProviders}
authRequestId={authRequestId}
requestId={requestId}
organization={sessionWithData.factors?.user?.organizationId}
linkOnly={true} // tell the callback function to just link the IDP and not login, to get an error when user is already available
></SignInWithIdp>

View File

@@ -36,7 +36,7 @@ export default async function Page(props: {
const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "idp" });
const { id, token, authRequestId, organization, link } = searchParams;
const { id, token, requestId, organization, link } = searchParams;
const { provider } = params;
const _headers = await headers();
@@ -68,7 +68,7 @@ export default async function Page(props: {
return loginSuccess(
userId,
{ idpIntentId: id, idpIntentToken: token },
authRequestId,
requestId,
branding,
);
}
@@ -119,7 +119,7 @@ export default async function Page(props: {
return linkingSuccess(
userId,
{ idpIntentId: id, idpIntentToken: token },
authRequestId,
requestId,
branding,
);
}
@@ -177,7 +177,7 @@ export default async function Page(props: {
return linkingSuccess(
foundUser.userId,
{ idpIntentId: id, idpIntentToken: token },
authRequestId,
requestId,
branding,
);
}
@@ -243,7 +243,7 @@ export default async function Page(props: {
<IdpSignin
userId={newUser.userId}
idpIntent={{ idpIntentId: id, idpIntentToken: token }}
authRequestId={authRequestId}
requestId={requestId}
/>
</div>
</DynamicTheme>

View File

@@ -12,7 +12,7 @@ export default async function Page(props: {
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "idp" });
const authRequestId = searchParams?.authRequestId;
const requestId = searchParams?.requestId;
const organization = searchParams?.organization;
const _headers = await headers();
@@ -41,7 +41,7 @@ export default async function Page(props: {
{identityProviders && (
<SignInWithIdp
identityProviders={identityProviders}
authRequestId={authRequestId}
requestId={requestId}
organization={organization}
></SignInWithIdp>
)}

View File

@@ -20,7 +20,7 @@ export default async function Page(props: {
const t = await getTranslations({ locale, namespace: "loginname" });
const loginName = searchParams?.loginName;
const authRequestId = searchParams?.authRequestId;
const requestId = searchParams?.requestId;
const organization = searchParams?.organization;
const suffix = searchParams?.suffix;
const submit: boolean = searchParams?.submit === "true";
@@ -72,7 +72,7 @@ export default async function Page(props: {
<UsernameForm
loginName={loginName}
authRequestId={authRequestId}
requestId={requestId}
organization={organization} // stick to "organization" as we still want to do user discovery based on the searchParams not the default organization, later the organization is determined by the found user
loginSettings={contextLoginSettings}
suffix={suffix}
@@ -82,7 +82,7 @@ export default async function Page(props: {
{identityProviders && (
<SignInWithIdp
identityProviders={identityProviders}
authRequestId={authRequestId}
requestId={requestId}
organization={organization}
></SignInWithIdp>
)}

View File

@@ -22,7 +22,7 @@ export default async function Page(props: {
const t = await getTranslations({ locale, namespace: "mfa" });
const tError = await getTranslations({ locale, namespace: "error" });
const { loginName, authRequestId, organization, sessionId } = searchParams;
const { loginName, requestId, organization, sessionId } = searchParams;
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
@@ -114,7 +114,7 @@ export default async function Page(props: {
<ChooseSecondFactor
loginName={loginName}
sessionId={sessionId}
authRequestId={authRequestId}
requestId={requestId}
organization={organization}
userMethods={sessionFactors.authMethods ?? []}
></ChooseSecondFactor>

View File

@@ -42,23 +42,17 @@ export default async function Page(props: {
const t = await getTranslations({ locale, namespace: "mfa" });
const tError = await getTranslations({ locale, namespace: "error" });
const {
loginName,
checkAfter,
force,
authRequestId,
organization,
sessionId,
} = searchParams;
const { loginName, checkAfter, force, requestId, organization, sessionId } =
searchParams;
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const sessionWithData = sessionId
? await loadSessionById(serviceUrl, sessionId, organization)
: await loadSessionByLoginname(serviceUrl, loginName, organization);
? await loadSessionById(sessionId, organization)
: await loadSessionByLoginname(loginName, organization);
async function getAuthMethodsAndUser(host: string, session?: Session) {
async function getAuthMethodsAndUser(session?: Session) {
const userId = session?.factors?.user?.id;
if (!userId) {
@@ -86,7 +80,6 @@ export default async function Page(props: {
}
async function loadSessionByLoginname(
host: string,
loginName?: string,
organization?: string,
) {
@@ -98,23 +91,18 @@ export default async function Page(props: {
organization,
},
}).then((session) => {
return getAuthMethodsAndUser(serviceUrl, session);
return getAuthMethodsAndUser(session);
});
}
async function loadSessionById(
host: string,
sessionId: string,
organization?: string,
) {
async function loadSessionById(sessionId: string, organization?: string) {
const recent = await getSessionCookieById({ sessionId, organization });
return getSession({
serviceUrl,
sessionId: recent.id,
sessionToken: recent.token,
}).then((sessionResponse) => {
return getAuthMethodsAndUser(serviceUrl, sessionResponse.session);
return getAuthMethodsAndUser(sessionResponse.session);
});
}
@@ -153,17 +141,20 @@ export default async function Page(props: {
{isSessionValid(sessionWithData).valid &&
loginSettings &&
sessionWithData && (
sessionWithData &&
sessionWithData.factors?.user?.id && (
<ChooseSecondFactorToSetup
userId={sessionWithData.factors?.user?.id}
loginName={loginName}
sessionId={sessionId}
authRequestId={authRequestId}
requestId={requestId}
organization={organization}
loginSettings={loginSettings}
userMethods={sessionWithData.authMethods ?? []}
phoneVerified={sessionWithData.phoneVerified ?? false}
emailVerified={sessionWithData.emailVerified ?? false}
checkAfter={checkAfter === "true"}
force={force === "true"}
></ChooseSecondFactorToSetup>
)}

View File

@@ -34,7 +34,7 @@ export default async function Page(props: {
const {
loginName, // send from password page
userId, // send from email link
authRequestId,
requestId,
sessionId,
organization,
code,
@@ -115,7 +115,7 @@ export default async function Page(props: {
<LoginOTP
loginName={loginName ?? session.factors?.user?.loginName}
sessionId={sessionId}
authRequestId={authRequestId}
requestId={requestId}
organization={
organization ?? session?.factors?.user?.organizationId
}

View File

@@ -29,7 +29,7 @@ export default async function Page(props: {
const t = await getTranslations({ locale, namespace: "otp" });
const tError = await getTranslations({ locale, namespace: "error" });
const { loginName, organization, sessionId, authRequestId, checkAfter } =
const { loginName, organization, sessionId, requestId, checkAfter } =
searchParams;
const { method } = params;
@@ -111,22 +111,22 @@ export default async function Page(props: {
}
if (checkAfter) {
if (authRequestId) {
paramsToContinue.append("authRequestId", authRequestId);
if (requestId) {
paramsToContinue.append("requestId", requestId);
}
urlToContinue = `/otp/${method}?` + paramsToContinue;
// immediately check the OTP on the next page if sms or email was set up
if (["email", "sms"].includes(method)) {
return redirect(urlToContinue);
}
} else if (authRequestId && sessionId) {
if (authRequestId) {
paramsToContinue.append("authRequest", authRequestId);
} else if (requestId && sessionId) {
if (requestId) {
paramsToContinue.append("authRequest", requestId);
}
urlToContinue = `/login?` + paramsToContinue;
} else if (loginName) {
if (authRequestId) {
paramsToContinue.append("authRequestId", authRequestId);
if (requestId) {
paramsToContinue.append("requestId", requestId);
}
urlToContinue = `/signedin?` + paramsToContinue;
}
@@ -165,7 +165,7 @@ export default async function Page(props: {
secret={totpResponse.secret as string}
loginName={loginName}
sessionId={sessionId}
authRequestId={authRequestId}
requestId={requestId}
organization={organization}
checkAfter={checkAfter === "true"}
loginSettings={loginSettings}

View File

@@ -17,7 +17,7 @@ export default async function Page(props: {
const t = await getTranslations({ locale, namespace: "passkey" });
const tError = await getTranslations({ locale, namespace: "error" });
const { loginName, altPassword, authRequestId, organization, sessionId } =
const { loginName, altPassword, requestId, organization, sessionId } =
searchParams;
const _headers = await headers();
@@ -76,7 +76,7 @@ export default async function Page(props: {
<LoginPasskey
loginName={loginName}
sessionId={sessionId}
authRequestId={authRequestId}
requestId={requestId}
altPassword={altPassword === "true"}
organization={organization}
/>

View File

@@ -16,8 +16,7 @@ export default async function Page(props: {
const t = await getTranslations({ locale, namespace: "passkey" });
const tError = await getTranslations({ locale, namespace: "error" });
const { loginName, prompt, organization, authRequestId, userId } =
searchParams;
const { loginName, prompt, organization, requestId, userId } = searchParams;
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
@@ -76,7 +75,7 @@ export default async function Page(props: {
sessionId={session.id}
isPrompt={!!prompt}
organization={organization}
authRequestId={authRequestId}
requestId={requestId}
/>
)}
</div>

View File

@@ -23,7 +23,7 @@ export default async function Page(props: {
const t = await getTranslations({ locale, namespace: "password" });
const tError = await getTranslations({ locale, namespace: "error" });
const { loginName, organization, authRequestId } = searchParams;
const { loginName, organization, requestId } = searchParams;
// also allow no session to be found (ignoreUnkownUsername)
const sessionFactors = await loadMostRecentSession({
@@ -84,7 +84,7 @@ export default async function Page(props: {
<ChangePasswordForm
sessionId={sessionFactors.id}
loginName={loginName}
authRequestId={authRequestId}
requestId={requestId}
organization={organization}
passwordComplexitySettings={passwordComplexity}
/>

View File

@@ -22,7 +22,7 @@ export default async function Page(props: {
const t = await getTranslations({ locale, namespace: "password" });
const tError = await getTranslations({ locale, namespace: "error" });
let { loginName, organization, authRequestId, alt } = searchParams;
let { loginName, organization, requestId, alt } = searchParams;
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
@@ -93,7 +93,7 @@ export default async function Page(props: {
{loginName && (
<PasswordForm
loginName={loginName}
authRequestId={authRequestId}
requestId={requestId}
organization={organization} // stick to "organization" as we still want to do user discovery based on the searchParams not the default organization, later the organization is determined by the found user
loginSettings={loginSettings}
promptPasswordless={

View File

@@ -23,7 +23,7 @@ export default async function Page(props: {
const t = await getTranslations({ locale, namespace: "password" });
const tError = await getTranslations({ locale, namespace: "error" });
const { userId, loginName, organization, authRequestId, code, initial } =
const { userId, loginName, organization, requestId, code, initial } =
searchParams;
const _headers = await headers();
@@ -113,7 +113,7 @@ export default async function Page(props: {
code={code}
userId={userId ?? (session?.factors?.user?.id as string)}
loginName={loginName ?? (user?.preferredLoginName as string)}
authRequestId={authRequestId}
requestId={requestId}
organization={organization}
passwordComplexitySettings={passwordComplexity}
codeRequired={!(initial === "true")}

View File

@@ -19,8 +19,7 @@ export default async function Page(props: {
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "register" });
let { firstname, lastname, email, organization, authRequestId } =
searchParams;
let { firstname, lastname, email, organization, requestId } = searchParams;
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
@@ -81,7 +80,7 @@ export default async function Page(props: {
firstname={firstname}
lastname={lastname}
email={email}
authRequestId={authRequestId}
requestId={requestId}
loginSettings={loginSettings}
></RegisterForm>
)}

View File

@@ -19,8 +19,7 @@ export default async function Page(props: {
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "register" });
let { firstname, lastname, email, organization, authRequestId } =
searchParams;
let { firstname, lastname, email, organization, requestId } = searchParams;
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
@@ -79,7 +78,7 @@ export default async function Page(props: {
firstname={firstname}
lastname={lastname}
organization={organization}
authRequestId={authRequestId}
requestId={requestId}
></SetRegisterPasswordForm>
)}
</div>

View File

@@ -6,6 +6,7 @@ import { getMostRecentCookieWithLoginname } from "@/lib/cookies";
import { getServiceUrlFromHeaders } from "@/lib/service";
import {
createCallback,
createResponse,
getBrandingSettings,
getLoginSettings,
getSession,
@@ -15,6 +16,7 @@ import {
CreateCallbackRequestSchema,
SessionSchema,
} from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
import { CreateResponseRequestSchema } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb";
import { getLocale, getTranslations } from "next-intl/server";
import { headers } from "next/headers";
import Link from "next/link";
@@ -24,16 +26,16 @@ async function loadSession(
serviceUrl: string,
loginName: string,
authRequestId?: string,
requestId?: string,
) {
const recent = await getMostRecentCookieWithLoginname({ loginName });
if (authRequestId) {
if (requestId && requestId.startsWith("oidc_")) {
return createCallback({
serviceUrl,
req: create(CreateCallbackRequestSchema, {
authRequestId,
authRequestId: requestId,
callbackKind: {
case: "session",
value: create(SessionSchema, {
@@ -45,7 +47,24 @@ async function loadSession(
}).then(({ callbackUrl }) => {
return redirect(callbackUrl);
});
} else if (requestId && requestId.startsWith("saml_")) {
return createResponse({
serviceUrl,
req: create(CreateResponseRequestSchema, {
samlRequestId: requestId.replace("saml_", ""),
responseKind: {
case: "session",
value: {
sessionId: recent.id,
sessionToken: recent.token,
},
},
}),
}).then(({ url }) => {
return redirect(url);
});
}
return getSession({
serviceUrl,
@@ -66,12 +85,12 @@ export default async function Page(props: { searchParams: Promise<any> }) {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const { loginName, authRequestId, organization } = searchParams;
const { loginName, requestId, organization } = searchParams;
const sessionFactors = await loadSession(
serviceUrl,
loginName,
authRequestId,
requestId,
);
const branding = await getBrandingSettings({
@@ -81,7 +100,7 @@ export default async function Page(props: { searchParams: Promise<any> }) {
});
let loginSettings;
if (!authRequestId) {
if (!requestId) {
loginSettings = await getLoginSettings({
serviceUrl,

View File

@@ -17,7 +17,7 @@ export default async function Page(props: {
const t = await getTranslations({ locale, namespace: "u2f" });
const tError = await getTranslations({ locale, namespace: "error" });
const { loginName, authRequestId, sessionId, organization } = searchParams;
const { loginName, requestId, sessionId, organization } = searchParams;
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
@@ -80,7 +80,7 @@ export default async function Page(props: {
<LoginPasskey
loginName={loginName}
sessionId={sessionId}
authRequestId={authRequestId}
requestId={requestId}
altPassword={false}
organization={organization}
login={false} // this sets the userVerificationRequirement to discouraged as its used as second factor

View File

@@ -16,7 +16,7 @@ export default async function Page(props: {
const t = await getTranslations({ locale, namespace: "u2f" });
const tError = await getTranslations({ locale, namespace: "error" });
const { loginName, organization, authRequestId, checkAfter } = searchParams;
const { loginName, organization, requestId, checkAfter } = searchParams;
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
@@ -62,7 +62,7 @@ export default async function Page(props: {
loginName={loginName}
sessionId={sessionFactors.id}
organization={organization}
authRequestId={authRequestId}
requestId={requestId}
checkAfter={checkAfter === "true"}
/>
)}

View File

@@ -22,7 +22,7 @@ export default async function Page(props: { searchParams: Promise<any> }) {
const t = await getTranslations({ locale, namespace: "verify" });
const tError = await getTranslations({ locale, namespace: "error" });
const { userId, loginName, code, organization, authRequestId, invite } =
const { userId, loginName, code, organization, requestId, invite } =
searchParams;
const _headers = await headers();
@@ -64,7 +64,7 @@ export default async function Page(props: { searchParams: Promise<any> }) {
userId: sessionFactors?.factors?.user?.id,
urlTemplate:
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` +
(authRequestId ? `&authRequestId=${authRequestId}` : ""),
(requestId ? `&requestId=${requestId}` : ""),
}).catch((error) => {
console.error("Could not resend verification email", error);
throw Error("Failed to send verification email");
@@ -77,7 +77,7 @@ export default async function Page(props: { searchParams: Promise<any> }) {
userId,
urlTemplate:
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` +
(authRequestId ? `&authRequestId=${authRequestId}` : ""),
(requestId ? `&requestId=${requestId}` : ""),
}).catch((error) => {
console.error("Could not resend verification email", error);
throw Error("Failed to send verification email");
@@ -120,8 +120,8 @@ export default async function Page(props: { searchParams: Promise<any> }) {
params.set("organization", organization);
}
if (authRequestId) {
params.set("authRequestId", authRequestId);
if (requestId) {
params.set("requestId", requestId);
}
return (
@@ -165,7 +165,7 @@ export default async function Page(props: { searchParams: Promise<any> }) {
userId={id}
loginName={loginName}
organization={organization}
authRequestId={authRequestId}
requestId={requestId}
authMethods={authMethods}
/>
) : (
@@ -176,7 +176,7 @@ export default async function Page(props: { searchParams: Promise<any> }) {
userId={id}
code={code}
isInvite={invite === "true"}
authRequestId={authRequestId}
requestId={requestId}
/>
))}
</div>

View File

@@ -1,28 +1,28 @@
import { getAllSessions } from "@/lib/cookies";
import { idpTypeToSlug } from "@/lib/idp";
import { loginWithOIDCandSession } from "@/lib/oidc";
import { loginWithSAMLandSession } from "@/lib/saml";
import { sendLoginname, SendLoginnameCommand } from "@/lib/server/loginname";
import { getServiceUrlFromHeaders } from "@/lib/service";
import { findValidSession } from "@/lib/session";
import {
createCallback,
createResponse,
getActiveIdentityProviders,
getAuthRequest,
getLoginSettings,
getOrgsByDomain,
listAuthenticationMethodTypes,
getSAMLRequest,
listSessions,
startIdentityProviderFlow,
} from "@/lib/zitadel";
import { create, timestampDate } from "@zitadel/client";
import {
AuthRequest,
Prompt,
} from "@zitadel/proto/zitadel/oidc/v2/authorization_pb";
import { create } from "@zitadel/client";
import { Prompt } from "@zitadel/proto/zitadel/oidc/v2/authorization_pb";
import {
CreateCallbackRequestSchema,
SessionSchema,
} from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
import { CreateResponseRequestSchema } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { headers } from "next/headers";
import { NextRequest, NextResponse } from "next/server";
@@ -30,12 +30,32 @@ export const dynamic = "force-dynamic";
export const revalidate = false;
export const fetchCache = "default-no-store";
const gotoAccounts = ({
request,
requestId,
organization,
}: {
request: NextRequest;
requestId: string;
organization?: string;
}): NextResponse<unknown> => {
const accountsUrl = new URL("/accounts", request.url);
if (requestId) {
accountsUrl.searchParams.set("requestId", requestId);
}
if (organization) {
accountsUrl.searchParams.set("organization", organization);
}
return NextResponse.redirect(accountsUrl);
};
async function loadSessions({
serviceUrl,
ids,
}: {
serviceUrl: string;
ids: string[];
}): Promise<Session[]> {
const response = await listSessions({
@@ -50,175 +70,23 @@ const ORG_SCOPE_REGEX = /urn:zitadel:iam:org:id:([0-9]+)/;
const ORG_DOMAIN_SCOPE_REGEX = /urn:zitadel:iam:org:domain:primary:(.+)/; // TODO: check regex for all domain character options
const IDP_SCOPE_REGEX = /urn:zitadel:iam:org:idp:id:(.+)/;
/**
* 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);
**/
async function isSessionValid(
serviceUrl: string,
session: Session,
): Promise<boolean> {
// session can't be checked without user
if (!session.factors?.user) {
console.warn("Session has no user");
return false;
}
let mfaValid = true;
const authMethodTypes = await listAuthenticationMethodTypes({
serviceUrl,
userId: session.factors.user.id,
});
const authMethods = authMethodTypes.authMethodTypes;
if (authMethods && authMethods.includes(AuthenticationMethodType.TOTP)) {
mfaValid = !!session.factors.totp?.verifiedAt;
if (!mfaValid) {
console.warn(
"Session has no valid totpEmail factor",
session.factors.totp?.verifiedAt,
);
}
} else if (
authMethods &&
authMethods.includes(AuthenticationMethodType.OTP_EMAIL)
) {
mfaValid = !!session.factors.otpEmail?.verifiedAt;
if (!mfaValid) {
console.warn(
"Session has no valid otpEmail factor",
session.factors.otpEmail?.verifiedAt,
);
}
} else if (
authMethods &&
authMethods.includes(AuthenticationMethodType.OTP_SMS)
) {
mfaValid = !!session.factors.otpSms?.verifiedAt;
if (!mfaValid) {
console.warn(
"Session has no valid otpSms factor",
session.factors.otpSms?.verifiedAt,
);
}
} else if (
authMethods &&
authMethods.includes(AuthenticationMethodType.U2F)
) {
mfaValid = !!session.factors.webAuthN?.verifiedAt;
if (!mfaValid) {
console.warn(
"Session has no valid u2f factor",
session.factors.webAuthN?.verifiedAt,
);
}
} else {
// only check settings if no auth methods are available, as this would require a setup
const loginSettings = await getLoginSettings({
serviceUrl,
organization: session.factors?.user?.organizationId,
});
if (loginSettings?.forceMfa || loginSettings?.forceMfaLocalOnly) {
const otpEmail = session.factors.otpEmail?.verifiedAt;
const otpSms = session.factors.otpSms?.verifiedAt;
const totp = session.factors.totp?.verifiedAt;
const webAuthN = session.factors.webAuthN?.verifiedAt;
const idp = session.factors.intent?.verifiedAt; // TODO: forceMFA should not consider this as valid factor
// must have one single check
mfaValid = !!(otpEmail || otpSms || totp || webAuthN || idp);
if (!mfaValid) {
console.warn("Session has no valid multifactor", session.factors);
}
} else {
mfaValid = true;
}
}
const validPassword = session?.factors?.password?.verifiedAt;
const validPasskey = session?.factors?.webAuthN?.verifiedAt;
const validIDP = session?.factors?.intent?.verifiedAt;
const stillValid = session.expirationDate
? timestampDate(session.expirationDate).getTime() > new Date().getTime()
: true;
if (!stillValid) {
console.warn(
"Session is expired",
session.expirationDate
? timestampDate(session.expirationDate).toDateString()
: "no expiration date",
);
}
const validChecks = !!(validPassword || validPasskey || validIDP);
return stillValid && validChecks && mfaValid;
}
async function findValidSession(
serviceUrl: string,
sessions: Session[],
authRequest: AuthRequest,
): Promise<Session | undefined> {
const sessionsWithHint = sessions.filter((s) => {
if (authRequest.hintUserId) {
return s.factors?.user?.id === authRequest.hintUserId;
}
if (authRequest.loginHint) {
return s.factors?.user?.loginName === authRequest.loginHint;
}
return true;
});
if (sessionsWithHint.length === 0) {
return undefined;
}
// sort by change date descending
sessionsWithHint.sort((a, b) => {
const dateA = a.changeDate ? timestampDate(a.changeDate).getTime() : 0;
const dateB = b.changeDate ? timestampDate(b.changeDate).getTime() : 0;
return dateB - dateA;
});
// return the first valid session according to settings
for (const session of sessionsWithHint) {
if (await isSessionValid(serviceUrl, session)) {
return session;
}
}
return undefined;
}
function constructUrl(request: NextRequest, path: string) {
const forwardedHost =
request.headers.get("x-zitadel-forward-host") ??
request.headers.get("host");
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || "";
return new URL(
`${basePath}${path}`,
forwardedHost?.startsWith("http")
? forwardedHost
: `https://${forwardedHost}`,
);
}
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const authRequestId = searchParams.get("authRequest");
const sessionId = searchParams.get("sessionId");
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const searchParams = request.nextUrl.searchParams;
const oidcRequestId = searchParams.get("authRequest"); // oidc initiated request
const samlRequestId = searchParams.get("samlRequest"); // saml initiated request
// internal request id which combines authRequest and samlRequest with the prefix oidc_ or saml_
let requestId =
searchParams.get("requestId") ||
`oidc_${oidcRequestId}` ||
`saml_${samlRequestId}`;
const sessionId = searchParams.get("sessionId");
// TODO: find a better way to handle _rsc (react server components) requests and block them to avoid conflicts when creating oidc callback
const _rsc = searchParams.get("_rsc");
if (_rsc) {
@@ -232,128 +100,36 @@ export async function GET(request: NextRequest) {
sessions = await loadSessions({ serviceUrl, ids });
}
if (authRequestId && sessionId) {
console.log(
`Login with session: ${sessionId} and authRequest: ${authRequestId}`,
);
const selectedSession = sessions.find((s) => s.id === sessionId);
if (selectedSession && selectedSession.id) {
console.log(`Found session ${selectedSession.id}`);
const isValid = await isSessionValid(
// complete flow if session and request id are provided
if (requestId && sessionId) {
if (requestId.startsWith("oidc_")) {
// this finishes the login process for OIDC
return loginWithOIDCandSession({
serviceUrl,
selectedSession,
);
console.log("Session is valid:", isValid);
if (!isValid && selectedSession.factors?.user) {
// if the session is not valid anymore, we need to redirect the user to re-authenticate /
// TODO: handle IDP intent direcly if available
const command: SendLoginnameCommand = {
loginName: selectedSession.factors.user?.loginName,
organization: selectedSession.factors?.user?.organizationId,
authRequestId: authRequestId,
};
const res = await sendLoginname(command);
if (res && "redirect" in res && res?.redirect) {
const absoluteUrl = new URL(res.redirect, request.nextUrl);
return NextResponse.redirect(absoluteUrl.toString());
}
}
const cookie = sessionCookies.find(
(cookie) => cookie.id === selectedSession?.id,
);
if (cookie && cookie.id && cookie.token) {
const session = {
sessionId: cookie?.id,
sessionToken: cookie?.token,
};
// works not with _rsc request
try {
const { callbackUrl } = await createCallback({
serviceUrl,
req: create(CreateCallbackRequestSchema, {
authRequestId,
callbackKind: {
case: "session",
value: create(SessionSchema, session),
},
}),
authRequest: requestId.replace("oidc_", ""),
sessionId,
sessions,
sessionCookies,
request,
});
if (callbackUrl) {
return NextResponse.redirect(callbackUrl);
} else {
return NextResponse.json(
{ error: "An error occurred!" },
{ status: 500 },
);
}
} catch (error: unknown) {
// handle already handled gracefully as these could come up if old emails with authRequestId are used (reset password, register emails etc.)
console.error(error);
if (
error &&
typeof error === "object" &&
"code" in error &&
error?.code === 9
) {
const loginSettings = await getLoginSettings({
} else if (requestId.startsWith("saml_")) {
// this finishes the login process for SAML
return loginWithSAMLandSession({
serviceUrl,
organization: selectedSession.factors?.user?.organizationId,
samlRequest: requestId.replace("saml_", ""),
sessionId,
sessions,
sessionCookies,
request,
});
if (loginSettings?.defaultRedirectUri) {
return NextResponse.redirect(loginSettings.defaultRedirectUri);
}
const signedinUrl = constructUrl(request, "/signedin");
const params = new URLSearchParams();
if (selectedSession.factors?.user?.loginName) {
params.append(
"loginName",
selectedSession.factors?.user?.loginName,
);
// signedinUrl.searchParams.set(
// "loginName",
// selectedSession.factors?.user?.loginName,
// );
}
if (selectedSession.factors?.user?.organizationId) {
params.append(
"organization",
selectedSession.factors?.user?.organizationId,
);
// signedinUrl.searchParams.set(
// "organization",
// selectedSession.factors?.user?.organizationId,
// );
}
return NextResponse.redirect(signedinUrl + "?" + params);
} else {
return NextResponse.json({ error }, { status: 500 });
}
}
}
}
}
if (authRequestId) {
// continue with OIDC
if (requestId && requestId.startsWith("oidc_")) {
const { authRequest } = await getAuthRequest({
serviceUrl,
authRequestId,
authRequestId: requestId.replace("oidc_", ""),
});
let organization = "";
@@ -400,7 +176,6 @@ export async function GET(request: NextRequest) {
const identityProviders = await getActiveIdentityProviders({
serviceUrl,
orgId: organization ? organization : undefined,
}).then((resp) => {
return resp.identityProviders;
@@ -416,8 +191,8 @@ export async function GET(request: NextRequest) {
const params = new URLSearchParams();
if (authRequestId) {
params.set("authRequestId", authRequestId);
if (requestId) {
params.set("requestId", requestId);
}
if (organization) {
@@ -426,7 +201,6 @@ export async function GET(request: NextRequest) {
return startIdentityProviderFlow({
serviceUrl,
idpId,
urls: {
successUrl:
@@ -448,41 +222,27 @@ export async function GET(request: NextRequest) {
}
}
const gotoAccounts = (): NextResponse<unknown> => {
const accountsUrl = constructUrl(request, "/accounts");
const params = new URLSearchParams();
if (authRequest?.id) {
params.append("authRequestId", authRequest.id);
// accountsUrl.searchParams.set("authRequestId", authRequest?.id);
}
if (organization) {
params.append("organization", organization);
// accountsUrl.searchParams.set("organization", organization);
}
return NextResponse.redirect(accountsUrl + "?" + params);
};
if (authRequest && authRequest.prompt.includes(Prompt.CREATE)) {
const registerUrl = constructUrl(request, "/register");
const params = new URLSearchParams();
const registerUrl = new URL("/register", request.url);
if (authRequest.id) {
params.append("authRequestId", authRequest.id);
// registerUrl.searchParams.set("authRequestId", authRequest.id);
registerUrl.searchParams.set("requestId", `oidc_${authRequest.id}`);
}
if (organization) {
params.append("organization", organization);
// registerUrl.searchParams.set("organization", organization);
registerUrl.searchParams.set("organization", organization);
}
return NextResponse.redirect(registerUrl + "?" + params);
return NextResponse.redirect(registerUrl);
}
// use existing session and hydrate it for oidc
if (authRequest && sessions.length) {
// if some accounts are available for selection and select_account is set
if (authRequest.prompt.includes(Prompt.SELECT_ACCOUNT)) {
return gotoAccounts();
return gotoAccounts({
request,
requestId: `oidc_${authRequest.id}`,
organization,
});
} else if (authRequest.prompt.includes(Prompt.LOGIN)) {
/**
* The login prompt instructs the authentication server to prompt the user for re-authentication, regardless of whether the user is already authenticated
@@ -493,7 +253,7 @@ export async function GET(request: NextRequest) {
try {
let command: SendLoginnameCommand = {
loginName: authRequest.loginHint,
authRequestId: authRequest.id,
requestId: authRequest.id,
};
if (organization) {
@@ -503,7 +263,7 @@ export async function GET(request: NextRequest) {
const res = await sendLoginname(command);
if (res && "redirect" in res && res?.redirect) {
const absoluteUrl = new URL(res.redirect, request.nextUrl);
const absoluteUrl = new URL(res.redirect, request.url);
return NextResponse.redirect(absoluteUrl.toString());
}
} catch (error) {
@@ -511,39 +271,31 @@ export async function GET(request: NextRequest) {
}
}
const loginNameUrl = constructUrl(request, "/loginname");
const params = new URLSearchParams();
const loginNameUrl = new URL("/loginname", request.url);
if (authRequest.id) {
params.append("authRequestId", authRequest.id);
// loginNameUrl.searchParams.set("authRequestId", authRequest.id);
loginNameUrl.searchParams.set("requestId", `oidc_${authRequest.id}`);
}
if (authRequest.loginHint) {
params.append("loginName", authRequest.loginHint);
// loginNameUrl.searchParams.set("loginName", authRequest.loginHint);
loginNameUrl.searchParams.set("loginName", authRequest.loginHint);
}
if (organization) {
params.append("organization", organization);
// loginNameUrl.searchParams.set("organization", organization);
loginNameUrl.searchParams.set("organization", organization);
}
if (suffix) {
params.append("suffix", suffix);
// loginNameUrl.searchParams.set("suffix", suffix);
loginNameUrl.searchParams.set("suffix", suffix);
}
return NextResponse.redirect(loginNameUrl + "?" + params);
return NextResponse.redirect(loginNameUrl);
} else if (authRequest.prompt.includes(Prompt.NONE)) {
/**
* With an OIDC none prompt, the authentication server must not display any authentication or consent user interface pages.
* This means that the user should not be prompted to enter their password again.
* Instead, the server attempts to silently authenticate the user using an existing session or other authentication mechanisms that do not require user interaction
**/
const selectedSession = await findValidSession(
const selectedSession = await findValidSession({
serviceUrl,
sessions,
authRequest,
);
});
if (!selectedSession || !selectedSession.id) {
return NextResponse.json(
@@ -570,9 +322,8 @@ export async function GET(request: NextRequest) {
const { callbackUrl } = await createCallback({
serviceUrl,
req: create(CreateCallbackRequestSchema, {
authRequestId,
authRequestId: requestId.replace("oidc_", ""),
callbackKind: {
case: "session",
value: create(SessionSchema, session),
@@ -582,15 +333,18 @@ export async function GET(request: NextRequest) {
return NextResponse.redirect(callbackUrl);
} else {
// check for loginHint, userId hint and valid sessions
let selectedSession = await findValidSession(
let selectedSession = await findValidSession({
serviceUrl,
sessions,
authRequest,
);
});
if (!selectedSession || !selectedSession.id) {
return gotoAccounts();
return gotoAccounts({
request,
requestId: `oidc_${authRequest.id}`,
organization,
});
}
const cookie = sessionCookies.find(
@@ -598,7 +352,11 @@ export async function GET(request: NextRequest) {
);
if (!cookie || !cookie.id || !cookie.token) {
return gotoAccounts();
return gotoAccounts({
request,
requestId: `oidc_${authRequest.id}`,
organization,
});
}
const session = {
@@ -611,7 +369,7 @@ export async function GET(request: NextRequest) {
serviceUrl,
req: create(CreateCallbackRequestSchema, {
authRequestId,
authRequestId: requestId.replace("oidc_", ""),
callbackKind: {
case: "session",
value: create(SessionSchema, session),
@@ -624,36 +382,148 @@ export async function GET(request: NextRequest) {
console.log(
"could not create callback, redirect user to choose other account",
);
return gotoAccounts();
return gotoAccounts({
request,
organization,
requestId: `oidc_${authRequest.id}`,
});
}
} catch (error) {
console.error(error);
return gotoAccounts();
return gotoAccounts({
request,
requestId: `oidc_${authRequest.id}`,
organization,
});
}
}
} else {
const loginNameUrl = constructUrl(request, "/loginname");
const loginNameUrl = new URL("/loginname", request.url);
const params = new URLSearchParams();
params.set("authRequestId", authRequestId);
// loginNameUrl.searchParams.set("authRequestId", authRequestId);
loginNameUrl.searchParams.set("requestId", requestId);
if (authRequest?.loginHint) {
params.set("loginName", authRequest.loginHint);
params.set("submit", "true"); // autosubmit
// loginNameUrl.searchParams.set("loginName", authRequest.loginHint);
// loginNameUrl.searchParams.set("submit", "true"); // autosubmit
loginNameUrl.searchParams.set("loginName", authRequest.loginHint);
loginNameUrl.searchParams.set("submit", "true"); // autosubmit
}
if (organization) {
params.set("organization", organization);
loginNameUrl.searchParams.append("organization", organization);
// loginNameUrl.searchParams.set("organization", organization);
}
return NextResponse.redirect(loginNameUrl + "?" + params);
return NextResponse.redirect(loginNameUrl);
}
}
// continue with SAML
else if (requestId && requestId.startsWith("saml_")) {
const { samlRequest } = await getSAMLRequest({
serviceUrl,
samlRequestId: requestId.replace("saml_", ""),
});
if (!samlRequest) {
return NextResponse.json(
{ error: "No samlRequest found" },
{ status: 400 },
);
}
let selectedSession = await findValidSession({
serviceUrl,
sessions,
samlRequest,
});
if (!selectedSession || !selectedSession.id) {
return gotoAccounts({
request,
requestId: `saml_${samlRequest.id}`,
});
}
const cookie = sessionCookies.find(
(cookie) => cookie.id === selectedSession.id,
);
if (!cookie || !cookie.id || !cookie.token) {
return gotoAccounts({
request,
requestId: `saml_${samlRequest.id}`,
// organization,
});
}
const session = {
sessionId: cookie.id,
sessionToken: cookie.token,
};
try {
const { url, binding } = await createResponse({
serviceUrl,
req: create(CreateResponseRequestSchema, {
samlRequestId: requestId.replace("saml_", ""),
responseKind: {
case: "session",
value: session,
},
}),
});
if (url && binding.case === "redirect") {
return NextResponse.redirect(url);
} else if (url && binding.case === "post") {
const formData = {
key1: "value1",
key2: "value2",
};
// Convert form data to URL-encoded string
const formBody = Object.entries(formData)
.map(
([key, value]) =>
encodeURIComponent(key) + "=" + encodeURIComponent(value),
)
.join("&");
// Make a POST request to the external URL with the form data
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: formBody,
});
// Handle the response from the external URL
if (response.ok) {
return NextResponse.json({
message: "SAML request completed successfully",
});
} else {
return NextResponse.json(
{ error: "Failed to complete SAML request" },
{ status: response.status },
);
}
} else {
console.log(
"could not create response, redirect user to choose other account",
);
return gotoAccounts({
request,
requestId: `saml_${samlRequest.id}`,
});
}
} catch (error) {
console.error(error);
return gotoAccounts({
request,
requestId: `saml_${samlRequest.id}`,
});
}
} else {
return NextResponse.json(
{ error: "No authRequestId provided" },
{ error: "No authRequest nor samlRequest provided" },
{ status: 500 },
);
}

View File

@@ -35,7 +35,7 @@ type Props = {
passwordComplexitySettings: PasswordComplexitySettings;
sessionId: string;
loginName: string;
authRequestId?: string;
requestId?: string;
organization?: string;
};
@@ -43,7 +43,7 @@ export function ChangePasswordForm({
passwordComplexitySettings,
sessionId,
loginName,
authRequestId,
requestId,
organization,
}: Props) {
const t = useTranslations("password");
@@ -97,7 +97,7 @@ export function ChangePasswordForm({
checks: create(ChecksSchema, {
password: { password: values.password },
}),
authRequestId,
requestId,
})
.catch(() => {
setError("Could not verify password");

View File

@@ -1,35 +1,44 @@
"use client";
import { skipMFAAndContinueWithNextUrl } from "@/lib/server/session";
import {
LoginSettings,
SecondFactorType,
} from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { EMAIL, SMS, TOTP, U2F } from "./auth-methods";
type Props = {
userId: string;
loginName?: string;
sessionId?: string;
authRequestId?: string;
requestId?: string;
organization?: string;
loginSettings: LoginSettings;
userMethods: AuthenticationMethodType[];
checkAfter: boolean;
phoneVerified: boolean;
emailVerified: boolean;
force: boolean;
};
export function ChooseSecondFactorToSetup({
userId,
loginName,
sessionId,
authRequestId,
requestId,
organization,
loginSettings,
userMethods,
checkAfter,
phoneVerified,
emailVerified,
force,
}: Props) {
const t = useTranslations("mfa");
const router = useRouter();
const params = new URLSearchParams({});
if (loginName) {
@@ -38,8 +47,8 @@ export function ChooseSecondFactorToSetup({
if (sessionId) {
params.append("sessionId", sessionId);
}
if (authRequestId) {
params.append("authRequestId", authRequestId);
if (requestId) {
params.append("requestId", requestId);
}
if (organization) {
params.append("organization", organization);
@@ -49,6 +58,7 @@ export function ChooseSecondFactorToSetup({
}
return (
<>
<div className="grid grid-cols-1 gap-5 w-full pt-4">
{loginSettings.secondFactors.map((factor) => {
switch (factor) {
@@ -83,5 +93,28 @@ export function ChooseSecondFactorToSetup({
}
})}
</div>
{!force && (
<button
className="transition-all text-sm hover:text-primary-light-500 dark:hover:text-primary-dark-500"
onClick={async () => {
const resp = await skipMFAAndContinueWithNextUrl({
userId,
loginName,
sessionId,
organization,
requestId,
});
if (resp?.redirect) {
return router.push(resp.redirect);
}
}}
type="button"
data-testid="reset-button"
>
{t("set.skip")}
</button>
)}
</>
);
}

View File

@@ -6,7 +6,7 @@ import { EMAIL, SMS, TOTP, U2F } from "./auth-methods";
type Props = {
loginName?: string;
sessionId?: string;
authRequestId?: string;
requestId?: string;
organization?: string;
userMethods: AuthenticationMethodType[];
};
@@ -14,7 +14,7 @@ type Props = {
export function ChooseSecondFactor({
loginName,
sessionId,
authRequestId,
requestId,
organization,
userMethods,
}: Props) {
@@ -26,8 +26,8 @@ export function ChooseSecondFactor({
if (sessionId) {
params.append("sessionId", sessionId);
}
if (authRequestId) {
params.append("authRequestId", authRequestId);
if (requestId) {
params.append("requestId", requestId);
}
if (organization) {
params.append("organization", organization);

View File

@@ -13,13 +13,13 @@ type Props = {
idpIntentId: string;
idpIntentToken: string;
};
authRequestId?: string;
requestId?: string;
};
export function IdpSignin({
userId,
idpIntent: { idpIntentId, idpIntentToken },
authRequestId,
requestId,
}: Props) {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@@ -33,7 +33,7 @@ export function IdpSignin({
idpIntentId,
idpIntentToken,
},
authRequestId,
requestId,
})
.then((response) => {
if (response && "error" in response && response?.error) {

View File

@@ -6,7 +6,7 @@ import { IdpSignin } from "../../idp-signin";
export async function linkingSuccess(
userId: string,
idpIntent: { idpIntentId: string; idpIntentToken: string },
authRequestId?: string,
requestId?: string,
branding?: BrandingSettings,
) {
const locale = getLocale();
@@ -21,7 +21,7 @@ export async function linkingSuccess(
<IdpSignin
userId={userId}
idpIntent={idpIntent}
authRequestId={authRequestId}
requestId={requestId}
/>
</div>
</DynamicTheme>

View File

@@ -6,7 +6,7 @@ import { IdpSignin } from "../../idp-signin";
export async function loginSuccess(
userId: string,
idpIntent: { idpIntentId: string; idpIntentToken: string },
authRequestId?: string,
requestId?: string,
branding?: BrandingSettings,
) {
const locale = getLocale();
@@ -21,7 +21,7 @@ export async function loginSuccess(
<IdpSignin
userId={userId}
idpIntent={idpIntent}
authRequestId={authRequestId}
requestId={requestId}
/>
</div>
</DynamicTheme>

View File

@@ -21,7 +21,7 @@ type Props = {
host: string | null;
loginName?: string;
sessionId?: string;
authRequestId?: string;
requestId?: string;
organization?: string;
method: string;
code?: string;
@@ -36,7 +36,7 @@ export function LoginOTP({
host,
loginName,
sessionId,
authRequestId,
requestId,
organization,
method,
code,
@@ -87,7 +87,7 @@ export function LoginOTP({
? {
urlTemplate:
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/otp/${method}?code={{.Code}}&userId={{.UserID}}&sessionId={{.SessionID}}` +
(authRequestId ? `&authRequestId=${authRequestId}` : ""),
(requestId ? `&requestId=${requestId}` : ""),
}
: {},
},
@@ -107,7 +107,7 @@ export function LoginOTP({
sessionId,
organization,
challenges,
authRequestId,
requestId,
})
.catch(() => {
setError("Could not request OTP challenge");
@@ -137,8 +137,8 @@ export function LoginOTP({
body.organization = organization;
}
if (authRequestId) {
body.authRequestId = authRequestId;
if (requestId) {
body.requestId = requestId;
}
let checks;
@@ -164,7 +164,7 @@ export function LoginOTP({
sessionId,
organization,
checks,
authRequestId,
requestId,
})
.catch(() => {
setError("Could not verify OTP code");
@@ -190,11 +190,11 @@ export function LoginOTP({
await new Promise((resolve) => setTimeout(resolve, 2000));
const url =
authRequestId && response.sessionId
requestId && response.sessionId
? await getNextUrl(
{
sessionId: response.sessionId,
authRequestId: authRequestId,
requestId: requestId,
organization: response.factors?.user?.organizationId,
},
loginSettings?.defaultRedirectUri,

View File

@@ -21,7 +21,7 @@ import { Spinner } from "./spinner";
type Props = {
loginName?: string;
sessionId?: string;
authRequestId?: string;
requestId?: string;
altPassword: boolean;
login?: boolean;
organization?: string;
@@ -30,7 +30,7 @@ type Props = {
export function LoginPasskey({
loginName,
sessionId,
authRequestId,
requestId,
altPassword,
organization,
login = true,
@@ -96,7 +96,7 @@ export function LoginPasskey({
userVerificationRequirement,
},
}),
authRequestId,
requestId,
})
.catch(() => {
setError("Could not request passkey challenge");
@@ -123,7 +123,7 @@ export function LoginPasskey({
checks: {
webAuthN: { credentialAssertionData: data },
} as Checks,
authRequestId,
requestId,
})
.catch(() => {
setError("Could not verify passkey");
@@ -220,8 +220,8 @@ export function LoginPasskey({
params.sessionId = sessionId;
}
if (authRequestId) {
params.authRequestId = authRequestId;
if (requestId) {
params.requestId = requestId;
}
if (organization) {

View File

@@ -22,7 +22,7 @@ type Props = {
loginSettings: LoginSettings | undefined;
loginName: string;
organization?: string;
authRequestId?: string;
requestId?: string;
isAlternative?: boolean; // whether password was requested as alternative auth method
promptPasswordless?: boolean;
};
@@ -31,7 +31,7 @@ export function PasswordForm({
loginSettings,
loginName,
organization,
authRequestId,
requestId,
promptPasswordless,
isAlternative,
}: Props) {
@@ -58,7 +58,7 @@ export function PasswordForm({
checks: create(ChecksSchema, {
password: { password: values.password },
}),
authRequestId,
requestId,
})
.catch(() => {
setError("Could not verify password");
@@ -86,7 +86,7 @@ export function PasswordForm({
const response = await resetPassword({
loginName,
organization,
authRequestId,
requestId,
})
.catch(() => {
setError("Could not reset password");
@@ -111,8 +111,8 @@ export function PasswordForm({
params.append("organization", organization);
}
if (authRequestId) {
params.append("authRequestId", authRequestId);
if (requestId) {
params.append("requestId", requestId);
}
return router.push("/password/set?" + params);

View File

@@ -36,7 +36,7 @@ type Props = {
lastname?: string;
email?: string;
organization?: string;
authRequestId?: string;
requestId?: string;
loginSettings?: LoginSettings;
};
@@ -46,7 +46,7 @@ export function RegisterForm({
firstname,
lastname,
organization,
authRequestId,
requestId,
loginSettings,
}: Props) {
const t = useTranslations("register");
@@ -73,7 +73,7 @@ export function RegisterForm({
firstName: values.firstname,
lastName: values.lastname,
organization: organization,
authRequestId: authRequestId,
requestId: requestId,
})
.catch(() => {
setError("Could not register user");
@@ -105,8 +105,8 @@ export function RegisterForm({
registerParams.organization = organization;
}
if (authRequestId) {
registerParams.authRequestId = authRequestId;
if (requestId) {
registerParams.requestId = requestId;
}
// redirect user to /register/password if password is chosen

View File

@@ -19,7 +19,7 @@ type Inputs = {};
type Props = {
sessionId: string;
isPrompt: boolean;
authRequestId?: string;
requestId?: string;
organization?: string;
};
@@ -27,7 +27,7 @@ export function RegisterPasskey({
sessionId,
isPrompt,
organization,
authRequestId,
requestId,
}: Props) {
const t = useTranslations("passkey");
@@ -161,8 +161,8 @@ export function RegisterPasskey({
params.set("organization", organization);
}
if (authRequestId) {
params.set("authRequestId", authRequestId);
if (requestId) {
params.set("requestId", requestId);
}
params.set("sessionId", sessionId);

View File

@@ -16,7 +16,7 @@ import { Spinner } from "./spinner";
type Props = {
loginName?: string;
sessionId: string;
authRequestId?: string;
requestId?: string;
organization?: string;
checkAfter: boolean;
loginSettings?: LoginSettings;
@@ -26,7 +26,7 @@ export function RegisterU2f({
loginName,
sessionId,
organization,
authRequestId,
requestId,
checkAfter,
loginSettings,
}: Props) {
@@ -166,18 +166,18 @@ export function RegisterU2f({
if (organization) {
paramsToContinue.append("organization", organization);
}
if (authRequestId) {
paramsToContinue.append("authRequestId", authRequestId);
if (requestId) {
paramsToContinue.append("requestId", requestId);
}
return router.push(`/u2f?` + paramsToContinue);
} else {
const url =
authRequestId && sessionId
requestId && sessionId
? await getNextUrl(
{
sessionId: sessionId,
authRequestId: authRequestId,
requestId: requestId,
organization: organization,
},
loginSettings?.defaultRedirectUri,

View File

@@ -31,11 +31,11 @@ export function isSessionValid(session: Partial<Session>): {
export function SessionItem({
session,
reload,
authRequestId,
requestId,
}: {
session: Session;
reload: () => void;
authRequestId?: string;
requestId?: string;
}) {
const [loading, setLoading] = useState<boolean>(false);
@@ -67,7 +67,7 @@ export function SessionItem({
if (valid && session?.factors?.user) {
const resp = await continueWithSession({
...session,
authRequestId: authRequestId,
requestId: requestId,
});
if (resp?.redirect) {
@@ -78,7 +78,7 @@ export function SessionItem({
const res = await sendLoginname({
loginName: session.factors?.user?.loginName,
organization: session.factors.user.organizationId,
authRequestId: authRequestId,
requestId: requestId,
})
.catch(() => {
setError("An internal error occurred");

View File

@@ -9,10 +9,10 @@ import { SessionItem } from "./session-item";
type Props = {
sessions: Session[];
authRequestId?: string;
requestId?: string;
};
export function SessionsList({ sessions, authRequestId }: Props) {
export function SessionsList({ sessions, requestId }: Props) {
const t = useTranslations("accounts");
const [list, setList] = useState<Session[]>(sessions);
return sessions ? (
@@ -34,7 +34,7 @@ export function SessionsList({ sessions, authRequestId }: Props) {
return (
<SessionItem
session={session}
authRequestId={authRequestId}
requestId={requestId}
reload={() => {
setList(list.filter((s) => s.id !== session.id));
}}

View File

@@ -39,14 +39,14 @@ type Props = {
loginName: string;
userId: string;
organization?: string;
authRequestId?: string;
requestId?: string;
codeRequired: boolean;
};
export function SetPasswordForm({
passwordComplexitySettings,
organization,
authRequestId,
requestId,
loginName,
userId,
code,
@@ -73,7 +73,7 @@ export function SetPasswordForm({
const response = await resetPassword({
loginName,
organization,
authRequestId,
requestId,
})
.catch(() => {
setError("Could not reset password");
@@ -137,7 +137,7 @@ export function SetPasswordForm({
checks: create(ChecksSchema, {
password: { password: values.password },
}),
authRequestId,
requestId,
})
.catch(() => {
setError("Could not verify password");

View File

@@ -32,7 +32,7 @@ type Props = {
firstname: string;
lastname: string;
organization?: string;
authRequestId?: string;
requestId?: string;
};
export function SetRegisterPasswordForm({
@@ -41,7 +41,7 @@ export function SetRegisterPasswordForm({
firstname,
lastname,
organization,
authRequestId,
requestId,
}: Props) {
const t = useTranslations("register");
@@ -66,7 +66,7 @@ export function SetRegisterPasswordForm({
firstName: firstname,
lastName: lastname,
organization: organization,
authRequestId: authRequestId,
requestId: requestId,
password: values.password,
})
.catch(() => {

View File

@@ -20,14 +20,14 @@ import { SignInWithGoogle } from "./idps/sign-in-with-google";
export interface SignInWithIDPProps {
children?: ReactNode;
identityProviders: IdentityProvider[];
authRequestId?: string;
requestId?: string;
organization?: string;
linkOnly?: boolean;
}
export function SignInWithIdp({
identityProviders,
authRequestId,
requestId,
organization,
linkOnly,
}: Readonly<SignInWithIDPProps>) {
@@ -40,7 +40,7 @@ export function SignInWithIdp({
setLoading(true);
const params = new URLSearchParams();
if (linkOnly) params.set("link", "true");
if (authRequestId) params.set("authRequestId", authRequestId);
if (requestId) params.set("requestId", requestId);
if (organization) params.set("organization", organization);
try {
@@ -64,7 +64,7 @@ export function SignInWithIdp({
setLoading(false);
}
},
[authRequestId, organization, linkOnly, router],
[requestId, organization, linkOnly, router],
);
const renderIDPButton = (idp: IdentityProvider) => {

View File

@@ -24,7 +24,7 @@ type Props = {
secret: string;
loginName?: string;
sessionId?: string;
authRequestId?: string;
requestId?: string;
organization?: string;
checkAfter?: boolean;
loginSettings?: LoginSettings;
@@ -34,7 +34,7 @@ export function TotpRegister({
secret,
loginName,
sessionId,
authRequestId,
requestId,
organization,
checkAfter,
loginSettings,
@@ -63,8 +63,8 @@ export function TotpRegister({
if (loginName) {
params.append("loginName", loginName);
}
if (authRequestId) {
params.append("authRequestId", authRequestId);
if (requestId) {
params.append("requestId", requestId);
}
if (organization) {
params.append("organization", organization);
@@ -73,11 +73,11 @@ export function TotpRegister({
return router.push(`/otp/time-based?` + params);
} else {
const url =
authRequestId && sessionId
requestId && sessionId
? await getNextUrl(
{
sessionId: sessionId,
authRequestId: authRequestId,
requestId: requestId,
organization: organization,
},
loginSettings?.defaultRedirectUri,

View File

@@ -25,8 +25,8 @@ export function UserAvatar({
params.set("organization", searchParams.organization);
}
if (searchParams?.authRequestId) {
params.set("authRequestId", searchParams.authRequestId);
if (searchParams?.requestId) {
params.set("requestId", searchParams.requestId);
}
if (searchParams?.loginName) {

View File

@@ -18,7 +18,7 @@ type Inputs = {
type Props = {
loginName: string | undefined;
authRequestId: string | undefined;
requestId: string | undefined;
loginSettings: LoginSettings | undefined;
organization?: string;
suffix?: string;
@@ -29,7 +29,7 @@ type Props = {
export function UsernameForm({
loginName,
authRequestId,
requestId,
organization,
suffix,
loginSettings,
@@ -56,7 +56,7 @@ export function UsernameForm({
const res = await sendLoginname({
loginName: values.loginName,
organization,
authRequestId,
requestId,
suffix,
})
.catch(() => {
@@ -117,8 +117,8 @@ export function UsernameForm({
if (organization) {
registerParams.append("organization", organization);
}
if (authRequestId) {
registerParams.append("authRequestId", authRequestId);
if (requestId) {
registerParams.append("requestId", requestId);
}
router.push("/register?" + registerParams);

View File

@@ -21,14 +21,14 @@ type Props = {
organization?: string;
code?: string;
isInvite: boolean;
authRequestId?: string;
requestId?: string;
};
export function VerifyForm({
userId,
loginName,
organization,
authRequestId,
requestId,
code,
isInvite,
}: Props) {
@@ -78,7 +78,7 @@ export function VerifyForm({
isInvite: isInvite,
loginName: loginName,
organization: organization,
authRequestId: authRequestId,
requestId: requestId,
})
.catch(() => {
setError("Could not verify user");

View File

@@ -15,13 +15,13 @@ import { Spinner } from "./spinner";
export function VerifyRedirectButton({
userId,
loginName,
authRequestId,
requestId,
authMethods,
organization,
}: {
userId?: string;
loginName?: string;
authRequestId: string;
requestId: string;
authMethods: AuthenticationMethodType[] | null;
organization?: string;
}) {
@@ -35,7 +35,7 @@ export function VerifyRedirectButton({
let command = {
organization,
authRequestId,
requestId,
} as SendVerificationRedirectWithoutCheckCommand;
if (userId) {

View File

@@ -1,12 +1,12 @@
type FinishFlowCommand =
| {
sessionId: string;
authRequestId: string;
requestId: string;
}
| { loginName: string };
/**
* for client: redirects user back to OIDC application or to a success page when using authRequestId, check if a default redirect and redirect to it, or just redirect to a success page with the loginName
* for client: redirects user back to an OIDC or SAML application or to a success page when using requestId, check if a default redirect and redirect to it, or just redirect to a success page with the loginName
* @param command
* @returns
*/
@@ -14,10 +14,10 @@ export async function getNextUrl(
command: FinishFlowCommand & { organization?: string },
defaultRedirectUri?: string,
): Promise<string> {
if ("sessionId" in command && "authRequestId" in command) {
if ("sessionId" in command && "requestId" in command) {
const params = new URLSearchParams({
sessionId: command.sessionId,
authRequest: command.authRequestId,
requestId: command.requestId,
});
if (command.organization) {

View File

@@ -15,7 +15,7 @@ export type Cookie = {
creationTs: string;
expirationTs: string;
changeTs: string;
authRequestId?: string; // if its linked to an OIDC flow
requestId?: string; // if its linked to an OIDC flow
};
type SessionCookie<T> = Cookie & T;

131
apps/login/src/lib/oidc.ts Normal file
View File

@@ -0,0 +1,131 @@
import { Cookie } from "@/lib/cookies";
import { sendLoginname, SendLoginnameCommand } from "@/lib/server/loginname";
import { createCallback, getLoginSettings } from "@/lib/zitadel";
import { create } from "@zitadel/client";
import {
CreateCallbackRequestSchema,
SessionSchema,
} from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { NextRequest, NextResponse } from "next/server";
import { isSessionValid } from "./session";
type LoginWithOIDCandSession = {
serviceUrl: string;
authRequest: string;
sessionId: string;
sessions: Session[];
sessionCookies: Cookie[];
request: NextRequest;
};
export async function loginWithOIDCandSession({
serviceUrl,
authRequest,
sessionId,
sessions,
sessionCookies,
request,
}: LoginWithOIDCandSession) {
console.log(
`Login with session: ${sessionId} and authRequest: ${authRequest}`,
);
const selectedSession = sessions.find((s) => s.id === sessionId);
if (selectedSession && selectedSession.id) {
console.log(`Found session ${selectedSession.id}`);
const isValid = await isSessionValid({
serviceUrl,
session: selectedSession,
});
console.log("Session is valid:", isValid);
if (!isValid && selectedSession.factors?.user) {
// if the session is not valid anymore, we need to redirect the user to re-authenticate /
// TODO: handle IDP intent direcly if available
const command: SendLoginnameCommand = {
loginName: selectedSession.factors.user?.loginName,
organization: selectedSession.factors?.user?.organizationId,
requestId: `oidc_${authRequest}`,
};
const res = await sendLoginname(command);
if (res && "redirect" in res && res?.redirect) {
const absoluteUrl = new URL(res.redirect, request.url);
return NextResponse.redirect(absoluteUrl.toString());
}
}
const cookie = sessionCookies.find(
(cookie) => cookie.id === selectedSession?.id,
);
if (cookie && cookie.id && cookie.token) {
const session = {
sessionId: cookie?.id,
sessionToken: cookie?.token,
};
// works not with _rsc request
try {
const { callbackUrl } = await createCallback({
serviceUrl,
req: create(CreateCallbackRequestSchema, {
authRequestId: authRequest,
callbackKind: {
case: "session",
value: create(SessionSchema, session),
},
}),
});
if (callbackUrl) {
return NextResponse.redirect(callbackUrl);
} else {
return NextResponse.json(
{ error: "An error occurred!" },
{ status: 500 },
);
}
} catch (error: unknown) {
// handle already handled gracefully as these could come up if old emails with requestId are used (reset password, register emails etc.)
console.error(error);
if (
error &&
typeof error === "object" &&
"code" in error &&
error?.code === 9
) {
const loginSettings = await getLoginSettings({
serviceUrl,
organization: selectedSession.factors?.user?.organizationId,
});
if (loginSettings?.defaultRedirectUri) {
return NextResponse.redirect(loginSettings.defaultRedirectUri);
}
const signedinUrl = new URL("/signedin", request.url);
if (selectedSession.factors?.user?.loginName) {
signedinUrl.searchParams.set(
"loginName",
selectedSession.factors?.user?.loginName,
);
}
if (selectedSession.factors?.user?.organizationId) {
signedinUrl.searchParams.set(
"organization",
selectedSession.factors?.user?.organizationId,
);
}
return NextResponse.redirect(signedinUrl);
} else {
return NextResponse.json({ error }, { status: 500 });
}
}
}
}
}

129
apps/login/src/lib/saml.ts Normal file
View File

@@ -0,0 +1,129 @@
import { Cookie } from "@/lib/cookies";
import { sendLoginname, SendLoginnameCommand } from "@/lib/server/loginname";
import { createResponse, getLoginSettings } from "@/lib/zitadel";
import { create } from "@zitadel/client";
import { CreateResponseRequestSchema } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { NextRequest, NextResponse } from "next/server";
import { isSessionValid } from "./session";
type LoginWithSAMLandSession = {
serviceUrl: string;
samlRequest: string;
sessionId: string;
sessions: Session[];
sessionCookies: Cookie[];
request: NextRequest;
};
export async function loginWithSAMLandSession({
serviceUrl,
samlRequest,
sessionId,
sessions,
sessionCookies,
request,
}: LoginWithSAMLandSession) {
console.log(
`Login with session: ${sessionId} and samlRequest: ${samlRequest}`,
);
const selectedSession = sessions.find((s) => s.id === sessionId);
if (selectedSession && selectedSession.id) {
console.log(`Found session ${selectedSession.id}`);
const isValid = await isSessionValid({
serviceUrl,
session: selectedSession,
});
console.log("Session is valid:", isValid);
if (!isValid && selectedSession.factors?.user) {
// if the session is not valid anymore, we need to redirect the user to re-authenticate /
// TODO: handle IDP intent direcly if available
const command: SendLoginnameCommand = {
loginName: selectedSession.factors.user?.loginName,
organization: selectedSession.factors?.user?.organizationId,
requestId: `saml_${samlRequest}`,
};
const res = await sendLoginname(command);
if (res && "redirect" in res && res?.redirect) {
const absoluteUrl = new URL(res.redirect, request.url);
return NextResponse.redirect(absoluteUrl.toString());
}
}
const cookie = sessionCookies.find(
(cookie) => cookie.id === selectedSession?.id,
);
if (cookie && cookie.id && cookie.token) {
const session = {
sessionId: cookie?.id,
sessionToken: cookie?.token,
};
// works not with _rsc request
try {
const { url } = await createResponse({
serviceUrl,
req: create(CreateResponseRequestSchema, {
samlRequestId: samlRequest,
responseKind: {
case: "session",
value: session,
},
}),
});
if (url) {
return NextResponse.redirect(url);
} else {
return NextResponse.json(
{ error: "An error occurred!" },
{ status: 500 },
);
}
} catch (error: unknown) {
// handle already handled gracefully as these could come up if old emails with requestId are used (reset password, register emails etc.)
console.error(error);
if (
error &&
typeof error === "object" &&
"code" in error &&
error?.code === 9
) {
const loginSettings = await getLoginSettings({
serviceUrl,
organization: selectedSession.factors?.user?.organizationId,
});
if (loginSettings?.defaultRedirectUri) {
return NextResponse.redirect(loginSettings.defaultRedirectUri);
}
const signedinUrl = new URL("/signedin", request.url);
if (selectedSession.factors?.user?.loginName) {
signedinUrl.searchParams.set(
"loginName",
selectedSession.factors?.user?.loginName,
);
}
if (selectedSession.factors?.user?.organizationId) {
signedinUrl.searchParams.set(
"organization",
selectedSession.factors?.user?.organizationId,
);
}
return NextResponse.redirect(signedinUrl);
} else {
return NextResponse.json({ error }, { status: 500 });
}
}
}
}
}

View File

@@ -30,7 +30,7 @@ type CustomCookieData = {
creationTs: string;
expirationTs: string;
changeTs: string;
authRequestId?: string; // if its linked to an OIDC flow
requestId?: string; // if its linked to an OIDC flow
};
const passwordAttemptsHandler = (error: ConnectError) => {
@@ -48,8 +48,7 @@ const passwordAttemptsHandler = (error: ConnectError) => {
export async function createSessionAndUpdateCookie(
checks: Checks,
challenges: RequestChallenges | undefined,
authRequestId: string | undefined,
requestId: string | undefined,
lifetime?: Duration,
): Promise<Session> {
const _headers = await headers();
@@ -57,9 +56,7 @@ export async function createSessionAndUpdateCookie(
const createdSession = await createSessionFromChecks({
serviceUrl,
checks,
challenges,
lifetime,
});
@@ -86,8 +83,8 @@ export async function createSessionAndUpdateCookie(
loginName: response.session.factors.user.loginName ?? "",
};
if (authRequestId) {
sessionCookie.authRequestId = authRequestId;
if (requestId) {
sessionCookie.requestId = requestId;
}
if (response.session.factors.user.organizationId) {
@@ -113,7 +110,7 @@ export async function createSessionForIdpAndUpdateCookie(
idpIntentId?: string | undefined;
idpIntentToken?: string | undefined;
},
authRequestId: string | undefined,
requestId: string | undefined,
lifetime?: Duration,
): Promise<Session> {
const _headers = await headers();
@@ -165,8 +162,8 @@ export async function createSessionForIdpAndUpdateCookie(
organization: session.factors.user.organizationId ?? "",
};
if (authRequestId) {
sessionCookie.authRequestId = authRequestId;
if (requestId) {
sessionCookie.requestId = requestId;
}
if (session.factors.user.organizationId) {
@@ -186,7 +183,7 @@ export async function setSessionAndUpdateCookie(
recentCookie: CustomCookieData,
checks?: Checks,
challenges?: RequestChallenges,
authRequestId?: string,
requestId?: string,
lifetime?: Duration,
) {
const _headers = await headers();
@@ -216,8 +213,8 @@ export async function setSessionAndUpdateCookie(
organization: recentCookie.organization,
};
if (authRequestId) {
sessionCookie.authRequestId = authRequestId;
if (requestId) {
sessionCookie.requestId = requestId;
}
return getSession({
@@ -241,8 +238,8 @@ export async function setSessionAndUpdateCookie(
organization: session.factors?.user?.organizationId ?? "",
};
if (sessionCookie.authRequestId) {
newCookie.authRequestId = sessionCookie.authRequestId;
if (sessionCookie.requestId) {
newCookie.requestId = sessionCookie.requestId;
}
return updateSessionCookie(sessionCookie.id, newCookie).then(() => {

View File

@@ -55,7 +55,7 @@ type CreateNewSessionCommand = {
loginName?: string;
password?: string;
organization?: string;
authRequestId?: string;
requestId?: string;
};
export async function createNewSessionFromIdpIntent(
@@ -92,7 +92,7 @@ export async function createNewSessionFromIdpIntent(
const session = await createSessionForIdpAndUpdateCookie(
command.userId,
command.idpIntent,
command.authRequestId,
command.requestId,
loginSettings?.externalLoginCheckLifetime,
);
@@ -110,7 +110,7 @@ export async function createNewSessionFromIdpIntent(
session,
humanUser,
command.organization,
command.authRequestId,
command.requestId,
);
if (emailVerificationCheck?.redirect) {
@@ -118,16 +118,16 @@ export async function createNewSessionFromIdpIntent(
}
// TODO: check if user has MFA methods
// const mfaFactorCheck = checkMFAFactors(session, loginSettings, authMethods, organization, authRequestId);
// const mfaFactorCheck = checkMFAFactors(session, loginSettings, authMethods, organization, requestId);
// if (mfaFactorCheck?.redirect) {
// return mfaFactorCheck;
// }
const url = await getNextUrl(
command.authRequestId && session.id
command.requestId && session.id
? {
sessionId: session.id,
authRequestId: command.authRequestId,
requestId: command.requestId,
organization: session.factors.user.organizationId,
}
: {

View File

@@ -11,7 +11,7 @@ type InviteUserCommand = {
lastName: string;
password?: string;
organization?: string;
authRequestId?: string;
requestId?: string;
};
export type RegisterUserResponse = {

View File

@@ -25,7 +25,7 @@ import { createSessionAndUpdateCookie } from "./cookie";
export type SendLoginnameCommand = {
loginName: string;
authRequestId?: string;
requestId?: string;
organization?: string;
suffix?: string;
};
@@ -96,8 +96,8 @@ export async function sendLoginname(command: SendLoginnameCommand) {
const params = new URLSearchParams();
if (command.authRequestId) {
params.set("authRequestId", command.authRequestId);
if (command.requestId) {
params.set("requestId", command.requestId);
}
if (command.organization) {
@@ -162,8 +162,8 @@ export async function sendLoginname(command: SendLoginnameCommand) {
const params = new URLSearchParams({ userId });
if (command.authRequestId) {
params.set("authRequestId", command.authRequestId);
if (command.requestId) {
params.set("requestId", command.requestId);
}
if (command.organization) {
@@ -243,8 +243,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
const session = await createSessionAndUpdateCookie(
checks,
undefined,
command.authRequestId,
command.requestId,
);
if (!session.factors?.user?.id) {
@@ -269,7 +268,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
session,
humanUser,
session.factors.user.organizationId,
command.authRequestId,
command.requestId,
);
if (inviteCheck?.redirect) {
@@ -288,8 +287,8 @@ export async function sendLoginname(command: SendLoginnameCommand) {
);
}
if (command.authRequestId) {
paramsAuthenticatorSetup.append("authRequestId", command.authRequestId);
if (command.requestId) {
paramsAuthenticatorSetup.append("requestId", command.requestId);
}
return { redirect: "/authenticator/set?" + paramsAuthenticatorSetup };
@@ -317,8 +316,8 @@ export async function sendLoginname(command: SendLoginnameCommand) {
command.organization ?? session.factors?.user?.organizationId;
}
if (command.authRequestId) {
paramsPassword.authRequestId = command.authRequestId;
if (command.requestId) {
paramsPassword.requestId = command.requestId;
}
return {
@@ -334,8 +333,8 @@ export async function sendLoginname(command: SendLoginnameCommand) {
}
const paramsPasskey: any = { loginName: command.loginName };
if (command.authRequestId) {
paramsPasskey.authRequestId = command.authRequestId;
if (command.requestId) {
paramsPasskey.requestId = command.requestId;
}
if (command.organization || session.factors?.user?.organizationId) {
@@ -353,8 +352,8 @@ export async function sendLoginname(command: SendLoginnameCommand) {
altPassword: `${methods.authMethodTypes.includes(1)}`, // show alternative password option
};
if (command.authRequestId) {
passkeyParams.authRequestId = command.authRequestId;
if (command.requestId) {
passkeyParams.requestId = command.requestId;
}
if (command.organization || session.factors?.user?.organizationId) {
@@ -373,8 +372,8 @@ export async function sendLoginname(command: SendLoginnameCommand) {
// user has no passkey setup and login settings allow passkeys
const paramsPasswordDefault: any = { loginName: command.loginName };
if (command.authRequestId) {
paramsPasswordDefault.authRequestId = command.authRequestId;
if (command.requestId) {
paramsPasswordDefault.requestId = command.requestId;
}
if (command.organization || session.factors?.user?.organizationId) {
@@ -437,8 +436,8 @@ export async function sendLoginname(command: SendLoginnameCommand) {
if (orgToRegisterOn && !loginSettingsByContext?.ignoreUnknownUsernames) {
const params = new URLSearchParams({ organization: orgToRegisterOn });
if (command.authRequestId) {
params.set("authRequestId", command.authRequestId);
if (command.requestId) {
params.set("requestId", command.requestId);
}
if (command.loginName) {
@@ -454,8 +453,8 @@ export async function sendLoginname(command: SendLoginnameCommand) {
loginName: command.loginName,
});
if (command.authRequestId) {
paramsPasswordDefault.append("authRequestId", command.authRequestId);
if (command.requestId) {
paramsPasswordDefault.append("requestId", command.requestId);
}
if (command.organization) {

View File

@@ -20,7 +20,7 @@ export type SetOTPCommand = {
loginName?: string;
sessionId?: string;
organization?: string;
authRequestId?: string;
requestId?: string;
code: string;
method: string;
};
@@ -72,7 +72,7 @@ export async function setOTP(command: SetOTPCommand) {
recentSession,
checks,
undefined,
command.authRequestId,
command.requestId,
loginSettings?.secondFactorCheckLifetime,
).then((session) => {
return {

View File

@@ -139,12 +139,12 @@ type SendPasskeyCommand = {
sessionId?: string;
organization?: string;
checks?: Checks;
authRequestId?: string;
requestId?: string;
lifetime?: Duration;
};
export async function sendPasskey(command: SendPasskeyCommand) {
let { loginName, sessionId, organization, checks, authRequestId } = command;
let { loginName, sessionId, organization, checks, requestId } = command;
const recentSession = sessionId
? await getSessionCookieById({ sessionId })
: loginName
@@ -176,7 +176,7 @@ export async function sendPasskey(command: SendPasskeyCommand) {
recentSession,
checks,
undefined,
authRequestId,
requestId,
lifetime,
);
@@ -203,7 +203,7 @@ export async function sendPasskey(command: SendPasskeyCommand) {
session,
humanUser,
organization,
authRequestId,
requestId,
);
if (emailVerificationCheck?.redirect) {
@@ -211,11 +211,11 @@ export async function sendPasskey(command: SendPasskeyCommand) {
}
const url =
authRequestId && session.id
requestId && session.id
? await getNextUrl(
{
sessionId: session.id,
authRequestId: authRequestId,
requestId: requestId,
organization: organization,
},
loginSettings?.defaultRedirectUri,

View File

@@ -16,7 +16,7 @@ import {
setPassword,
setUserPassword,
} from "@/lib/zitadel";
import { create } from "@zitadel/client";
import { ConnectError, create } from "@zitadel/client";
import { createServerTransport } from "@zitadel/client/node";
import { createUserServiceClient } from "@zitadel/client/v2";
import {
@@ -42,7 +42,7 @@ import {
type ResetPasswordCommand = {
loginName: string;
organization?: string;
authRequestId?: string;
requestId?: string;
};
export async function resetPassword(command: ResetPasswordCommand) {
@@ -77,7 +77,7 @@ export async function resetPassword(command: ResetPasswordCommand) {
userId,
urlTemplate:
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` +
(command.authRequestId ? `&authRequestId=${command.authRequestId}` : ""),
(command.requestId ? `&requestId=${command.requestId}` : ""),
});
}
@@ -85,7 +85,7 @@ export type UpdateSessionCommand = {
loginName: string;
organization?: string;
checks: Checks;
authRequestId?: string;
requestId?: string;
};
export async function sendPassword(command: UpdateSessionCommand) {
@@ -128,8 +128,7 @@ export async function sendPassword(command: UpdateSessionCommand) {
try {
session = await createSessionAndUpdateCookie(
checks,
undefined,
command.authRequestId,
command.requestId,
loginSettings?.passwordCheckLifetime,
);
} catch (error: any) {
@@ -161,7 +160,7 @@ export async function sendPassword(command: UpdateSessionCommand) {
sessionCookie,
command.checks,
undefined,
command.authRequestId,
command.requestId,
loginSettings?.passwordCheckLifetime,
);
} catch (error: any) {
@@ -228,7 +227,7 @@ export async function sendPassword(command: UpdateSessionCommand) {
session,
humanUser,
command.organization,
command.authRequestId,
command.requestId,
);
if (passwordChangedCheck?.redirect) {
@@ -245,7 +244,7 @@ export async function sendPassword(command: UpdateSessionCommand) {
session,
humanUser,
command.organization,
command.authRequestId,
command.requestId,
);
if (emailVerificationCheck?.redirect) {
@@ -269,23 +268,24 @@ export async function sendPassword(command: UpdateSessionCommand) {
return { error: "Could not verify password!" };
}
const mfaFactorCheck = checkMFAFactors(
const mfaFactorCheck = await checkMFAFactors(
serviceUrl,
session,
loginSettings,
authMethods,
command.organization,
command.authRequestId,
command.requestId,
);
if (mfaFactorCheck?.redirect) {
return mfaFactorCheck;
}
if (command.authRequestId && session.id) {
if (command.requestId && session.id) {
const nextUrl = await getNextUrl(
{
sessionId: session.id,
authRequestId: command.authRequestId,
requestId: command.requestId,
organization:
command.organization ?? session.factors?.user?.organizationId,
},
@@ -435,7 +435,7 @@ export async function checkSessionAndSetPassword({
},
{},
)
.catch((error) => {
.catch((error: ConnectError) => {
console.log(error);
if (error.code === 7) {
return { error: "Session is not valid." };

View File

@@ -19,7 +19,7 @@ type RegisterUserCommand = {
lastName: string;
password?: string;
organization?: string;
authRequestId?: string;
requestId?: string;
};
export type RegisterUserResponse = {
@@ -71,8 +71,7 @@ export async function registerUser(command: RegisterUserCommand) {
const session = await createSessionAndUpdateCookie(
checks,
undefined,
command.authRequestId,
command.requestId,
command.password ? loginSettings?.passwordCheckLifetime : undefined,
);
@@ -86,8 +85,8 @@ export async function registerUser(command: RegisterUserCommand) {
organization: session.factors.user.organizationId,
});
if (command.authRequestId) {
params.append("authRequestId", command.authRequestId);
if (command.requestId) {
params.append("requestId", command.requestId);
}
return { redirect: "/passkey/set?" + params };
@@ -111,7 +110,7 @@ export async function registerUser(command: RegisterUserCommand) {
session,
humanUser,
session.factors.user.organizationId,
command.authRequestId,
command.requestId,
);
if (emailVerificationCheck?.redirect) {
@@ -119,10 +118,10 @@ export async function registerUser(command: RegisterUserCommand) {
}
const url = await getNextUrl(
command.authRequestId && session.id
command.requestId && session.id
? {
sessionId: session.id,
authRequestId: command.authRequestId,
requestId: command.requestId,
organization: session.factors.user.organizationId,
}
: {

View File

@@ -4,6 +4,7 @@ import { setSessionAndUpdateCookie } from "@/lib/server/cookie";
import {
deleteSession,
getLoginSettings,
humanMFAInitSkipped,
listAuthenticationMethodTypes,
} from "@/lib/zitadel";
import { Duration } from "@zitadel/client";
@@ -20,10 +21,57 @@ import {
} from "../cookies";
import { getServiceUrlFromHeaders } from "../service";
export async function skipMFAAndContinueWithNextUrl({
userId,
requestId,
loginName,
sessionId,
organization,
}: {
userId: string;
loginName?: string;
sessionId?: string;
requestId?: string;
organization?: string;
}) {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const loginSettings = await getLoginSettings({
serviceUrl,
organization: organization,
});
await humanMFAInitSkipped({ serviceUrl, userId });
const url =
requestId && sessionId
? await getNextUrl(
{
sessionId: sessionId,
requestId: requestId,
organization: organization,
},
loginSettings?.defaultRedirectUri,
)
: loginName
? await getNextUrl(
{
loginName: loginName,
organization: organization,
},
loginSettings?.defaultRedirectUri,
)
: null;
if (url) {
return { redirect: url };
}
}
export async function continueWithSession({
authRequestId,
requestId,
...session
}: Session & { authRequestId?: string }) {
}: Session & { requestId?: string }) {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
@@ -34,11 +82,11 @@ export async function continueWithSession({
});
const url =
authRequestId && session.id && session.factors?.user
requestId && session.id && session.factors?.user
? await getNextUrl(
{
sessionId: session.id,
authRequestId: authRequestId,
requestId: requestId,
organization: session.factors.user.organizationId,
},
loginSettings?.defaultRedirectUri,
@@ -62,20 +110,14 @@ export type UpdateSessionCommand = {
sessionId?: string;
organization?: string;
checks?: Checks;
authRequestId?: string;
requestId?: string;
challenges?: RequestChallenges;
lifetime?: Duration;
};
export async function updateSession(options: UpdateSessionCommand) {
let {
loginName,
sessionId,
organization,
checks,
authRequestId,
challenges,
} = options;
let { loginName, sessionId, organization, checks, requestId, challenges } =
options;
const recentSession = sessionId
? await getSessionCookieById({ sessionId })
: loginName
@@ -123,7 +165,7 @@ export async function updateSession(options: UpdateSessionCommand) {
recentSession,
checks,
challenges,
authRequestId,
requestId,
lifetime,
);

View File

@@ -59,7 +59,7 @@ type VerifyUserByEmailCommand = {
organization?: string;
code: string;
isInvite: boolean;
authRequestId?: string;
requestId?: string;
};
export async function sendVerification(command: VerifyUserByEmailCommand) {
@@ -155,11 +155,7 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
},
});
session = await createSessionAndUpdateCookie(
checks,
undefined,
command.authRequestId,
);
session = await createSessionAndUpdateCookie(checks, command.requestId);
}
if (!session?.factors?.user?.id) {
@@ -207,12 +203,13 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
}
// redirect to mfa factor if user has one, or redirect to set one up
const mfaFactorCheck = checkMFAFactors(
const mfaFactorCheck = await checkMFAFactors(
serviceUrl,
session,
loginSettings,
authMethodResponse.authMethodTypes,
command.organization,
command.authRequestId,
command.requestId,
);
if (mfaFactorCheck?.redirect) {
@@ -220,11 +217,11 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
}
// login user if no additional steps are required
if (command.authRequestId && session.id) {
if (command.requestId && session.id) {
const nextUrl = await getNextUrl(
{
sessionId: session.id,
authRequestId: command.authRequestId,
requestId: command.requestId,
organization:
command.organization ?? session.factors?.user?.organizationId,
},
@@ -248,7 +245,7 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
type resendVerifyEmailCommand = {
userId: string;
isInvite: boolean;
authRequestId?: string;
requestId?: string;
};
export async function resendVerification(command: resendVerifyEmailCommand) {
@@ -269,9 +266,7 @@ export async function resendVerification(command: resendVerifyEmailCommand) {
serviceUrl,
urlTemplate:
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` +
(command.authRequestId
? `&authRequestId=${command.authRequestId}`
: ""),
(command.requestId ? `&requestId=${command.requestId}` : ""),
});
}
@@ -292,7 +287,7 @@ export async function sendEmailCode(command: sendEmailCommand) {
export type SendVerificationRedirectWithoutCheckCommand = {
organization?: string;
authRequestId?: string;
requestId?: string;
} & (
| { userId: string; loginName?: never }
| { userId?: never; loginName: string }
@@ -371,11 +366,7 @@ export async function sendVerificationRedirectWithoutCheck(
},
});
session = await createSessionAndUpdateCookie(
checks,
undefined,
command.authRequestId,
);
session = await createSessionAndUpdateCookie(checks, command.requestId);
}
if (!session?.factors?.user?.id) {
@@ -418,17 +409,17 @@ export async function sendVerificationRedirectWithoutCheck(
const loginSettings = await getLoginSettings({
serviceUrl,
organization: user.details?.resourceOwner,
});
// redirect to mfa factor if user has one, or redirect to set one up
const mfaFactorCheck = checkMFAFactors(
const mfaFactorCheck = await checkMFAFactors(
serviceUrl,
session,
loginSettings,
authMethodResponse.authMethodTypes,
command.organization,
command.authRequestId,
command.requestId,
);
if (mfaFactorCheck?.redirect) {
@@ -436,11 +427,11 @@ export async function sendVerificationRedirectWithoutCheck(
}
// login user if no additional steps are required
if (command.authRequestId && session.id) {
if (command.requestId && session.id) {
const nextUrl = await getNextUrl(
{
sessionId: session.id,
authRequestId: command.authRequestId,
requestId: command.requestId,
organization:
command.organization ?? session.factors?.user?.organizationId,
},

View File

@@ -3,6 +3,7 @@ import { createServerTransport } from "@zitadel/client/node";
import { IdentityProviderService } from "@zitadel/proto/zitadel/idp/v2/idp_service_pb";
import { OIDCService } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
import { OrganizationService } from "@zitadel/proto/zitadel/org/v2/org_service_pb";
import { SAMLService } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb";
import { SessionService } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb";
import { UserService } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
@@ -15,7 +16,8 @@ type ServiceClass =
| typeof OrganizationService
| typeof SessionService
| typeof OIDCService
| typeof SettingsService;
| typeof SettingsService
| typeof SAMLService;
export async function createServiceForHost<T extends ServiceClass>(
service: T,

View File

@@ -1,7 +1,15 @@
import { timestampDate } from "@zitadel/client";
import { AuthRequest } from "@zitadel/proto/zitadel/oidc/v2/authorization_pb";
import { SAMLRequest } from "@zitadel/proto/zitadel/saml/v2/authorization_pb";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { GetSessionResponse } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { getMostRecentCookieWithLoginname } from "./cookies";
import { getSession } from "./zitadel";
import {
getLoginSettings,
getSession,
listAuthenticationMethodTypes,
} from "./zitadel";
type LoadMostRecentSessionParams = {
serviceUrl: string;
@@ -29,3 +37,160 @@ export async function loadMostRecentSession({
sessionToken: recent.token,
}).then((resp: GetSessionResponse) => resp.session);
}
/**
* 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);
**/
export async function isSessionValid({
serviceUrl,
session,
}: {
serviceUrl: string;
session: Session;
}): Promise<boolean> {
// session can't be checked without user
if (!session.factors?.user) {
console.warn("Session has no user");
return false;
}
let mfaValid = true;
const authMethodTypes = await listAuthenticationMethodTypes({
serviceUrl,
userId: session.factors.user.id,
});
const authMethods = authMethodTypes.authMethodTypes;
if (authMethods && authMethods.includes(AuthenticationMethodType.TOTP)) {
mfaValid = !!session.factors.totp?.verifiedAt;
if (!mfaValid) {
console.warn(
"Session has no valid totpEmail factor",
session.factors.totp?.verifiedAt,
);
}
} else if (
authMethods &&
authMethods.includes(AuthenticationMethodType.OTP_EMAIL)
) {
mfaValid = !!session.factors.otpEmail?.verifiedAt;
if (!mfaValid) {
console.warn(
"Session has no valid otpEmail factor",
session.factors.otpEmail?.verifiedAt,
);
}
} else if (
authMethods &&
authMethods.includes(AuthenticationMethodType.OTP_SMS)
) {
mfaValid = !!session.factors.otpSms?.verifiedAt;
if (!mfaValid) {
console.warn(
"Session has no valid otpSms factor",
session.factors.otpSms?.verifiedAt,
);
}
} else if (
authMethods &&
authMethods.includes(AuthenticationMethodType.U2F)
) {
mfaValid = !!session.factors.webAuthN?.verifiedAt;
if (!mfaValid) {
console.warn(
"Session has no valid u2f factor",
session.factors.webAuthN?.verifiedAt,
);
}
} else {
// only check settings if no auth methods are available, as this would require a setup
const loginSettings = await getLoginSettings({
serviceUrl,
organization: session.factors?.user?.organizationId,
});
if (loginSettings?.forceMfa || loginSettings?.forceMfaLocalOnly) {
const otpEmail = session.factors.otpEmail?.verifiedAt;
const otpSms = session.factors.otpSms?.verifiedAt;
const totp = session.factors.totp?.verifiedAt;
const webAuthN = session.factors.webAuthN?.verifiedAt;
const idp = session.factors.intent?.verifiedAt; // TODO: forceMFA should not consider this as valid factor
// must have one single check
mfaValid = !!(otpEmail || otpSms || totp || webAuthN || idp);
if (!mfaValid) {
console.warn("Session has no valid multifactor", session.factors);
}
} else {
mfaValid = true;
}
}
const validPassword = session?.factors?.password?.verifiedAt;
const validPasskey = session?.factors?.webAuthN?.verifiedAt;
const validIDP = session?.factors?.intent?.verifiedAt;
const stillValid = session.expirationDate
? timestampDate(session.expirationDate).getTime() > new Date().getTime()
: true;
if (!stillValid) {
console.warn(
"Session is expired",
session.expirationDate
? timestampDate(session.expirationDate).toDateString()
: "no expiration date",
);
}
const validChecks = !!(validPassword || validPasskey || validIDP);
return stillValid && validChecks && mfaValid;
}
export async function findValidSession({
serviceUrl,
sessions,
authRequest,
samlRequest,
}: {
serviceUrl: string;
sessions: Session[];
authRequest?: AuthRequest;
samlRequest?: SAMLRequest;
}): Promise<Session | undefined> {
const sessionsWithHint = sessions.filter((s) => {
if (authRequest && authRequest.hintUserId) {
return s.factors?.user?.id === authRequest.hintUserId;
}
if (authRequest && authRequest.loginHint) {
return s.factors?.user?.loginName === authRequest.loginHint;
}
if (samlRequest) {
// TODO: do whatever
return true;
}
return true;
});
if (sessionsWithHint.length === 0) {
return undefined;
}
// sort by change date descending
sessionsWithHint.sort((a, b) => {
const dateA = a.changeDate ? timestampDate(a.changeDate).getTime() : 0;
const dateB = b.changeDate ? timestampDate(b.changeDate).getTime() : 0;
return dateB - dateA;
});
// return the first valid session according to settings
for (const session of sessionsWithHint) {
if (await isSessionValid({ serviceUrl, session })) {
return session;
}
}
return undefined;
}

View File

@@ -5,13 +5,14 @@ import { PasswordExpirySettings } from "@zitadel/proto/zitadel/settings/v2/passw
import { HumanUser } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import moment from "moment";
import { getUserByID } from "./zitadel";
export function checkPasswordChangeRequired(
expirySettings: PasswordExpirySettings | undefined,
session: Session,
humanUser: HumanUser | undefined,
organization?: string,
authRequestId?: string,
requestId?: string,
) {
let isOutdated = false;
if (expirySettings?.maxAgeDays && humanUser?.passwordChanged) {
@@ -35,8 +36,8 @@ export function checkPasswordChangeRequired(
);
}
if (authRequestId) {
params.append("authRequestId", authRequestId);
if (requestId) {
params.append("requestId", requestId);
}
return { redirect: "/password/change?" + params };
@@ -47,7 +48,7 @@ export function checkInvite(
session: Session,
humanUser?: HumanUser,
organization?: string,
authRequestId?: string,
requestId?: string,
) {
if (!humanUser?.email?.isVerified) {
const paramsVerify = new URLSearchParams({
@@ -63,8 +64,8 @@ export function checkInvite(
);
}
if (authRequestId) {
paramsVerify.append("authRequestId", authRequestId);
if (requestId) {
paramsVerify.append("requestId", requestId);
}
return { redirect: "/verify?" + paramsVerify };
@@ -75,7 +76,7 @@ export function checkEmailVerification(
session: Session,
humanUser?: HumanUser,
organization?: string,
authRequestId?: string,
requestId?: string,
) {
if (
!humanUser?.email?.isVerified &&
@@ -85,8 +86,8 @@ export function checkEmailVerification(
loginName: session.factors?.user?.loginName as string,
});
if (authRequestId) {
params.append("authRequestId", authRequestId);
if (requestId) {
params.append("requestId", requestId);
}
if (organization || session.factors?.user?.organizationId) {
@@ -100,12 +101,13 @@ export function checkEmailVerification(
}
}
export function checkMFAFactors(
export async function checkMFAFactors(
serviceUrl: string,
session: Session,
loginSettings: LoginSettings | undefined,
authMethods: AuthenticationMethodType[],
organization?: string,
authRequestId?: string,
requestId?: string,
) {
const availableMultiFactors = authMethods?.filter(
(m: AuthenticationMethodType) =>
@@ -128,8 +130,8 @@ export function checkMFAFactors(
loginName: session.factors?.user?.loginName as string,
});
if (authRequestId) {
params.append("authRequestId", authRequestId);
if (requestId) {
params.append("requestId", requestId);
}
if (organization || session.factors?.user?.organizationId) {
@@ -155,8 +157,8 @@ export function checkMFAFactors(
loginName: session.factors?.user?.loginName as string,
});
if (authRequestId) {
params.append("authRequestId", authRequestId);
if (requestId) {
params.append("requestId", requestId);
}
if (organization || session.factors?.user?.organizationId) {
@@ -177,8 +179,62 @@ export function checkMFAFactors(
checkAfter: "true", // this defines if the check is directly made after the setup
});
if (authRequestId) {
params.append("authRequestId", authRequestId);
if (requestId) {
params.append("requestId", requestId);
}
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 };
} else if (
loginSettings?.mfaInitSkipLifetime &&
(loginSettings.mfaInitSkipLifetime.nanos > 0 ||
loginSettings.mfaInitSkipLifetime.seconds > 0) &&
!availableMultiFactors.length &&
session?.factors?.user?.id
) {
const userResponse = await getUserByID({
serviceUrl,
userId: session.factors?.user?.id,
});
const humanUser =
userResponse?.user?.type.case === "human"
? userResponse?.user.type.value
: undefined;
if (humanUser?.mfaInitSkipped) {
const mfaInitSkippedTimestamp = timestampDate(humanUser.mfaInitSkipped);
const mfaInitSkipLifetimeMillis =
Number(loginSettings.mfaInitSkipLifetime.seconds) * 1000 +
loginSettings.mfaInitSkipLifetime.nanos / 1000000;
const currentTime = Date.now();
const mfaInitSkippedTime = mfaInitSkippedTimestamp.getTime();
const timeDifference = currentTime - mfaInitSkippedTime;
if (!(timeDifference > mfaInitSkipLifetimeMillis)) {
// if the time difference is smaller than the lifetime, skip the mfa setup
return;
}
}
// the user has never skipped the mfa init but we have a setting so we redirect
const params = new URLSearchParams({
loginName: session.factors?.user?.loginName as string,
force: "false", // this defines if the mfa is not forced in the settings and can be skipped
checkAfter: "true", // this defines if the check is directly made after the setup
});
if (requestId) {
params.append("requestId", requestId);
}
if (organization || session.factors?.user?.organizationId) {
@@ -191,28 +247,4 @@ export function checkMFAFactors(
// 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);
// }
}

View File

@@ -8,6 +8,10 @@ import {
} from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
import { OrganizationService } from "@zitadel/proto/zitadel/org/v2/org_service_pb";
import {
CreateResponseRequest,
SAMLService,
} from "@zitadel/proto/zitadel/saml/v2/saml_service_pb";
import { RequestChallenges } from "@zitadel/proto/zitadel/session/v2/challenge_pb";
import {
Checks,
@@ -55,11 +59,9 @@ async function cacheWrapper<T>(callback: Promise<T>) {
export async function getBrandingSettings({
serviceUrl,
organization,
}: {
serviceUrl: string;
organization?: string;
}) {
const settingsService: Client<typeof SettingsService> =
@@ -74,11 +76,9 @@ export async function getBrandingSettings({
export async function getLoginSettings({
serviceUrl,
organization,
}: {
serviceUrl: string;
organization?: string;
}) {
const settingsService: Client<typeof SettingsService> =
@@ -93,11 +93,9 @@ export async function getLoginSettings({
export async function getLockoutSettings({
serviceUrl,
orgId,
}: {
serviceUrl: string;
orgId?: string;
}) {
const settingsService: Client<typeof SettingsService> =
@@ -112,11 +110,9 @@ export async function getLockoutSettings({
export async function getPasswordExpirySettings({
serviceUrl,
orgId,
}: {
serviceUrl: string;
orgId?: string;
}) {
const settingsService: Client<typeof SettingsService> =
@@ -131,11 +127,9 @@ export async function getPasswordExpirySettings({
export async function listIDPLinks({
serviceUrl,
userId,
}: {
serviceUrl: string;
userId: string;
}) {
const userService: Client<typeof UserService> = await createServiceForHost(
@@ -148,11 +142,9 @@ export async function listIDPLinks({
export async function addOTPEmail({
serviceUrl,
userId,
}: {
serviceUrl: string;
userId: string;
}) {
const userService: Client<typeof UserService> = await createServiceForHost(
@@ -165,11 +157,9 @@ export async function addOTPEmail({
export async function addOTPSMS({
serviceUrl,
userId,
}: {
serviceUrl: string;
userId: string;
}) {
const userService: Client<typeof UserService> = await createServiceForHost(
@@ -182,11 +172,9 @@ export async function addOTPSMS({
export async function registerTOTP({
serviceUrl,
userId,
}: {
serviceUrl: string;
userId: string;
}) {
const userService: Client<typeof UserService> = await createServiceForHost(
@@ -214,11 +202,9 @@ export async function getGeneralSettings({
export async function getLegalAndSupportSettings({
serviceUrl,
organization,
}: {
serviceUrl: string;
organization?: string;
}) {
const settingsService: Client<typeof SettingsService> =
@@ -233,11 +219,9 @@ export async function getLegalAndSupportSettings({
export async function getPasswordComplexitySettings({
serviceUrl,
organization,
}: {
serviceUrl: string;
organization?: string;
}) {
const settingsService: Client<typeof SettingsService> =
@@ -252,32 +236,26 @@ export async function getPasswordComplexitySettings({
export async function createSessionFromChecks({
serviceUrl,
checks,
challenges,
lifetime,
}: {
serviceUrl: string;
checks: Checks;
challenges: RequestChallenges | undefined;
lifetime?: Duration;
}) {
const sessionService: Client<typeof SessionService> =
await createServiceForHost(SessionService, serviceUrl);
return sessionService.createSession({ checks, challenges, lifetime }, {});
return sessionService.createSession({ checks, lifetime }, {});
}
export async function createSessionForUserIdAndIdpIntent({
serviceUrl,
userId,
idpIntent,
lifetime,
}: {
serviceUrl: string;
userId: string;
idpIntent: {
idpIntentId?: string | undefined;
@@ -304,7 +282,6 @@ export async function createSessionForUserIdAndIdpIntent({
export async function setSession({
serviceUrl,
sessionId,
sessionToken,
challenges,
@@ -312,7 +289,6 @@ export async function setSession({
lifetime,
}: {
serviceUrl: string;
sessionId: string;
sessionToken: string;
challenges: RequestChallenges | undefined;
@@ -337,12 +313,10 @@ export async function setSession({
export async function getSession({
serviceUrl,
sessionId,
sessionToken,
}: {
serviceUrl: string;
sessionId: string;
sessionToken: string;
}) {
@@ -354,12 +328,10 @@ export async function getSession({
export async function deleteSession({
serviceUrl,
sessionId,
sessionToken,
}: {
serviceUrl: string;
sessionId: string;
sessionToken: string;
}) {
@@ -371,7 +343,6 @@ export async function deleteSession({
type ListSessionsCommand = {
serviceUrl: string;
ids: string[];
};
@@ -400,7 +371,6 @@ export async function listSessions({
export type AddHumanUserData = {
serviceUrl: string;
firstName: string;
lastName: string;
email: string;
@@ -410,7 +380,6 @@ export type AddHumanUserData = {
export async function addHumanUser({
serviceUrl,
email,
firstName,
lastName,
@@ -443,11 +412,9 @@ export async function addHumanUser({
export async function addHuman({
serviceUrl,
request,
}: {
serviceUrl: string;
request: AddHumanUserRequest;
}) {
const userService: Client<typeof UserService> = await createServiceForHost(
@@ -460,12 +427,10 @@ export async function addHuman({
export async function verifyTOTPRegistration({
serviceUrl,
code,
userId,
}: {
serviceUrl: string;
code: string;
userId: string;
}) {
@@ -479,11 +444,9 @@ export async function verifyTOTPRegistration({
export async function getUserByID({
serviceUrl,
userId,
}: {
serviceUrl: string;
userId: string;
}) {
const userService: Client<typeof UserService> = await createServiceForHost(
@@ -494,14 +457,27 @@ export async function getUserByID({
return userService.getUserByID({ userId }, {});
}
export async function humanMFAInitSkipped({
serviceUrl,
userId,
}: {
serviceUrl: string;
userId: string;
}) {
const userService: Client<typeof UserService> = await createServiceForHost(
UserService,
serviceUrl,
);
return userService.humanMFAInitSkipped({ userId }, {});
}
export async function verifyInviteCode({
serviceUrl,
userId,
verificationCode,
}: {
serviceUrl: string;
userId: string;
verificationCode: string;
}) {
@@ -515,11 +491,9 @@ export async function verifyInviteCode({
export async function resendInviteCode({
serviceUrl,
userId,
}: {
serviceUrl: string;
userId: string;
}) {
const userService: Client<typeof UserService> = await createServiceForHost(
@@ -532,12 +506,10 @@ export async function resendInviteCode({
export async function sendEmailCode({
serviceUrl,
userId,
urlTemplate,
}: {
serviceUrl: string;
userId: string;
urlTemplate: string;
}) {
@@ -563,12 +535,10 @@ export async function sendEmailCode({
export async function createInviteCode({
serviceUrl,
urlTemplate,
userId,
}: {
serviceUrl: string;
urlTemplate: string;
userId: string;
}) {
@@ -600,7 +570,6 @@ export async function createInviteCode({
export type ListUsersCommand = {
serviceUrl: string;
loginName?: string;
userName?: string;
email?: string;
@@ -610,7 +579,6 @@ export type ListUsersCommand = {
export async function listUsers({
serviceUrl,
loginName,
userName,
phone,
@@ -709,7 +677,6 @@ export async function listUsers({
export type SearchUsersCommand = {
serviceUrl: string;
searchValue: string;
loginSettings: LoginSettings;
organizationId?: string;
@@ -755,7 +722,6 @@ const EmailQuery = (searchValue: string) =>
* */
export async function searchUsers({
serviceUrl,
searchValue,
loginSettings,
organizationId,
@@ -900,11 +866,9 @@ export async function getDefaultOrg({
export async function getOrgsByDomain({
serviceUrl,
domain,
}: {
serviceUrl: string;
domain: string;
}) {
const orgService: Client<typeof OrganizationService> =
@@ -927,7 +891,6 @@ export async function getOrgsByDomain({
export async function startIdentityProviderFlow({
serviceUrl,
idpId,
urls,
}: {
@@ -952,7 +915,6 @@ export async function startIdentityProviderFlow({
export async function retrieveIdentityProviderInformation({
serviceUrl,
idpIntentId,
idpIntentToken,
}: {
@@ -974,11 +936,9 @@ export async function retrieveIdentityProviderInformation({
export async function getAuthRequest({
serviceUrl,
authRequestId,
}: {
serviceUrl: string;
authRequestId: string;
}) {
const oidcService = await createServiceForHost(OIDCService, serviceUrl);
@@ -990,11 +950,9 @@ export async function getAuthRequest({
export async function createCallback({
serviceUrl,
req,
}: {
serviceUrl: string;
req: CreateCallbackRequest;
}) {
const oidcService = await createServiceForHost(OIDCService, serviceUrl);
@@ -1002,14 +960,38 @@ export async function createCallback({
return oidcService.createCallback(req);
}
export async function getSAMLRequest({
serviceUrl,
samlRequestId,
}: {
serviceUrl: string;
samlRequestId: string;
}) {
const samlService = await createServiceForHost(SAMLService, serviceUrl);
return samlService.getSAMLRequest({
samlRequestId,
});
}
export async function createResponse({
serviceUrl,
req,
}: {
serviceUrl: string;
req: CreateResponseRequest;
}) {
const samlService = await createServiceForHost(SAMLService, serviceUrl);
return samlService.createResponse(req);
}
export async function verifyEmail({
serviceUrl,
userId,
verificationCode,
}: {
serviceUrl: string;
userId: string;
verificationCode: string;
}) {
@@ -1029,12 +1011,10 @@ export async function verifyEmail({
export async function resendEmailCode({
serviceUrl,
userId,
urlTemplate,
}: {
serviceUrl: string;
userId: string;
urlTemplate: string;
}) {
@@ -1058,12 +1038,10 @@ export async function resendEmailCode({
export async function retrieveIDPIntent({
serviceUrl,
id,
token,
}: {
serviceUrl: string;
id: string;
token: string;
}) {
@@ -1080,11 +1058,9 @@ export async function retrieveIDPIntent({
export async function getIDPByID({
serviceUrl,
id,
}: {
serviceUrl: string;
id: string;
}) {
const idpService: Client<typeof IdentityProviderService> =
@@ -1095,12 +1071,10 @@ export async function getIDPByID({
export async function addIDPLink({
serviceUrl,
idp,
userId,
}: {
serviceUrl: string;
idp: { id: string; userId: string; userName: string };
userId: string;
}) {
@@ -1124,12 +1098,10 @@ export async function addIDPLink({
export async function passwordReset({
serviceUrl,
userId,
urlTemplate,
}: {
serviceUrl: string;
userId: string;
urlTemplate?: string;
}) {
@@ -1161,14 +1133,12 @@ export async function passwordReset({
export async function setUserPassword({
serviceUrl,
userId,
password,
user,
code,
}: {
serviceUrl: string;
userId: string;
password: string;
user: User;
@@ -1224,11 +1194,9 @@ export async function setUserPassword({
export async function setPassword({
serviceUrl,
payload,
}: {
serviceUrl: string;
payload: SetPasswordRequest;
}) {
const userService: Client<typeof UserService> = await createServiceForHost(
@@ -1247,11 +1215,9 @@ export async function setPassword({
*/
export async function createPasskeyRegistrationLink({
serviceUrl,
userId,
}: {
serviceUrl: string;
userId: string;
}) {
const userService: Client<typeof UserService> = await createServiceForHost(
@@ -1277,12 +1243,10 @@ export async function createPasskeyRegistrationLink({
*/
export async function registerU2F({
serviceUrl,
userId,
domain,
}: {
serviceUrl: string;
userId: string;
domain: string;
}) {
@@ -1305,11 +1269,9 @@ export async function registerU2F({
*/
export async function verifyU2FRegistration({
serviceUrl,
request,
}: {
serviceUrl: string;
request: VerifyU2FRegistrationRequest;
}) {
const userService: Client<typeof UserService> = await createServiceForHost(
@@ -1329,12 +1291,10 @@ export async function verifyU2FRegistration({
*/
export async function getActiveIdentityProviders({
serviceUrl,
orgId,
linking_allowed,
}: {
serviceUrl: string;
orgId?: string;
linking_allowed?: boolean;
}) {
@@ -1356,11 +1316,9 @@ export async function getActiveIdentityProviders({
*/
export async function verifyPasskeyRegistration({
serviceUrl,
request,
}: {
serviceUrl: string;
request: VerifyPasskeyRegistrationRequest;
}) {
const userService: Client<typeof UserService> = await createServiceForHost(
@@ -1381,13 +1339,11 @@ export async function verifyPasskeyRegistration({
*/
export async function registerPasskey({
serviceUrl,
userId,
code,
domain,
}: {
serviceUrl: string;
userId: string;
code: { id: string; code: string };
domain: string;
@@ -1415,7 +1371,6 @@ export async function listAuthenticationMethodTypes({
userId,
}: {
serviceUrl: string;
userId: string;
}) {
const userService: Client<typeof UserService> = await createServiceForHost(

View File

@@ -4,6 +4,7 @@ import { IdentityProviderService } from "@zitadel/proto/zitadel/idp/v2/idp_servi
import { RequestContextSchema } from "@zitadel/proto/zitadel/object/v2/object_pb.js";
import { OIDCService } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb.js";
import { OrganizationService } from "@zitadel/proto/zitadel/org/v2/org_service_pb.js";
import { SAMLService } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb.js";
import { SessionService } from "@zitadel/proto/zitadel/session/v2/session_service_pb.js";
import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb.js";
import { UserService } from "@zitadel/proto/zitadel/user/v2/user_service_pb.js";
@@ -14,6 +15,7 @@ export const createUserServiceClient = createClientFor(UserService);
export const createSettingsServiceClient = createClientFor(SettingsService);
export const createSessionServiceClient = createClientFor(SessionService);
export const createOIDCServiceClient = createClientFor(OIDCService);
export const createSAMLServiceClient = createClientFor(SAMLService);
export const createOrganizationServiceClient = createClientFor(OrganizationService);
export const createFeatureServiceClient = createClientFor(FeatureService);
export const createIdpServiceClient = createClientFor(IdentityProviderService);