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 ExpirationDate: 2099-01-01T00:00:00Z
DefaultInstance: 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: PrivacyPolicy:
TOSLink: "https://zitadel.com/docs/legal/terms-of-service" TOSLink: "https://zitadel.com/docs/legal/terms-of-service"
PrivacyLink: "https://zitadel.com/docs/legal/policies/privacy-policy" PrivacyLink: "https://zitadel.com/docs/legal/policies/privacy-policy"

View File

@@ -71,7 +71,8 @@
}, },
"set": { "set": {
"title": "2-Faktor einrichten", "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": { "otp": {

View File

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

View File

@@ -71,7 +71,8 @@
}, },
"set": { "set": {
"title": "Configurar autenticación de 2 factores", "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": { "otp": {

View File

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

View File

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

View File

@@ -389,7 +389,6 @@ In future, self service options to jump to are shown below, like:
## Currently NOT Supported ## Currently NOT Supported
- Login Settings: multifactor init prompt
- forceMFA on login settings is not checked for IDPs - 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. 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 locale = getLocale();
const t = await getTranslations({ locale, namespace: "accounts" }); const t = await getTranslations({ locale, namespace: "accounts" });
const authRequestId = searchParams?.authRequestId; const requestId = searchParams?.requestId;
const organization = searchParams?.organization; const organization = searchParams?.organization;
const _headers = await headers(); const _headers = await headers();
@@ -62,8 +62,8 @@ export default async function Page(props: {
const params = new URLSearchParams(); const params = new URLSearchParams();
if (authRequestId) { if (requestId) {
params.append("authRequestId", authRequestId); params.append("requestId", requestId);
} }
if (organization) { if (organization) {
@@ -77,7 +77,7 @@ export default async function Page(props: {
<p className="ztdl-p mb-6 block">{t("description")}</p> <p className="ztdl-p mb-6 block">{t("description")}</p>
<div className="flex flex-col w-full space-y-2"> <div className="flex flex-col w-full space-y-2">
<SessionsList sessions={sessions} authRequestId={authRequestId} /> <SessionsList sessions={sessions} requestId={requestId} />
<Link href={`/loginname?` + params}> <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="flex flex-row items-center py-3 px-4 hover:bg-black/10 dark:hover:bg-white/10 rounded-md transition-all">
<div className="w-8 h-8 mr-4 flex flex-row justify-center items-center rounded-full bg-black/5 dark:bg-white/5"> <div className="w-8 h-8 mr-4 flex flex-row justify-center items-center rounded-full bg-black/5 dark:bg-white/5">

View File

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

View File

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

View File

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

View File

@@ -20,7 +20,7 @@ export default async function Page(props: {
const t = await getTranslations({ locale, namespace: "loginname" }); const t = await getTranslations({ locale, namespace: "loginname" });
const loginName = searchParams?.loginName; const loginName = searchParams?.loginName;
const authRequestId = searchParams?.authRequestId; const requestId = searchParams?.requestId;
const organization = searchParams?.organization; const organization = searchParams?.organization;
const suffix = searchParams?.suffix; const suffix = searchParams?.suffix;
const submit: boolean = searchParams?.submit === "true"; const submit: boolean = searchParams?.submit === "true";
@@ -72,7 +72,7 @@ export default async function Page(props: {
<UsernameForm <UsernameForm
loginName={loginName} 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 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} loginSettings={contextLoginSettings}
suffix={suffix} suffix={suffix}
@@ -82,7 +82,7 @@ export default async function Page(props: {
{identityProviders && ( {identityProviders && (
<SignInWithIdp <SignInWithIdp
identityProviders={identityProviders} identityProviders={identityProviders}
authRequestId={authRequestId} requestId={requestId}
organization={organization} organization={organization}
></SignInWithIdp> ></SignInWithIdp>
)} )}

View File

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

View File

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

View File

@@ -34,7 +34,7 @@ export default async function Page(props: {
const { const {
loginName, // send from password page loginName, // send from password page
userId, // send from email link userId, // send from email link
authRequestId, requestId,
sessionId, sessionId,
organization, organization,
code, code,
@@ -115,7 +115,7 @@ export default async function Page(props: {
<LoginOTP <LoginOTP
loginName={loginName ?? session.factors?.user?.loginName} loginName={loginName ?? session.factors?.user?.loginName}
sessionId={sessionId} sessionId={sessionId}
authRequestId={authRequestId} requestId={requestId}
organization={ organization={
organization ?? session?.factors?.user?.organizationId 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 t = await getTranslations({ locale, namespace: "otp" });
const tError = await getTranslations({ locale, namespace: "error" }); const tError = await getTranslations({ locale, namespace: "error" });
const { loginName, organization, sessionId, authRequestId, checkAfter } = const { loginName, organization, sessionId, requestId, checkAfter } =
searchParams; searchParams;
const { method } = params; const { method } = params;
@@ -111,22 +111,22 @@ export default async function Page(props: {
} }
if (checkAfter) { if (checkAfter) {
if (authRequestId) { if (requestId) {
paramsToContinue.append("authRequestId", authRequestId); paramsToContinue.append("requestId", requestId);
} }
urlToContinue = `/otp/${method}?` + paramsToContinue; urlToContinue = `/otp/${method}?` + paramsToContinue;
// immediately check the OTP on the next page if sms or email was set up // immediately check the OTP on the next page if sms or email was set up
if (["email", "sms"].includes(method)) { if (["email", "sms"].includes(method)) {
return redirect(urlToContinue); return redirect(urlToContinue);
} }
} else if (authRequestId && sessionId) { } else if (requestId && sessionId) {
if (authRequestId) { if (requestId) {
paramsToContinue.append("authRequest", authRequestId); paramsToContinue.append("authRequest", requestId);
} }
urlToContinue = `/login?` + paramsToContinue; urlToContinue = `/login?` + paramsToContinue;
} else if (loginName) { } else if (loginName) {
if (authRequestId) { if (requestId) {
paramsToContinue.append("authRequestId", authRequestId); paramsToContinue.append("requestId", requestId);
} }
urlToContinue = `/signedin?` + paramsToContinue; urlToContinue = `/signedin?` + paramsToContinue;
} }
@@ -165,7 +165,7 @@ export default async function Page(props: {
secret={totpResponse.secret as string} secret={totpResponse.secret as string}
loginName={loginName} loginName={loginName}
sessionId={sessionId} sessionId={sessionId}
authRequestId={authRequestId} requestId={requestId}
organization={organization} organization={organization}
checkAfter={checkAfter === "true"} checkAfter={checkAfter === "true"}
loginSettings={loginSettings} loginSettings={loginSettings}

View File

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

View File

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

View File

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

View File

@@ -22,7 +22,7 @@ export default async function Page(props: {
const t = await getTranslations({ locale, namespace: "password" }); const t = await getTranslations({ locale, namespace: "password" });
const tError = await getTranslations({ locale, namespace: "error" }); const tError = await getTranslations({ locale, namespace: "error" });
let { loginName, organization, authRequestId, alt } = searchParams; let { loginName, organization, requestId, alt } = searchParams;
const _headers = await headers(); const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers); const { serviceUrl } = getServiceUrlFromHeaders(_headers);
@@ -93,7 +93,7 @@ export default async function Page(props: {
{loginName && ( {loginName && (
<PasswordForm <PasswordForm
loginName={loginName} 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 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} loginSettings={loginSettings}
promptPasswordless={ promptPasswordless={

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,7 +17,7 @@ export default async function Page(props: {
const t = await getTranslations({ locale, namespace: "u2f" }); const t = await getTranslations({ locale, namespace: "u2f" });
const tError = await getTranslations({ locale, namespace: "error" }); const tError = await getTranslations({ locale, namespace: "error" });
const { loginName, authRequestId, sessionId, organization } = searchParams; const { loginName, requestId, sessionId, organization } = searchParams;
const _headers = await headers(); const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers); const { serviceUrl } = getServiceUrlFromHeaders(_headers);
@@ -80,7 +80,7 @@ export default async function Page(props: {
<LoginPasskey <LoginPasskey
loginName={loginName} loginName={loginName}
sessionId={sessionId} sessionId={sessionId}
authRequestId={authRequestId} requestId={requestId}
altPassword={false} altPassword={false}
organization={organization} organization={organization}
login={false} // this sets the userVerificationRequirement to discouraged as its used as second factor 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 t = await getTranslations({ locale, namespace: "u2f" });
const tError = await getTranslations({ locale, namespace: "error" }); const tError = await getTranslations({ locale, namespace: "error" });
const { loginName, organization, authRequestId, checkAfter } = searchParams; const { loginName, organization, requestId, checkAfter } = searchParams;
const _headers = await headers(); const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers); const { serviceUrl } = getServiceUrlFromHeaders(_headers);
@@ -62,7 +62,7 @@ export default async function Page(props: {
loginName={loginName} loginName={loginName}
sessionId={sessionFactors.id} sessionId={sessionFactors.id}
organization={organization} organization={organization}
authRequestId={authRequestId} requestId={requestId}
checkAfter={checkAfter === "true"} 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 t = await getTranslations({ locale, namespace: "verify" });
const tError = await getTranslations({ locale, namespace: "error" }); const tError = await getTranslations({ locale, namespace: "error" });
const { userId, loginName, code, organization, authRequestId, invite } = const { userId, loginName, code, organization, requestId, invite } =
searchParams; searchParams;
const _headers = await headers(); const _headers = await headers();
@@ -64,7 +64,7 @@ export default async function Page(props: { searchParams: Promise<any> }) {
userId: sessionFactors?.factors?.user?.id, userId: sessionFactors?.factors?.user?.id,
urlTemplate: urlTemplate:
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` + `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` +
(authRequestId ? `&authRequestId=${authRequestId}` : ""), (requestId ? `&requestId=${requestId}` : ""),
}).catch((error) => { }).catch((error) => {
console.error("Could not resend verification email", error); console.error("Could not resend verification email", error);
throw Error("Failed to send verification email"); throw Error("Failed to send verification email");
@@ -77,7 +77,7 @@ export default async function Page(props: { searchParams: Promise<any> }) {
userId, userId,
urlTemplate: urlTemplate:
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` + `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` +
(authRequestId ? `&authRequestId=${authRequestId}` : ""), (requestId ? `&requestId=${requestId}` : ""),
}).catch((error) => { }).catch((error) => {
console.error("Could not resend verification email", error); console.error("Could not resend verification email", error);
throw Error("Failed to send verification email"); throw Error("Failed to send verification email");
@@ -120,8 +120,8 @@ export default async function Page(props: { searchParams: Promise<any> }) {
params.set("organization", organization); params.set("organization", organization);
} }
if (authRequestId) { if (requestId) {
params.set("authRequestId", authRequestId); params.set("requestId", requestId);
} }
return ( return (
@@ -165,7 +165,7 @@ export default async function Page(props: { searchParams: Promise<any> }) {
userId={id} userId={id}
loginName={loginName} loginName={loginName}
organization={organization} organization={organization}
authRequestId={authRequestId} requestId={requestId}
authMethods={authMethods} authMethods={authMethods}
/> />
) : ( ) : (
@@ -176,7 +176,7 @@ export default async function Page(props: { searchParams: Promise<any> }) {
userId={id} userId={id}
code={code} code={code}
isInvite={invite === "true"} isInvite={invite === "true"}
authRequestId={authRequestId} requestId={requestId}
/> />
))} ))}
</div> </div>

View File

@@ -1,28 +1,28 @@
import { getAllSessions } from "@/lib/cookies"; import { getAllSessions } from "@/lib/cookies";
import { idpTypeToSlug } from "@/lib/idp"; import { idpTypeToSlug } from "@/lib/idp";
import { loginWithOIDCandSession } from "@/lib/oidc";
import { loginWithSAMLandSession } from "@/lib/saml";
import { sendLoginname, SendLoginnameCommand } from "@/lib/server/loginname"; import { sendLoginname, SendLoginnameCommand } from "@/lib/server/loginname";
import { getServiceUrlFromHeaders } from "@/lib/service"; import { getServiceUrlFromHeaders } from "@/lib/service";
import { findValidSession } from "@/lib/session";
import { import {
createCallback, createCallback,
createResponse,
getActiveIdentityProviders, getActiveIdentityProviders,
getAuthRequest, getAuthRequest,
getLoginSettings,
getOrgsByDomain, getOrgsByDomain,
listAuthenticationMethodTypes, getSAMLRequest,
listSessions, listSessions,
startIdentityProviderFlow, startIdentityProviderFlow,
} from "@/lib/zitadel"; } from "@/lib/zitadel";
import { create, timestampDate } from "@zitadel/client"; import { create } from "@zitadel/client";
import { import { Prompt } from "@zitadel/proto/zitadel/oidc/v2/authorization_pb";
AuthRequest,
Prompt,
} from "@zitadel/proto/zitadel/oidc/v2/authorization_pb";
import { import {
CreateCallbackRequestSchema, CreateCallbackRequestSchema,
SessionSchema, SessionSchema,
} from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb"; } 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 { 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 { headers } from "next/headers";
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
@@ -30,12 +30,32 @@ export const dynamic = "force-dynamic";
export const revalidate = false; export const revalidate = false;
export const fetchCache = "default-no-store"; 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({ async function loadSessions({
serviceUrl, serviceUrl,
ids, ids,
}: { }: {
serviceUrl: string; serviceUrl: string;
ids: string[]; ids: string[];
}): Promise<Session[]> { }): Promise<Session[]> {
const response = await listSessions({ 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 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:(.+)/; 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) { 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 _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_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 // 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"); const _rsc = searchParams.get("_rsc");
if (_rsc) { if (_rsc) {
@@ -232,128 +100,36 @@ export async function GET(request: NextRequest) {
sessions = await loadSessions({ serviceUrl, ids }); sessions = await loadSessions({ serviceUrl, ids });
} }
if (authRequestId && sessionId) { // complete flow if session and request id are provided
console.log( if (requestId && sessionId) {
`Login with session: ${sessionId} and authRequest: ${authRequestId}`, if (requestId.startsWith("oidc_")) {
); // this finishes the login process for OIDC
return loginWithOIDCandSession({
const selectedSession = sessions.find((s) => s.id === sessionId);
if (selectedSession && selectedSession.id) {
console.log(`Found session ${selectedSession.id}`);
const isValid = await isSessionValid(
serviceUrl, serviceUrl,
authRequest: requestId.replace("oidc_", ""),
selectedSession, sessionId,
); sessions,
sessionCookies,
console.log("Session is valid:", isValid); request,
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),
},
}),
}); });
if (callbackUrl) { } else if (requestId.startsWith("saml_")) {
return NextResponse.redirect(callbackUrl); // this finishes the login process for SAML
} else { return loginWithSAMLandSession({
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({
serviceUrl, serviceUrl,
samlRequest: requestId.replace("saml_", ""),
organization: selectedSession.factors?.user?.organizationId, 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({ const { authRequest } = await getAuthRequest({
serviceUrl, serviceUrl,
authRequestId: requestId.replace("oidc_", ""),
authRequestId,
}); });
let organization = ""; let organization = "";
@@ -400,7 +176,6 @@ export async function GET(request: NextRequest) {
const identityProviders = await getActiveIdentityProviders({ const identityProviders = await getActiveIdentityProviders({
serviceUrl, serviceUrl,
orgId: organization ? organization : undefined, orgId: organization ? organization : undefined,
}).then((resp) => { }).then((resp) => {
return resp.identityProviders; return resp.identityProviders;
@@ -416,8 +191,8 @@ export async function GET(request: NextRequest) {
const params = new URLSearchParams(); const params = new URLSearchParams();
if (authRequestId) { if (requestId) {
params.set("authRequestId", authRequestId); params.set("requestId", requestId);
} }
if (organization) { if (organization) {
@@ -426,7 +201,6 @@ export async function GET(request: NextRequest) {
return startIdentityProviderFlow({ return startIdentityProviderFlow({
serviceUrl, serviceUrl,
idpId, idpId,
urls: { urls: {
successUrl: 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)) { if (authRequest && authRequest.prompt.includes(Prompt.CREATE)) {
const registerUrl = constructUrl(request, "/register"); const registerUrl = new URL("/register", request.url);
const params = new URLSearchParams();
if (authRequest.id) { if (authRequest.id) {
params.append("authRequestId", authRequest.id); registerUrl.searchParams.set("requestId", `oidc_${authRequest.id}`);
// registerUrl.searchParams.set("authRequestId", authRequest.id);
} }
if (organization) { 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 // use existing session and hydrate it for oidc
if (authRequest && sessions.length) { if (authRequest && sessions.length) {
// if some accounts are available for selection and select_account is set // if some accounts are available for selection and select_account is set
if (authRequest.prompt.includes(Prompt.SELECT_ACCOUNT)) { if (authRequest.prompt.includes(Prompt.SELECT_ACCOUNT)) {
return gotoAccounts(); return gotoAccounts({
request,
requestId: `oidc_${authRequest.id}`,
organization,
});
} else if (authRequest.prompt.includes(Prompt.LOGIN)) { } 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 * 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 { try {
let command: SendLoginnameCommand = { let command: SendLoginnameCommand = {
loginName: authRequest.loginHint, loginName: authRequest.loginHint,
authRequestId: authRequest.id, requestId: authRequest.id,
}; };
if (organization) { if (organization) {
@@ -503,7 +263,7 @@ export async function GET(request: NextRequest) {
const res = await sendLoginname(command); const res = await sendLoginname(command);
if (res && "redirect" in res && res?.redirect) { 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()); return NextResponse.redirect(absoluteUrl.toString());
} }
} catch (error) { } catch (error) {
@@ -511,39 +271,31 @@ export async function GET(request: NextRequest) {
} }
} }
const loginNameUrl = constructUrl(request, "/loginname"); const loginNameUrl = new URL("/loginname", request.url);
const params = new URLSearchParams();
if (authRequest.id) { if (authRequest.id) {
params.append("authRequestId", authRequest.id); loginNameUrl.searchParams.set("requestId", `oidc_${authRequest.id}`);
// loginNameUrl.searchParams.set("authRequestId", authRequest.id);
} }
if (authRequest.loginHint) { if (authRequest.loginHint) {
params.append("loginName", authRequest.loginHint); loginNameUrl.searchParams.set("loginName", authRequest.loginHint);
// loginNameUrl.searchParams.set("loginName", authRequest.loginHint);
} }
if (organization) { if (organization) {
params.append("organization", organization); loginNameUrl.searchParams.set("organization", organization);
// loginNameUrl.searchParams.set("organization", organization);
} }
if (suffix) { 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)) { } 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. * 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. * 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 * 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, serviceUrl,
sessions, sessions,
authRequest, authRequest,
); });
if (!selectedSession || !selectedSession.id) { if (!selectedSession || !selectedSession.id) {
return NextResponse.json( return NextResponse.json(
@@ -570,9 +322,8 @@ export async function GET(request: NextRequest) {
const { callbackUrl } = await createCallback({ const { callbackUrl } = await createCallback({
serviceUrl, serviceUrl,
req: create(CreateCallbackRequestSchema, { req: create(CreateCallbackRequestSchema, {
authRequestId, authRequestId: requestId.replace("oidc_", ""),
callbackKind: { callbackKind: {
case: "session", case: "session",
value: create(SessionSchema, session), value: create(SessionSchema, session),
@@ -582,15 +333,18 @@ export async function GET(request: NextRequest) {
return NextResponse.redirect(callbackUrl); return NextResponse.redirect(callbackUrl);
} else { } else {
// check for loginHint, userId hint and valid sessions // check for loginHint, userId hint and valid sessions
let selectedSession = await findValidSession( let selectedSession = await findValidSession({
serviceUrl, serviceUrl,
sessions, sessions,
authRequest, authRequest,
); });
if (!selectedSession || !selectedSession.id) { if (!selectedSession || !selectedSession.id) {
return gotoAccounts(); return gotoAccounts({
request,
requestId: `oidc_${authRequest.id}`,
organization,
});
} }
const cookie = sessionCookies.find( const cookie = sessionCookies.find(
@@ -598,7 +352,11 @@ export async function GET(request: NextRequest) {
); );
if (!cookie || !cookie.id || !cookie.token) { if (!cookie || !cookie.id || !cookie.token) {
return gotoAccounts(); return gotoAccounts({
request,
requestId: `oidc_${authRequest.id}`,
organization,
});
} }
const session = { const session = {
@@ -611,7 +369,7 @@ export async function GET(request: NextRequest) {
serviceUrl, serviceUrl,
req: create(CreateCallbackRequestSchema, { req: create(CreateCallbackRequestSchema, {
authRequestId, authRequestId: requestId.replace("oidc_", ""),
callbackKind: { callbackKind: {
case: "session", case: "session",
value: create(SessionSchema, session), value: create(SessionSchema, session),
@@ -624,36 +382,148 @@ export async function GET(request: NextRequest) {
console.log( console.log(
"could not create callback, redirect user to choose other account", "could not create callback, redirect user to choose other account",
); );
return gotoAccounts(); return gotoAccounts({
request,
organization,
requestId: `oidc_${authRequest.id}`,
});
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return gotoAccounts(); return gotoAccounts({
request,
requestId: `oidc_${authRequest.id}`,
organization,
});
} }
} }
} else { } else {
const loginNameUrl = constructUrl(request, "/loginname"); const loginNameUrl = new URL("/loginname", request.url);
const params = new URLSearchParams(); loginNameUrl.searchParams.set("requestId", requestId);
params.set("authRequestId", authRequestId);
// loginNameUrl.searchParams.set("authRequestId", authRequestId);
if (authRequest?.loginHint) { if (authRequest?.loginHint) {
params.set("loginName", authRequest.loginHint); loginNameUrl.searchParams.set("loginName", authRequest.loginHint);
params.set("submit", "true"); // autosubmit loginNameUrl.searchParams.set("submit", "true"); // autosubmit
// loginNameUrl.searchParams.set("loginName", authRequest.loginHint);
// loginNameUrl.searchParams.set("submit", "true"); // autosubmit
} }
if (organization) { if (organization) {
params.set("organization", organization); loginNameUrl.searchParams.append("organization", organization);
// loginNameUrl.searchParams.set("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 { } else {
return NextResponse.json( return NextResponse.json(
{ error: "No authRequestId provided" }, { error: "No authRequest nor samlRequest provided" },
{ status: 500 }, { status: 500 },
); );
} }

View File

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

View File

@@ -1,35 +1,44 @@
"use client"; "use client";
import { skipMFAAndContinueWithNextUrl } from "@/lib/server/session";
import { import {
LoginSettings, LoginSettings,
SecondFactorType, SecondFactorType,
} from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { EMAIL, SMS, TOTP, U2F } from "./auth-methods"; import { EMAIL, SMS, TOTP, U2F } from "./auth-methods";
type Props = { type Props = {
userId: string;
loginName?: string; loginName?: string;
sessionId?: string; sessionId?: string;
authRequestId?: string; requestId?: string;
organization?: string; organization?: string;
loginSettings: LoginSettings; loginSettings: LoginSettings;
userMethods: AuthenticationMethodType[]; userMethods: AuthenticationMethodType[];
checkAfter: boolean; checkAfter: boolean;
phoneVerified: boolean; phoneVerified: boolean;
emailVerified: boolean; emailVerified: boolean;
force: boolean;
}; };
export function ChooseSecondFactorToSetup({ export function ChooseSecondFactorToSetup({
userId,
loginName, loginName,
sessionId, sessionId,
authRequestId, requestId,
organization, organization,
loginSettings, loginSettings,
userMethods, userMethods,
checkAfter, checkAfter,
phoneVerified, phoneVerified,
emailVerified, emailVerified,
force,
}: Props) { }: Props) {
const t = useTranslations("mfa");
const router = useRouter();
const params = new URLSearchParams({}); const params = new URLSearchParams({});
if (loginName) { if (loginName) {
@@ -38,8 +47,8 @@ export function ChooseSecondFactorToSetup({
if (sessionId) { if (sessionId) {
params.append("sessionId", sessionId); params.append("sessionId", sessionId);
} }
if (authRequestId) { if (requestId) {
params.append("authRequestId", authRequestId); params.append("requestId", requestId);
} }
if (organization) { if (organization) {
params.append("organization", organization); params.append("organization", organization);
@@ -49,6 +58,7 @@ export function ChooseSecondFactorToSetup({
} }
return ( return (
<>
<div className="grid grid-cols-1 gap-5 w-full pt-4"> <div className="grid grid-cols-1 gap-5 w-full pt-4">
{loginSettings.secondFactors.map((factor) => { {loginSettings.secondFactors.map((factor) => {
switch (factor) { switch (factor) {
@@ -83,5 +93,28 @@ export function ChooseSecondFactorToSetup({
} }
})} })}
</div> </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 = { type Props = {
loginName?: string; loginName?: string;
sessionId?: string; sessionId?: string;
authRequestId?: string; requestId?: string;
organization?: string; organization?: string;
userMethods: AuthenticationMethodType[]; userMethods: AuthenticationMethodType[];
}; };
@@ -14,7 +14,7 @@ type Props = {
export function ChooseSecondFactor({ export function ChooseSecondFactor({
loginName, loginName,
sessionId, sessionId,
authRequestId, requestId,
organization, organization,
userMethods, userMethods,
}: Props) { }: Props) {
@@ -26,8 +26,8 @@ export function ChooseSecondFactor({
if (sessionId) { if (sessionId) {
params.append("sessionId", sessionId); params.append("sessionId", sessionId);
} }
if (authRequestId) { if (requestId) {
params.append("authRequestId", authRequestId); params.append("requestId", requestId);
} }
if (organization) { if (organization) {
params.append("organization", organization); params.append("organization", organization);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,12 @@
type FinishFlowCommand = type FinishFlowCommand =
| { | {
sessionId: string; sessionId: string;
authRequestId: string; requestId: string;
} }
| { loginName: 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 * @param command
* @returns * @returns
*/ */
@@ -14,10 +14,10 @@ export async function getNextUrl(
command: FinishFlowCommand & { organization?: string }, command: FinishFlowCommand & { organization?: string },
defaultRedirectUri?: string, defaultRedirectUri?: string,
): Promise<string> { ): Promise<string> {
if ("sessionId" in command && "authRequestId" in command) { if ("sessionId" in command && "requestId" in command) {
const params = new URLSearchParams({ const params = new URLSearchParams({
sessionId: command.sessionId, sessionId: command.sessionId,
authRequest: command.authRequestId, requestId: command.requestId,
}); });
if (command.organization) { if (command.organization) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -59,7 +59,7 @@ type VerifyUserByEmailCommand = {
organization?: string; organization?: string;
code: string; code: string;
isInvite: boolean; isInvite: boolean;
authRequestId?: string; requestId?: string;
}; };
export async function sendVerification(command: VerifyUserByEmailCommand) { export async function sendVerification(command: VerifyUserByEmailCommand) {
@@ -155,11 +155,7 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
}, },
}); });
session = await createSessionAndUpdateCookie( session = await createSessionAndUpdateCookie(checks, command.requestId);
checks,
undefined,
command.authRequestId,
);
} }
if (!session?.factors?.user?.id) { 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 // redirect to mfa factor if user has one, or redirect to set one up
const mfaFactorCheck = checkMFAFactors( const mfaFactorCheck = await checkMFAFactors(
serviceUrl,
session, session,
loginSettings, loginSettings,
authMethodResponse.authMethodTypes, authMethodResponse.authMethodTypes,
command.organization, command.organization,
command.authRequestId, command.requestId,
); );
if (mfaFactorCheck?.redirect) { if (mfaFactorCheck?.redirect) {
@@ -220,11 +217,11 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
} }
// login user if no additional steps are required // login user if no additional steps are required
if (command.authRequestId && session.id) { if (command.requestId && session.id) {
const nextUrl = await getNextUrl( const nextUrl = await getNextUrl(
{ {
sessionId: session.id, sessionId: session.id,
authRequestId: command.authRequestId, requestId: command.requestId,
organization: organization:
command.organization ?? session.factors?.user?.organizationId, command.organization ?? session.factors?.user?.organizationId,
}, },
@@ -248,7 +245,7 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
type resendVerifyEmailCommand = { type resendVerifyEmailCommand = {
userId: string; userId: string;
isInvite: boolean; isInvite: boolean;
authRequestId?: string; requestId?: string;
}; };
export async function resendVerification(command: resendVerifyEmailCommand) { export async function resendVerification(command: resendVerifyEmailCommand) {
@@ -269,9 +266,7 @@ export async function resendVerification(command: resendVerifyEmailCommand) {
serviceUrl, serviceUrl,
urlTemplate: urlTemplate:
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` + `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` +
(command.authRequestId (command.requestId ? `&requestId=${command.requestId}` : ""),
? `&authRequestId=${command.authRequestId}`
: ""),
}); });
} }
@@ -292,7 +287,7 @@ export async function sendEmailCode(command: sendEmailCommand) {
export type SendVerificationRedirectWithoutCheckCommand = { export type SendVerificationRedirectWithoutCheckCommand = {
organization?: string; organization?: string;
authRequestId?: string; requestId?: string;
} & ( } & (
| { userId: string; loginName?: never } | { userId: string; loginName?: never }
| { userId?: never; loginName: string } | { userId?: never; loginName: string }
@@ -371,11 +366,7 @@ export async function sendVerificationRedirectWithoutCheck(
}, },
}); });
session = await createSessionAndUpdateCookie( session = await createSessionAndUpdateCookie(checks, command.requestId);
checks,
undefined,
command.authRequestId,
);
} }
if (!session?.factors?.user?.id) { if (!session?.factors?.user?.id) {
@@ -418,17 +409,17 @@ export async function sendVerificationRedirectWithoutCheck(
const loginSettings = await getLoginSettings({ const loginSettings = await getLoginSettings({
serviceUrl, serviceUrl,
organization: user.details?.resourceOwner, organization: user.details?.resourceOwner,
}); });
// redirect to mfa factor if user has one, or redirect to set one up // redirect to mfa factor if user has one, or redirect to set one up
const mfaFactorCheck = checkMFAFactors( const mfaFactorCheck = await checkMFAFactors(
serviceUrl,
session, session,
loginSettings, loginSettings,
authMethodResponse.authMethodTypes, authMethodResponse.authMethodTypes,
command.organization, command.organization,
command.authRequestId, command.requestId,
); );
if (mfaFactorCheck?.redirect) { if (mfaFactorCheck?.redirect) {
@@ -436,11 +427,11 @@ export async function sendVerificationRedirectWithoutCheck(
} }
// login user if no additional steps are required // login user if no additional steps are required
if (command.authRequestId && session.id) { if (command.requestId && session.id) {
const nextUrl = await getNextUrl( const nextUrl = await getNextUrl(
{ {
sessionId: session.id, sessionId: session.id,
authRequestId: command.authRequestId, requestId: command.requestId,
organization: organization:
command.organization ?? session.factors?.user?.organizationId, 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 { IdentityProviderService } from "@zitadel/proto/zitadel/idp/v2/idp_service_pb";
import { OIDCService } from "@zitadel/proto/zitadel/oidc/v2/oidc_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 { 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 { SessionService } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb"; import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb";
import { UserService } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { UserService } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
@@ -15,7 +16,8 @@ type ServiceClass =
| typeof OrganizationService | typeof OrganizationService
| typeof SessionService | typeof SessionService
| typeof OIDCService | typeof OIDCService
| typeof SettingsService; | typeof SettingsService
| typeof SAMLService;
export async function createServiceForHost<T extends ServiceClass>( export async function createServiceForHost<T extends ServiceClass>(
service: T, 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 { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { GetSessionResponse } from "@zitadel/proto/zitadel/session/v2/session_service_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 { getMostRecentCookieWithLoginname } from "./cookies";
import { getSession } from "./zitadel"; import {
getLoginSettings,
getSession,
listAuthenticationMethodTypes,
} from "./zitadel";
type LoadMostRecentSessionParams = { type LoadMostRecentSessionParams = {
serviceUrl: string; serviceUrl: string;
@@ -29,3 +37,160 @@ export async function loadMostRecentSession({
sessionToken: recent.token, sessionToken: recent.token,
}).then((resp: GetSessionResponse) => resp.session); }).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 { HumanUser } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import moment from "moment"; import moment from "moment";
import { getUserByID } from "./zitadel";
export function checkPasswordChangeRequired( export function checkPasswordChangeRequired(
expirySettings: PasswordExpirySettings | undefined, expirySettings: PasswordExpirySettings | undefined,
session: Session, session: Session,
humanUser: HumanUser | undefined, humanUser: HumanUser | undefined,
organization?: string, organization?: string,
authRequestId?: string, requestId?: string,
) { ) {
let isOutdated = false; let isOutdated = false;
if (expirySettings?.maxAgeDays && humanUser?.passwordChanged) { if (expirySettings?.maxAgeDays && humanUser?.passwordChanged) {
@@ -35,8 +36,8 @@ export function checkPasswordChangeRequired(
); );
} }
if (authRequestId) { if (requestId) {
params.append("authRequestId", authRequestId); params.append("requestId", requestId);
} }
return { redirect: "/password/change?" + params }; return { redirect: "/password/change?" + params };
@@ -47,7 +48,7 @@ export function checkInvite(
session: Session, session: Session,
humanUser?: HumanUser, humanUser?: HumanUser,
organization?: string, organization?: string,
authRequestId?: string, requestId?: string,
) { ) {
if (!humanUser?.email?.isVerified) { if (!humanUser?.email?.isVerified) {
const paramsVerify = new URLSearchParams({ const paramsVerify = new URLSearchParams({
@@ -63,8 +64,8 @@ export function checkInvite(
); );
} }
if (authRequestId) { if (requestId) {
paramsVerify.append("authRequestId", authRequestId); paramsVerify.append("requestId", requestId);
} }
return { redirect: "/verify?" + paramsVerify }; return { redirect: "/verify?" + paramsVerify };
@@ -75,7 +76,7 @@ export function checkEmailVerification(
session: Session, session: Session,
humanUser?: HumanUser, humanUser?: HumanUser,
organization?: string, organization?: string,
authRequestId?: string, requestId?: string,
) { ) {
if ( if (
!humanUser?.email?.isVerified && !humanUser?.email?.isVerified &&
@@ -85,8 +86,8 @@ export function checkEmailVerification(
loginName: session.factors?.user?.loginName as string, loginName: session.factors?.user?.loginName as string,
}); });
if (authRequestId) { if (requestId) {
params.append("authRequestId", authRequestId); params.append("requestId", requestId);
} }
if (organization || session.factors?.user?.organizationId) { if (organization || session.factors?.user?.organizationId) {
@@ -100,12 +101,13 @@ export function checkEmailVerification(
} }
} }
export function checkMFAFactors( export async function checkMFAFactors(
serviceUrl: string,
session: Session, session: Session,
loginSettings: LoginSettings | undefined, loginSettings: LoginSettings | undefined,
authMethods: AuthenticationMethodType[], authMethods: AuthenticationMethodType[],
organization?: string, organization?: string,
authRequestId?: string, requestId?: string,
) { ) {
const availableMultiFactors = authMethods?.filter( const availableMultiFactors = authMethods?.filter(
(m: AuthenticationMethodType) => (m: AuthenticationMethodType) =>
@@ -128,8 +130,8 @@ export function checkMFAFactors(
loginName: session.factors?.user?.loginName as string, loginName: session.factors?.user?.loginName as string,
}); });
if (authRequestId) { if (requestId) {
params.append("authRequestId", authRequestId); params.append("requestId", requestId);
} }
if (organization || session.factors?.user?.organizationId) { if (organization || session.factors?.user?.organizationId) {
@@ -155,8 +157,8 @@ export function checkMFAFactors(
loginName: session.factors?.user?.loginName as string, loginName: session.factors?.user?.loginName as string,
}); });
if (authRequestId) { if (requestId) {
params.append("authRequestId", authRequestId); params.append("requestId", requestId);
} }
if (organization || session.factors?.user?.organizationId) { 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 checkAfter: "true", // this defines if the check is directly made after the setup
}); });
if (authRequestId) { if (requestId) {
params.append("authRequestId", authRequestId); 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) { if (organization || session.factors?.user?.organizationId) {
@@ -191,28 +247,4 @@ export function checkMFAFactors(
// TODO: provide a way to setup passkeys on mfa page? // TODO: provide a way to setup passkeys on mfa page?
return { redirect: `/mfa/set?` + params }; 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"; } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
import { OrganizationService } from "@zitadel/proto/zitadel/org/v2/org_service_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 { RequestChallenges } from "@zitadel/proto/zitadel/session/v2/challenge_pb";
import { import {
Checks, Checks,
@@ -55,11 +59,9 @@ async function cacheWrapper<T>(callback: Promise<T>) {
export async function getBrandingSettings({ export async function getBrandingSettings({
serviceUrl, serviceUrl,
organization, organization,
}: { }: {
serviceUrl: string; serviceUrl: string;
organization?: string; organization?: string;
}) { }) {
const settingsService: Client<typeof SettingsService> = const settingsService: Client<typeof SettingsService> =
@@ -74,11 +76,9 @@ export async function getBrandingSettings({
export async function getLoginSettings({ export async function getLoginSettings({
serviceUrl, serviceUrl,
organization, organization,
}: { }: {
serviceUrl: string; serviceUrl: string;
organization?: string; organization?: string;
}) { }) {
const settingsService: Client<typeof SettingsService> = const settingsService: Client<typeof SettingsService> =
@@ -93,11 +93,9 @@ export async function getLoginSettings({
export async function getLockoutSettings({ export async function getLockoutSettings({
serviceUrl, serviceUrl,
orgId, orgId,
}: { }: {
serviceUrl: string; serviceUrl: string;
orgId?: string; orgId?: string;
}) { }) {
const settingsService: Client<typeof SettingsService> = const settingsService: Client<typeof SettingsService> =
@@ -112,11 +110,9 @@ export async function getLockoutSettings({
export async function getPasswordExpirySettings({ export async function getPasswordExpirySettings({
serviceUrl, serviceUrl,
orgId, orgId,
}: { }: {
serviceUrl: string; serviceUrl: string;
orgId?: string; orgId?: string;
}) { }) {
const settingsService: Client<typeof SettingsService> = const settingsService: Client<typeof SettingsService> =
@@ -131,11 +127,9 @@ export async function getPasswordExpirySettings({
export async function listIDPLinks({ export async function listIDPLinks({
serviceUrl, serviceUrl,
userId, userId,
}: { }: {
serviceUrl: string; serviceUrl: string;
userId: string; userId: string;
}) { }) {
const userService: Client<typeof UserService> = await createServiceForHost( const userService: Client<typeof UserService> = await createServiceForHost(
@@ -148,11 +142,9 @@ export async function listIDPLinks({
export async function addOTPEmail({ export async function addOTPEmail({
serviceUrl, serviceUrl,
userId, userId,
}: { }: {
serviceUrl: string; serviceUrl: string;
userId: string; userId: string;
}) { }) {
const userService: Client<typeof UserService> = await createServiceForHost( const userService: Client<typeof UserService> = await createServiceForHost(
@@ -165,11 +157,9 @@ export async function addOTPEmail({
export async function addOTPSMS({ export async function addOTPSMS({
serviceUrl, serviceUrl,
userId, userId,
}: { }: {
serviceUrl: string; serviceUrl: string;
userId: string; userId: string;
}) { }) {
const userService: Client<typeof UserService> = await createServiceForHost( const userService: Client<typeof UserService> = await createServiceForHost(
@@ -182,11 +172,9 @@ export async function addOTPSMS({
export async function registerTOTP({ export async function registerTOTP({
serviceUrl, serviceUrl,
userId, userId,
}: { }: {
serviceUrl: string; serviceUrl: string;
userId: string; userId: string;
}) { }) {
const userService: Client<typeof UserService> = await createServiceForHost( const userService: Client<typeof UserService> = await createServiceForHost(
@@ -214,11 +202,9 @@ export async function getGeneralSettings({
export async function getLegalAndSupportSettings({ export async function getLegalAndSupportSettings({
serviceUrl, serviceUrl,
organization, organization,
}: { }: {
serviceUrl: string; serviceUrl: string;
organization?: string; organization?: string;
}) { }) {
const settingsService: Client<typeof SettingsService> = const settingsService: Client<typeof SettingsService> =
@@ -233,11 +219,9 @@ export async function getLegalAndSupportSettings({
export async function getPasswordComplexitySettings({ export async function getPasswordComplexitySettings({
serviceUrl, serviceUrl,
organization, organization,
}: { }: {
serviceUrl: string; serviceUrl: string;
organization?: string; organization?: string;
}) { }) {
const settingsService: Client<typeof SettingsService> = const settingsService: Client<typeof SettingsService> =
@@ -252,32 +236,26 @@ export async function getPasswordComplexitySettings({
export async function createSessionFromChecks({ export async function createSessionFromChecks({
serviceUrl, serviceUrl,
checks, checks,
challenges,
lifetime, lifetime,
}: { }: {
serviceUrl: string; serviceUrl: string;
checks: Checks; checks: Checks;
challenges: RequestChallenges | undefined;
lifetime?: Duration; lifetime?: Duration;
}) { }) {
const sessionService: Client<typeof SessionService> = const sessionService: Client<typeof SessionService> =
await createServiceForHost(SessionService, serviceUrl); await createServiceForHost(SessionService, serviceUrl);
return sessionService.createSession({ checks, challenges, lifetime }, {}); return sessionService.createSession({ checks, lifetime }, {});
} }
export async function createSessionForUserIdAndIdpIntent({ export async function createSessionForUserIdAndIdpIntent({
serviceUrl, serviceUrl,
userId, userId,
idpIntent, idpIntent,
lifetime, lifetime,
}: { }: {
serviceUrl: string; serviceUrl: string;
userId: string; userId: string;
idpIntent: { idpIntent: {
idpIntentId?: string | undefined; idpIntentId?: string | undefined;
@@ -304,7 +282,6 @@ export async function createSessionForUserIdAndIdpIntent({
export async function setSession({ export async function setSession({
serviceUrl, serviceUrl,
sessionId, sessionId,
sessionToken, sessionToken,
challenges, challenges,
@@ -312,7 +289,6 @@ export async function setSession({
lifetime, lifetime,
}: { }: {
serviceUrl: string; serviceUrl: string;
sessionId: string; sessionId: string;
sessionToken: string; sessionToken: string;
challenges: RequestChallenges | undefined; challenges: RequestChallenges | undefined;
@@ -337,12 +313,10 @@ export async function setSession({
export async function getSession({ export async function getSession({
serviceUrl, serviceUrl,
sessionId, sessionId,
sessionToken, sessionToken,
}: { }: {
serviceUrl: string; serviceUrl: string;
sessionId: string; sessionId: string;
sessionToken: string; sessionToken: string;
}) { }) {
@@ -354,12 +328,10 @@ export async function getSession({
export async function deleteSession({ export async function deleteSession({
serviceUrl, serviceUrl,
sessionId, sessionId,
sessionToken, sessionToken,
}: { }: {
serviceUrl: string; serviceUrl: string;
sessionId: string; sessionId: string;
sessionToken: string; sessionToken: string;
}) { }) {
@@ -371,7 +343,6 @@ export async function deleteSession({
type ListSessionsCommand = { type ListSessionsCommand = {
serviceUrl: string; serviceUrl: string;
ids: string[]; ids: string[];
}; };
@@ -400,7 +371,6 @@ export async function listSessions({
export type AddHumanUserData = { export type AddHumanUserData = {
serviceUrl: string; serviceUrl: string;
firstName: string; firstName: string;
lastName: string; lastName: string;
email: string; email: string;
@@ -410,7 +380,6 @@ export type AddHumanUserData = {
export async function addHumanUser({ export async function addHumanUser({
serviceUrl, serviceUrl,
email, email,
firstName, firstName,
lastName, lastName,
@@ -443,11 +412,9 @@ export async function addHumanUser({
export async function addHuman({ export async function addHuman({
serviceUrl, serviceUrl,
request, request,
}: { }: {
serviceUrl: string; serviceUrl: string;
request: AddHumanUserRequest; request: AddHumanUserRequest;
}) { }) {
const userService: Client<typeof UserService> = await createServiceForHost( const userService: Client<typeof UserService> = await createServiceForHost(
@@ -460,12 +427,10 @@ export async function addHuman({
export async function verifyTOTPRegistration({ export async function verifyTOTPRegistration({
serviceUrl, serviceUrl,
code, code,
userId, userId,
}: { }: {
serviceUrl: string; serviceUrl: string;
code: string; code: string;
userId: string; userId: string;
}) { }) {
@@ -479,11 +444,9 @@ export async function verifyTOTPRegistration({
export async function getUserByID({ export async function getUserByID({
serviceUrl, serviceUrl,
userId, userId,
}: { }: {
serviceUrl: string; serviceUrl: string;
userId: string; userId: string;
}) { }) {
const userService: Client<typeof UserService> = await createServiceForHost( const userService: Client<typeof UserService> = await createServiceForHost(
@@ -494,14 +457,27 @@ export async function getUserByID({
return userService.getUserByID({ userId }, {}); 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({ export async function verifyInviteCode({
serviceUrl, serviceUrl,
userId, userId,
verificationCode, verificationCode,
}: { }: {
serviceUrl: string; serviceUrl: string;
userId: string; userId: string;
verificationCode: string; verificationCode: string;
}) { }) {
@@ -515,11 +491,9 @@ export async function verifyInviteCode({
export async function resendInviteCode({ export async function resendInviteCode({
serviceUrl, serviceUrl,
userId, userId,
}: { }: {
serviceUrl: string; serviceUrl: string;
userId: string; userId: string;
}) { }) {
const userService: Client<typeof UserService> = await createServiceForHost( const userService: Client<typeof UserService> = await createServiceForHost(
@@ -532,12 +506,10 @@ export async function resendInviteCode({
export async function sendEmailCode({ export async function sendEmailCode({
serviceUrl, serviceUrl,
userId, userId,
urlTemplate, urlTemplate,
}: { }: {
serviceUrl: string; serviceUrl: string;
userId: string; userId: string;
urlTemplate: string; urlTemplate: string;
}) { }) {
@@ -563,12 +535,10 @@ export async function sendEmailCode({
export async function createInviteCode({ export async function createInviteCode({
serviceUrl, serviceUrl,
urlTemplate, urlTemplate,
userId, userId,
}: { }: {
serviceUrl: string; serviceUrl: string;
urlTemplate: string; urlTemplate: string;
userId: string; userId: string;
}) { }) {
@@ -600,7 +570,6 @@ export async function createInviteCode({
export type ListUsersCommand = { export type ListUsersCommand = {
serviceUrl: string; serviceUrl: string;
loginName?: string; loginName?: string;
userName?: string; userName?: string;
email?: string; email?: string;
@@ -610,7 +579,6 @@ export type ListUsersCommand = {
export async function listUsers({ export async function listUsers({
serviceUrl, serviceUrl,
loginName, loginName,
userName, userName,
phone, phone,
@@ -709,7 +677,6 @@ export async function listUsers({
export type SearchUsersCommand = { export type SearchUsersCommand = {
serviceUrl: string; serviceUrl: string;
searchValue: string; searchValue: string;
loginSettings: LoginSettings; loginSettings: LoginSettings;
organizationId?: string; organizationId?: string;
@@ -755,7 +722,6 @@ const EmailQuery = (searchValue: string) =>
* */ * */
export async function searchUsers({ export async function searchUsers({
serviceUrl, serviceUrl,
searchValue, searchValue,
loginSettings, loginSettings,
organizationId, organizationId,
@@ -900,11 +866,9 @@ export async function getDefaultOrg({
export async function getOrgsByDomain({ export async function getOrgsByDomain({
serviceUrl, serviceUrl,
domain, domain,
}: { }: {
serviceUrl: string; serviceUrl: string;
domain: string; domain: string;
}) { }) {
const orgService: Client<typeof OrganizationService> = const orgService: Client<typeof OrganizationService> =
@@ -927,7 +891,6 @@ export async function getOrgsByDomain({
export async function startIdentityProviderFlow({ export async function startIdentityProviderFlow({
serviceUrl, serviceUrl,
idpId, idpId,
urls, urls,
}: { }: {
@@ -952,7 +915,6 @@ export async function startIdentityProviderFlow({
export async function retrieveIdentityProviderInformation({ export async function retrieveIdentityProviderInformation({
serviceUrl, serviceUrl,
idpIntentId, idpIntentId,
idpIntentToken, idpIntentToken,
}: { }: {
@@ -974,11 +936,9 @@ export async function retrieveIdentityProviderInformation({
export async function getAuthRequest({ export async function getAuthRequest({
serviceUrl, serviceUrl,
authRequestId, authRequestId,
}: { }: {
serviceUrl: string; serviceUrl: string;
authRequestId: string; authRequestId: string;
}) { }) {
const oidcService = await createServiceForHost(OIDCService, serviceUrl); const oidcService = await createServiceForHost(OIDCService, serviceUrl);
@@ -990,11 +950,9 @@ export async function getAuthRequest({
export async function createCallback({ export async function createCallback({
serviceUrl, serviceUrl,
req, req,
}: { }: {
serviceUrl: string; serviceUrl: string;
req: CreateCallbackRequest; req: CreateCallbackRequest;
}) { }) {
const oidcService = await createServiceForHost(OIDCService, serviceUrl); const oidcService = await createServiceForHost(OIDCService, serviceUrl);
@@ -1002,14 +960,38 @@ export async function createCallback({
return oidcService.createCallback(req); 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({ export async function verifyEmail({
serviceUrl, serviceUrl,
userId, userId,
verificationCode, verificationCode,
}: { }: {
serviceUrl: string; serviceUrl: string;
userId: string; userId: string;
verificationCode: string; verificationCode: string;
}) { }) {
@@ -1029,12 +1011,10 @@ export async function verifyEmail({
export async function resendEmailCode({ export async function resendEmailCode({
serviceUrl, serviceUrl,
userId, userId,
urlTemplate, urlTemplate,
}: { }: {
serviceUrl: string; serviceUrl: string;
userId: string; userId: string;
urlTemplate: string; urlTemplate: string;
}) { }) {
@@ -1058,12 +1038,10 @@ export async function resendEmailCode({
export async function retrieveIDPIntent({ export async function retrieveIDPIntent({
serviceUrl, serviceUrl,
id, id,
token, token,
}: { }: {
serviceUrl: string; serviceUrl: string;
id: string; id: string;
token: string; token: string;
}) { }) {
@@ -1080,11 +1058,9 @@ export async function retrieveIDPIntent({
export async function getIDPByID({ export async function getIDPByID({
serviceUrl, serviceUrl,
id, id,
}: { }: {
serviceUrl: string; serviceUrl: string;
id: string; id: string;
}) { }) {
const idpService: Client<typeof IdentityProviderService> = const idpService: Client<typeof IdentityProviderService> =
@@ -1095,12 +1071,10 @@ export async function getIDPByID({
export async function addIDPLink({ export async function addIDPLink({
serviceUrl, serviceUrl,
idp, idp,
userId, userId,
}: { }: {
serviceUrl: string; serviceUrl: string;
idp: { id: string; userId: string; userName: string }; idp: { id: string; userId: string; userName: string };
userId: string; userId: string;
}) { }) {
@@ -1124,12 +1098,10 @@ export async function addIDPLink({
export async function passwordReset({ export async function passwordReset({
serviceUrl, serviceUrl,
userId, userId,
urlTemplate, urlTemplate,
}: { }: {
serviceUrl: string; serviceUrl: string;
userId: string; userId: string;
urlTemplate?: string; urlTemplate?: string;
}) { }) {
@@ -1161,14 +1133,12 @@ export async function passwordReset({
export async function setUserPassword({ export async function setUserPassword({
serviceUrl, serviceUrl,
userId, userId,
password, password,
user, user,
code, code,
}: { }: {
serviceUrl: string; serviceUrl: string;
userId: string; userId: string;
password: string; password: string;
user: User; user: User;
@@ -1224,11 +1194,9 @@ export async function setUserPassword({
export async function setPassword({ export async function setPassword({
serviceUrl, serviceUrl,
payload, payload,
}: { }: {
serviceUrl: string; serviceUrl: string;
payload: SetPasswordRequest; payload: SetPasswordRequest;
}) { }) {
const userService: Client<typeof UserService> = await createServiceForHost( const userService: Client<typeof UserService> = await createServiceForHost(
@@ -1247,11 +1215,9 @@ export async function setPassword({
*/ */
export async function createPasskeyRegistrationLink({ export async function createPasskeyRegistrationLink({
serviceUrl, serviceUrl,
userId, userId,
}: { }: {
serviceUrl: string; serviceUrl: string;
userId: string; userId: string;
}) { }) {
const userService: Client<typeof UserService> = await createServiceForHost( const userService: Client<typeof UserService> = await createServiceForHost(
@@ -1277,12 +1243,10 @@ export async function createPasskeyRegistrationLink({
*/ */
export async function registerU2F({ export async function registerU2F({
serviceUrl, serviceUrl,
userId, userId,
domain, domain,
}: { }: {
serviceUrl: string; serviceUrl: string;
userId: string; userId: string;
domain: string; domain: string;
}) { }) {
@@ -1305,11 +1269,9 @@ export async function registerU2F({
*/ */
export async function verifyU2FRegistration({ export async function verifyU2FRegistration({
serviceUrl, serviceUrl,
request, request,
}: { }: {
serviceUrl: string; serviceUrl: string;
request: VerifyU2FRegistrationRequest; request: VerifyU2FRegistrationRequest;
}) { }) {
const userService: Client<typeof UserService> = await createServiceForHost( const userService: Client<typeof UserService> = await createServiceForHost(
@@ -1329,12 +1291,10 @@ export async function verifyU2FRegistration({
*/ */
export async function getActiveIdentityProviders({ export async function getActiveIdentityProviders({
serviceUrl, serviceUrl,
orgId, orgId,
linking_allowed, linking_allowed,
}: { }: {
serviceUrl: string; serviceUrl: string;
orgId?: string; orgId?: string;
linking_allowed?: boolean; linking_allowed?: boolean;
}) { }) {
@@ -1356,11 +1316,9 @@ export async function getActiveIdentityProviders({
*/ */
export async function verifyPasskeyRegistration({ export async function verifyPasskeyRegistration({
serviceUrl, serviceUrl,
request, request,
}: { }: {
serviceUrl: string; serviceUrl: string;
request: VerifyPasskeyRegistrationRequest; request: VerifyPasskeyRegistrationRequest;
}) { }) {
const userService: Client<typeof UserService> = await createServiceForHost( const userService: Client<typeof UserService> = await createServiceForHost(
@@ -1381,13 +1339,11 @@ export async function verifyPasskeyRegistration({
*/ */
export async function registerPasskey({ export async function registerPasskey({
serviceUrl, serviceUrl,
userId, userId,
code, code,
domain, domain,
}: { }: {
serviceUrl: string; serviceUrl: string;
userId: string; userId: string;
code: { id: string; code: string }; code: { id: string; code: string };
domain: string; domain: string;
@@ -1415,7 +1371,6 @@ export async function listAuthenticationMethodTypes({
userId, userId,
}: { }: {
serviceUrl: string; serviceUrl: string;
userId: string; userId: string;
}) { }) {
const userService: Client<typeof UserService> = await createServiceForHost( 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 { RequestContextSchema } from "@zitadel/proto/zitadel/object/v2/object_pb.js";
import { OIDCService } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_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 { 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 { SessionService } from "@zitadel/proto/zitadel/session/v2/session_service_pb.js";
import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_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"; 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 createSettingsServiceClient = createClientFor(SettingsService);
export const createSessionServiceClient = createClientFor(SessionService); export const createSessionServiceClient = createClientFor(SessionService);
export const createOIDCServiceClient = createClientFor(OIDCService); export const createOIDCServiceClient = createClientFor(OIDCService);
export const createSAMLServiceClient = createClientFor(SAMLService);
export const createOrganizationServiceClient = createClientFor(OrganizationService); export const createOrganizationServiceClient = createClientFor(OrganizationService);
export const createFeatureServiceClient = createClientFor(FeatureService); export const createFeatureServiceClient = createClientFor(FeatureService);
export const createIdpServiceClient = createClientFor(IdentityProviderService); export const createIdpServiceClient = createClientFor(IdentityProviderService);