diff --git a/acceptance/zitadel.yaml b/acceptance/zitadel.yaml index 5a17264eb6..0678e8ff86 100644 --- a/acceptance/zitadel.yaml +++ b/acceptance/zitadel.yaml @@ -16,6 +16,26 @@ FirstInstance: ExpirationDate: 2099-01-01T00:00:00Z DefaultInstance: + LoginPolicy: + AllowUsernamePassword: true # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_ALLOWUSERNAMEPASSWORD + AllowRegister: true # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_ALLOWREGISTER + AllowExternalIDP: true # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_ALLOWEXTERNALIDP + ForceMFA: false # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_FORCEMFA + HidePasswordReset: false # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_HIDEPASSWORDRESET + IgnoreUnknownUsernames: false # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_IGNOREUNKNOWNUSERNAMES + AllowDomainDiscovery: true # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_ALLOWDOMAINDISCOVERY + # 1 is allowed, 0 is not allowed + PasswordlessType: 1 # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_PASSWORDLESSTYPE + # DefaultRedirectURL is empty by default because we use the Console UI + DefaultRedirectURI: # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_DEFAULTREDIRECTURI + # 240h = 10d + PasswordCheckLifetime: 240h # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_PASSWORDCHECKLIFETIME + # 240h = 10d + ExternalLoginCheckLifetime: 240h # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_EXTERNALLOGINCHECKLIFETIME + # 720h = 30d + MfaInitSkipLifetime: 0h # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_MFAINITSKIPLIFETIME + SecondFactorCheckLifetime: 18h # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_SECONDFACTORCHECKLIFETIME + MultiFactorCheckLifetime: 12h # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_MULTIFACTORCHECKLIFETIME PrivacyPolicy: TOSLink: "https://zitadel.com/docs/legal/terms-of-service" PrivacyLink: "https://zitadel.com/docs/legal/policies/privacy-policy" diff --git a/apps/login/locales/de.json b/apps/login/locales/de.json index 5e8756c89b..db46321b05 100644 --- a/apps/login/locales/de.json +++ b/apps/login/locales/de.json @@ -71,7 +71,8 @@ }, "set": { "title": "2-Faktor einrichten", - "description": "Wählen Sie einen der folgenden zweiten Faktoren." + "description": "Wählen Sie einen der folgenden zweiten Faktoren.", + "skip": "Überspringen" } }, "otp": { diff --git a/apps/login/locales/en.json b/apps/login/locales/en.json index 3101f222d5..36776ccbd9 100644 --- a/apps/login/locales/en.json +++ b/apps/login/locales/en.json @@ -71,7 +71,8 @@ }, "set": { "title": "Set up 2-Factor", - "description": "Choose one of the following second factors." + "description": "Choose one of the following second factors.", + "skip": "Skip" } }, "otp": { diff --git a/apps/login/locales/es.json b/apps/login/locales/es.json index 5a9b6f6324..4eba3a9696 100644 --- a/apps/login/locales/es.json +++ b/apps/login/locales/es.json @@ -71,7 +71,8 @@ }, "set": { "title": "Configurar autenticación de 2 factores", - "description": "Elige uno de los siguientes factores secundarios." + "description": "Elige uno de los siguientes factores secundarios.", + "skip": "Omitir" } }, "otp": { diff --git a/apps/login/locales/it.json b/apps/login/locales/it.json index 1423c43cfe..d0969c86b3 100644 --- a/apps/login/locales/it.json +++ b/apps/login/locales/it.json @@ -71,7 +71,8 @@ }, "set": { "title": "Configura l'autenticazione a 2 fattori", - "description": "Scegli uno dei seguenti secondi fattori." + "description": "Scegli uno dei seguenti secondi fattori.", + "skip": "Salta" } }, "otp": { diff --git a/apps/login/locales/zh.json b/apps/login/locales/zh.json index acd03cc5b6..9c87a53a65 100644 --- a/apps/login/locales/zh.json +++ b/apps/login/locales/zh.json @@ -71,7 +71,8 @@ }, "set": { "title": "设置双因素认证", - "description": "选择以下的一个第二因素。" + "description": "选择以下的一个第二因素。", + "skip": "跳过" } }, "otp": { diff --git a/apps/login/readme.md b/apps/login/readme.md index 4df81f9f9d..120fad3cd7 100644 --- a/apps/login/readme.md +++ b/apps/login/readme.md @@ -389,7 +389,6 @@ In future, self service options to jump to are shown below, like: ## Currently NOT Supported -- Login Settings: multifactor init prompt - forceMFA on login settings is not checked for IDPs Also note that IDP logins are considered as valid MFA. An additional MFA check will be implemented in future if enforced. diff --git a/apps/login/src/app/(login)/accounts/page.tsx b/apps/login/src/app/(login)/accounts/page.tsx index 76ce1fe9ad..1823310e97 100644 --- a/apps/login/src/app/(login)/accounts/page.tsx +++ b/apps/login/src/app/(login)/accounts/page.tsx @@ -36,7 +36,7 @@ export default async function Page(props: { const locale = getLocale(); const t = await getTranslations({ locale, namespace: "accounts" }); - const authRequestId = searchParams?.authRequestId; + const requestId = searchParams?.requestId; const organization = searchParams?.organization; const _headers = await headers(); @@ -62,8 +62,8 @@ export default async function Page(props: { const params = new URLSearchParams(); - if (authRequestId) { - params.append("authRequestId", authRequestId); + if (requestId) { + params.append("requestId", requestId); } if (organization) { @@ -77,7 +77,7 @@ export default async function Page(props: {

{t("description")}

- +
diff --git a/apps/login/src/app/(login)/authenticator/set/page.tsx b/apps/login/src/app/(login)/authenticator/set/page.tsx index 55a7eb6946..19bf0883e1 100644 --- a/apps/login/src/app/(login)/authenticator/set/page.tsx +++ b/apps/login/src/app/(login)/authenticator/set/page.tsx @@ -27,7 +27,7 @@ export default async function Page(props: { const t = await getTranslations({ locale, namespace: "authenticator" }); const tError = await getTranslations({ locale, namespace: "error" }); - const { loginName, authRequestId, organization, sessionId } = searchParams; + const { loginName, requestId, organization, sessionId } = searchParams; const _headers = await headers(); const { serviceUrl } = getServiceUrlFromHeaders(_headers); @@ -141,8 +141,8 @@ export default async function Page(props: { params.set("organization", sessionWithData.factors?.user?.organizationId); } - if (authRequestId) { - params.set("authRequestId", authRequestId); + if (requestId) { + params.set("requestId", requestId); } return ( @@ -174,7 +174,7 @@ export default async function Page(props: { {loginSettings?.allowExternalIdp && identityProviders && ( diff --git a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx index 0d67b446c4..4ef0b50933 100644 --- a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx +++ b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx @@ -36,7 +36,7 @@ export default async function Page(props: { const searchParams = await props.searchParams; const locale = getLocale(); const t = await getTranslations({ locale, namespace: "idp" }); - const { id, token, authRequestId, organization, link } = searchParams; + const { id, token, requestId, organization, link } = searchParams; const { provider } = params; const _headers = await headers(); @@ -68,7 +68,7 @@ export default async function Page(props: { return loginSuccess( userId, { idpIntentId: id, idpIntentToken: token }, - authRequestId, + requestId, branding, ); } @@ -119,7 +119,7 @@ export default async function Page(props: { return linkingSuccess( userId, { idpIntentId: id, idpIntentToken: token }, - authRequestId, + requestId, branding, ); } @@ -177,7 +177,7 @@ export default async function Page(props: { return linkingSuccess( foundUser.userId, { idpIntentId: id, idpIntentToken: token }, - authRequestId, + requestId, branding, ); } @@ -243,7 +243,7 @@ export default async function Page(props: {
diff --git a/apps/login/src/app/(login)/idp/page.tsx b/apps/login/src/app/(login)/idp/page.tsx index 6cbe5291be..67b3ac1cf7 100644 --- a/apps/login/src/app/(login)/idp/page.tsx +++ b/apps/login/src/app/(login)/idp/page.tsx @@ -12,7 +12,7 @@ export default async function Page(props: { const locale = getLocale(); const t = await getTranslations({ locale, namespace: "idp" }); - const authRequestId = searchParams?.authRequestId; + const requestId = searchParams?.requestId; const organization = searchParams?.organization; const _headers = await headers(); @@ -41,7 +41,7 @@ export default async function Page(props: { {identityProviders && ( )} diff --git a/apps/login/src/app/(login)/loginname/page.tsx b/apps/login/src/app/(login)/loginname/page.tsx index e39bd3cddb..85327c7a85 100644 --- a/apps/login/src/app/(login)/loginname/page.tsx +++ b/apps/login/src/app/(login)/loginname/page.tsx @@ -20,7 +20,7 @@ export default async function Page(props: { const t = await getTranslations({ locale, namespace: "loginname" }); const loginName = searchParams?.loginName; - const authRequestId = searchParams?.authRequestId; + const requestId = searchParams?.requestId; const organization = searchParams?.organization; const suffix = searchParams?.suffix; const submit: boolean = searchParams?.submit === "true"; @@ -72,7 +72,7 @@ export default async function Page(props: { )} diff --git a/apps/login/src/app/(login)/mfa/page.tsx b/apps/login/src/app/(login)/mfa/page.tsx index 2152c5e069..47853a2b3f 100644 --- a/apps/login/src/app/(login)/mfa/page.tsx +++ b/apps/login/src/app/(login)/mfa/page.tsx @@ -22,7 +22,7 @@ export default async function Page(props: { const t = await getTranslations({ locale, namespace: "mfa" }); const tError = await getTranslations({ locale, namespace: "error" }); - const { loginName, authRequestId, organization, sessionId } = searchParams; + const { loginName, requestId, organization, sessionId } = searchParams; const _headers = await headers(); const { serviceUrl } = getServiceUrlFromHeaders(_headers); @@ -114,7 +114,7 @@ export default async function Page(props: { diff --git a/apps/login/src/app/(login)/mfa/set/page.tsx b/apps/login/src/app/(login)/mfa/set/page.tsx index 04558689c8..498b063428 100644 --- a/apps/login/src/app/(login)/mfa/set/page.tsx +++ b/apps/login/src/app/(login)/mfa/set/page.tsx @@ -42,23 +42,17 @@ export default async function Page(props: { const t = await getTranslations({ locale, namespace: "mfa" }); const tError = await getTranslations({ locale, namespace: "error" }); - const { - loginName, - checkAfter, - force, - authRequestId, - organization, - sessionId, - } = searchParams; + const { loginName, checkAfter, force, requestId, organization, sessionId } = + searchParams; const _headers = await headers(); const { serviceUrl } = getServiceUrlFromHeaders(_headers); const sessionWithData = sessionId - ? await loadSessionById(serviceUrl, sessionId, organization) - : await loadSessionByLoginname(serviceUrl, loginName, organization); + ? await loadSessionById(sessionId, organization) + : await loadSessionByLoginname(loginName, organization); - async function getAuthMethodsAndUser(host: string, session?: Session) { + async function getAuthMethodsAndUser(session?: Session) { const userId = session?.factors?.user?.id; if (!userId) { @@ -86,7 +80,6 @@ export default async function Page(props: { } async function loadSessionByLoginname( - host: string, loginName?: string, organization?: string, ) { @@ -98,23 +91,18 @@ export default async function Page(props: { organization, }, }).then((session) => { - return getAuthMethodsAndUser(serviceUrl, session); + return getAuthMethodsAndUser(session); }); } - async function loadSessionById( - host: string, - sessionId: string, - organization?: string, - ) { + async function loadSessionById(sessionId: string, organization?: string) { const recent = await getSessionCookieById({ sessionId, organization }); return getSession({ serviceUrl, - sessionId: recent.id, sessionToken: recent.token, }).then((sessionResponse) => { - return getAuthMethodsAndUser(serviceUrl, sessionResponse.session); + return getAuthMethodsAndUser(sessionResponse.session); }); } @@ -153,17 +141,20 @@ export default async function Page(props: { {isSessionValid(sessionWithData).valid && loginSettings && - sessionWithData && ( + sessionWithData && + sessionWithData.factors?.user?.id && ( )} diff --git a/apps/login/src/app/(login)/otp/[method]/page.tsx b/apps/login/src/app/(login)/otp/[method]/page.tsx index c938167ba1..51881e1bf7 100644 --- a/apps/login/src/app/(login)/otp/[method]/page.tsx +++ b/apps/login/src/app/(login)/otp/[method]/page.tsx @@ -34,7 +34,7 @@ export default async function Page(props: { const { loginName, // send from password page userId, // send from email link - authRequestId, + requestId, sessionId, organization, code, @@ -115,7 +115,7 @@ export default async function Page(props: { diff --git a/apps/login/src/app/(login)/passkey/set/page.tsx b/apps/login/src/app/(login)/passkey/set/page.tsx index 4617bc756f..64e3cf2a8e 100644 --- a/apps/login/src/app/(login)/passkey/set/page.tsx +++ b/apps/login/src/app/(login)/passkey/set/page.tsx @@ -16,8 +16,7 @@ export default async function Page(props: { const t = await getTranslations({ locale, namespace: "passkey" }); const tError = await getTranslations({ locale, namespace: "error" }); - const { loginName, prompt, organization, authRequestId, userId } = - searchParams; + const { loginName, prompt, organization, requestId, userId } = searchParams; const _headers = await headers(); const { serviceUrl } = getServiceUrlFromHeaders(_headers); @@ -76,7 +75,7 @@ export default async function Page(props: { sessionId={session.id} isPrompt={!!prompt} organization={organization} - authRequestId={authRequestId} + requestId={requestId} /> )}
diff --git a/apps/login/src/app/(login)/password/change/page.tsx b/apps/login/src/app/(login)/password/change/page.tsx index 16aa4d1058..af55c524e4 100644 --- a/apps/login/src/app/(login)/password/change/page.tsx +++ b/apps/login/src/app/(login)/password/change/page.tsx @@ -23,7 +23,7 @@ export default async function Page(props: { const t = await getTranslations({ locale, namespace: "password" }); const tError = await getTranslations({ locale, namespace: "error" }); - const { loginName, organization, authRequestId } = searchParams; + const { loginName, organization, requestId } = searchParams; // also allow no session to be found (ignoreUnkownUsername) const sessionFactors = await loadMostRecentSession({ @@ -84,7 +84,7 @@ export default async function Page(props: { diff --git a/apps/login/src/app/(login)/password/page.tsx b/apps/login/src/app/(login)/password/page.tsx index a230424b25..e1190911b2 100644 --- a/apps/login/src/app/(login)/password/page.tsx +++ b/apps/login/src/app/(login)/password/page.tsx @@ -22,7 +22,7 @@ export default async function Page(props: { const t = await getTranslations({ locale, namespace: "password" }); const tError = await getTranslations({ locale, namespace: "error" }); - let { loginName, organization, authRequestId, alt } = searchParams; + let { loginName, organization, requestId, alt } = searchParams; const _headers = await headers(); const { serviceUrl } = getServiceUrlFromHeaders(_headers); @@ -93,7 +93,7 @@ export default async function Page(props: { {loginName && ( )} diff --git a/apps/login/src/app/(login)/register/password/page.tsx b/apps/login/src/app/(login)/register/password/page.tsx index 47bb651c3b..20a76363e6 100644 --- a/apps/login/src/app/(login)/register/password/page.tsx +++ b/apps/login/src/app/(login)/register/password/page.tsx @@ -19,8 +19,7 @@ export default async function Page(props: { const locale = getLocale(); const t = await getTranslations({ locale, namespace: "register" }); - let { firstname, lastname, email, organization, authRequestId } = - searchParams; + let { firstname, lastname, email, organization, requestId } = searchParams; const _headers = await headers(); const { serviceUrl } = getServiceUrlFromHeaders(_headers); @@ -79,7 +78,7 @@ export default async function Page(props: { firstname={firstname} lastname={lastname} organization={organization} - authRequestId={authRequestId} + requestId={requestId} > )}
diff --git a/apps/login/src/app/(login)/signedin/page.tsx b/apps/login/src/app/(login)/signedin/page.tsx index b64e87b56b..c6b747befa 100644 --- a/apps/login/src/app/(login)/signedin/page.tsx +++ b/apps/login/src/app/(login)/signedin/page.tsx @@ -6,6 +6,7 @@ import { getMostRecentCookieWithLoginname } from "@/lib/cookies"; import { getServiceUrlFromHeaders } from "@/lib/service"; import { createCallback, + createResponse, getBrandingSettings, getLoginSettings, getSession, @@ -15,6 +16,7 @@ import { CreateCallbackRequestSchema, SessionSchema, } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb"; +import { CreateResponseRequestSchema } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb"; import { getLocale, getTranslations } from "next-intl/server"; import { headers } from "next/headers"; import Link from "next/link"; @@ -24,16 +26,16 @@ async function loadSession( serviceUrl: string, loginName: string, - authRequestId?: string, + requestId?: string, ) { const recent = await getMostRecentCookieWithLoginname({ loginName }); - if (authRequestId) { + if (requestId && requestId.startsWith("oidc_")) { return createCallback({ serviceUrl, req: create(CreateCallbackRequestSchema, { - authRequestId, + authRequestId: requestId, callbackKind: { case: "session", value: create(SessionSchema, { @@ -45,7 +47,24 @@ async function loadSession( }).then(({ callbackUrl }) => { return redirect(callbackUrl); }); + } else if (requestId && requestId.startsWith("saml_")) { + return createResponse({ + serviceUrl, + req: create(CreateResponseRequestSchema, { + samlRequestId: requestId.replace("saml_", ""), + responseKind: { + case: "session", + value: { + sessionId: recent.id, + sessionToken: recent.token, + }, + }, + }), + }).then(({ url }) => { + return redirect(url); + }); } + return getSession({ serviceUrl, @@ -66,12 +85,12 @@ export default async function Page(props: { searchParams: Promise }) { const _headers = await headers(); const { serviceUrl } = getServiceUrlFromHeaders(_headers); - const { loginName, authRequestId, organization } = searchParams; + const { loginName, requestId, organization } = searchParams; const sessionFactors = await loadSession( serviceUrl, loginName, - authRequestId, + requestId, ); const branding = await getBrandingSettings({ @@ -81,7 +100,7 @@ export default async function Page(props: { searchParams: Promise }) { }); let loginSettings; - if (!authRequestId) { + if (!requestId) { loginSettings = await getLoginSettings({ serviceUrl, diff --git a/apps/login/src/app/(login)/u2f/page.tsx b/apps/login/src/app/(login)/u2f/page.tsx index 1b8b30af14..139f0f1a50 100644 --- a/apps/login/src/app/(login)/u2f/page.tsx +++ b/apps/login/src/app/(login)/u2f/page.tsx @@ -17,7 +17,7 @@ export default async function Page(props: { const t = await getTranslations({ locale, namespace: "u2f" }); const tError = await getTranslations({ locale, namespace: "error" }); - const { loginName, authRequestId, sessionId, organization } = searchParams; + const { loginName, requestId, sessionId, organization } = searchParams; const _headers = await headers(); const { serviceUrl } = getServiceUrlFromHeaders(_headers); @@ -80,7 +80,7 @@ export default async function Page(props: { )} diff --git a/apps/login/src/app/(login)/verify/page.tsx b/apps/login/src/app/(login)/verify/page.tsx index 91009315cf..e9d2029cbd 100644 --- a/apps/login/src/app/(login)/verify/page.tsx +++ b/apps/login/src/app/(login)/verify/page.tsx @@ -22,7 +22,7 @@ export default async function Page(props: { searchParams: Promise }) { const t = await getTranslations({ locale, namespace: "verify" }); const tError = await getTranslations({ locale, namespace: "error" }); - const { userId, loginName, code, organization, authRequestId, invite } = + const { userId, loginName, code, organization, requestId, invite } = searchParams; const _headers = await headers(); @@ -64,7 +64,7 @@ export default async function Page(props: { searchParams: Promise }) { userId: sessionFactors?.factors?.user?.id, urlTemplate: `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` + - (authRequestId ? `&authRequestId=${authRequestId}` : ""), + (requestId ? `&requestId=${requestId}` : ""), }).catch((error) => { console.error("Could not resend verification email", error); throw Error("Failed to send verification email"); @@ -77,7 +77,7 @@ export default async function Page(props: { searchParams: Promise }) { userId, urlTemplate: `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` + - (authRequestId ? `&authRequestId=${authRequestId}` : ""), + (requestId ? `&requestId=${requestId}` : ""), }).catch((error) => { console.error("Could not resend verification email", error); throw Error("Failed to send verification email"); @@ -120,8 +120,8 @@ export default async function Page(props: { searchParams: Promise }) { params.set("organization", organization); } - if (authRequestId) { - params.set("authRequestId", authRequestId); + if (requestId) { + params.set("requestId", requestId); } return ( @@ -165,7 +165,7 @@ export default async function Page(props: { searchParams: Promise }) { userId={id} loginName={loginName} organization={organization} - authRequestId={authRequestId} + requestId={requestId} authMethods={authMethods} /> ) : ( @@ -176,7 +176,7 @@ export default async function Page(props: { searchParams: Promise }) { userId={id} code={code} isInvite={invite === "true"} - authRequestId={authRequestId} + requestId={requestId} /> ))} diff --git a/apps/login/src/app/login/route.ts b/apps/login/src/app/login/route.ts index 9d95e28d70..dd91519ee4 100644 --- a/apps/login/src/app/login/route.ts +++ b/apps/login/src/app/login/route.ts @@ -1,28 +1,28 @@ import { getAllSessions } from "@/lib/cookies"; import { idpTypeToSlug } from "@/lib/idp"; +import { loginWithOIDCandSession } from "@/lib/oidc"; +import { loginWithSAMLandSession } from "@/lib/saml"; import { sendLoginname, SendLoginnameCommand } from "@/lib/server/loginname"; import { getServiceUrlFromHeaders } from "@/lib/service"; +import { findValidSession } from "@/lib/session"; import { createCallback, + createResponse, getActiveIdentityProviders, getAuthRequest, - getLoginSettings, getOrgsByDomain, - listAuthenticationMethodTypes, + getSAMLRequest, listSessions, startIdentityProviderFlow, } from "@/lib/zitadel"; -import { create, timestampDate } from "@zitadel/client"; -import { - AuthRequest, - Prompt, -} from "@zitadel/proto/zitadel/oidc/v2/authorization_pb"; +import { create } from "@zitadel/client"; +import { Prompt } from "@zitadel/proto/zitadel/oidc/v2/authorization_pb"; import { CreateCallbackRequestSchema, SessionSchema, } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb"; +import { CreateResponseRequestSchema } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; -import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { headers } from "next/headers"; import { NextRequest, NextResponse } from "next/server"; @@ -30,12 +30,32 @@ export const dynamic = "force-dynamic"; export const revalidate = false; export const fetchCache = "default-no-store"; +const gotoAccounts = ({ + request, + requestId, + organization, +}: { + request: NextRequest; + requestId: string; + organization?: string; +}): NextResponse => { + const accountsUrl = new URL("/accounts", request.url); + + if (requestId) { + accountsUrl.searchParams.set("requestId", requestId); + } + if (organization) { + accountsUrl.searchParams.set("organization", organization); + } + + return NextResponse.redirect(accountsUrl); +}; + async function loadSessions({ serviceUrl, ids, }: { serviceUrl: string; - ids: string[]; }): Promise { const response = await listSessions({ @@ -50,175 +70,23 @@ const ORG_SCOPE_REGEX = /urn:zitadel:iam:org:id:([0-9]+)/; const ORG_DOMAIN_SCOPE_REGEX = /urn:zitadel:iam:org:domain:primary:(.+)/; // TODO: check regex for all domain character options const IDP_SCOPE_REGEX = /urn:zitadel:iam:org:idp:id:(.+)/; -/** - * mfa is required, session is not valid anymore (e.g. session expired, user logged out, etc.) - * to check for mfa for automatically selected session -> const response = await listAuthenticationMethodTypes(userId); - **/ -async function isSessionValid( - serviceUrl: string, - - session: Session, -): Promise { - // 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 { - const sessionsWithHint = sessions.filter((s) => { - if (authRequest.hintUserId) { - return s.factors?.user?.id === authRequest.hintUserId; - } - if (authRequest.loginHint) { - return s.factors?.user?.loginName === authRequest.loginHint; - } - return true; - }); - - if (sessionsWithHint.length === 0) { - return undefined; - } - - // sort by change date descending - sessionsWithHint.sort((a, b) => { - const dateA = a.changeDate ? timestampDate(a.changeDate).getTime() : 0; - const dateB = b.changeDate ? timestampDate(b.changeDate).getTime() : 0; - return dateB - dateA; - }); - - // return the first valid session according to settings - for (const session of sessionsWithHint) { - if (await isSessionValid(serviceUrl, session)) { - return session; - } - } - - return undefined; -} - -function constructUrl(request: NextRequest, path: string) { - const forwardedHost = - request.headers.get("x-zitadel-forward-host") ?? - request.headers.get("host"); - const basePath = process.env.NEXT_PUBLIC_BASE_PATH || ""; - return new URL( - `${basePath}${path}`, - forwardedHost?.startsWith("http") - ? forwardedHost - : `https://${forwardedHost}`, - ); -} - export async function GET(request: NextRequest) { - const searchParams = request.nextUrl.searchParams; - const authRequestId = searchParams.get("authRequest"); - const sessionId = searchParams.get("sessionId"); - const _headers = await headers(); const { serviceUrl } = getServiceUrlFromHeaders(_headers); + const searchParams = request.nextUrl.searchParams; + + const oidcRequestId = searchParams.get("authRequest"); // oidc initiated request + const samlRequestId = searchParams.get("samlRequest"); // saml initiated request + + // internal request id which combines authRequest and samlRequest with the prefix oidc_ or saml_ + let requestId = + searchParams.get("requestId") || + `oidc_${oidcRequestId}` || + `saml_${samlRequestId}`; + + const sessionId = searchParams.get("sessionId"); + // TODO: find a better way to handle _rsc (react server components) requests and block them to avoid conflicts when creating oidc callback const _rsc = searchParams.get("_rsc"); if (_rsc) { @@ -232,128 +100,36 @@ export async function GET(request: NextRequest) { sessions = await loadSessions({ serviceUrl, ids }); } - if (authRequestId && sessionId) { - console.log( - `Login with session: ${sessionId} and authRequest: ${authRequestId}`, - ); - - const selectedSession = sessions.find((s) => s.id === sessionId); - - if (selectedSession && selectedSession.id) { - console.log(`Found session ${selectedSession.id}`); - - const isValid = await isSessionValid( + // complete flow if session and request id are provided + if (requestId && sessionId) { + if (requestId.startsWith("oidc_")) { + // this finishes the login process for OIDC + return loginWithOIDCandSession({ serviceUrl, - - selectedSession, - ); - - console.log("Session is valid:", isValid); - - if (!isValid && selectedSession.factors?.user) { - // if the session is not valid anymore, we need to redirect the user to re-authenticate / - // TODO: handle IDP intent direcly if available - const command: SendLoginnameCommand = { - loginName: selectedSession.factors.user?.loginName, - organization: selectedSession.factors?.user?.organizationId, - authRequestId: authRequestId, - }; - - const res = await sendLoginname(command); - - if (res && "redirect" in res && res?.redirect) { - const absoluteUrl = new URL(res.redirect, request.nextUrl); - return NextResponse.redirect(absoluteUrl.toString()); - } - } - - const cookie = sessionCookies.find( - (cookie) => cookie.id === selectedSession?.id, - ); - - if (cookie && cookie.id && cookie.token) { - const session = { - sessionId: cookie?.id, - sessionToken: cookie?.token, - }; - - // works not with _rsc request - try { - const { callbackUrl } = await createCallback({ - serviceUrl, - - req: create(CreateCallbackRequestSchema, { - authRequestId, - callbackKind: { - case: "session", - value: create(SessionSchema, session), - }, - }), - }); - if (callbackUrl) { - return NextResponse.redirect(callbackUrl); - } else { - return NextResponse.json( - { error: "An error occurred!" }, - { status: 500 }, - ); - } - } catch (error: unknown) { - // handle already handled gracefully as these could come up if old emails with authRequestId are used (reset password, register emails etc.) - console.error(error); - if ( - error && - typeof error === "object" && - "code" in error && - error?.code === 9 - ) { - const loginSettings = await getLoginSettings({ - serviceUrl, - - organization: selectedSession.factors?.user?.organizationId, - }); - - 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 }); - } - } - } + authRequest: requestId.replace("oidc_", ""), + sessionId, + sessions, + sessionCookies, + request, + }); + } else if (requestId.startsWith("saml_")) { + // this finishes the login process for SAML + return loginWithSAMLandSession({ + serviceUrl, + samlRequest: requestId.replace("saml_", ""), + sessionId, + sessions, + sessionCookies, + request, + }); } } - if (authRequestId) { + // continue with OIDC + if (requestId && requestId.startsWith("oidc_")) { const { authRequest } = await getAuthRequest({ serviceUrl, - - authRequestId, + authRequestId: requestId.replace("oidc_", ""), }); let organization = ""; @@ -400,7 +176,6 @@ export async function GET(request: NextRequest) { const identityProviders = await getActiveIdentityProviders({ serviceUrl, - orgId: organization ? organization : undefined, }).then((resp) => { return resp.identityProviders; @@ -416,8 +191,8 @@ export async function GET(request: NextRequest) { const params = new URLSearchParams(); - if (authRequestId) { - params.set("authRequestId", authRequestId); + if (requestId) { + params.set("requestId", requestId); } if (organization) { @@ -426,7 +201,6 @@ export async function GET(request: NextRequest) { return startIdentityProviderFlow({ serviceUrl, - idpId, urls: { successUrl: @@ -448,41 +222,27 @@ export async function GET(request: NextRequest) { } } - const gotoAccounts = (): NextResponse => { - const accountsUrl = constructUrl(request, "/accounts"); - const params = new URLSearchParams(); - if (authRequest?.id) { - params.append("authRequestId", authRequest.id); - // accountsUrl.searchParams.set("authRequestId", authRequest?.id); - } - if (organization) { - params.append("organization", organization); - // accountsUrl.searchParams.set("organization", organization); - } - - return NextResponse.redirect(accountsUrl + "?" + params); - }; - if (authRequest && authRequest.prompt.includes(Prompt.CREATE)) { - const registerUrl = constructUrl(request, "/register"); - const params = new URLSearchParams(); + const registerUrl = new URL("/register", request.url); if (authRequest.id) { - params.append("authRequestId", authRequest.id); - // registerUrl.searchParams.set("authRequestId", authRequest.id); + registerUrl.searchParams.set("requestId", `oidc_${authRequest.id}`); } if (organization) { - params.append("organization", organization); - // registerUrl.searchParams.set("organization", organization); + registerUrl.searchParams.set("organization", organization); } - return NextResponse.redirect(registerUrl + "?" + params); + return NextResponse.redirect(registerUrl); } // use existing session and hydrate it for oidc if (authRequest && sessions.length) { // if some accounts are available for selection and select_account is set if (authRequest.prompt.includes(Prompt.SELECT_ACCOUNT)) { - return gotoAccounts(); + return gotoAccounts({ + request, + requestId: `oidc_${authRequest.id}`, + organization, + }); } else if (authRequest.prompt.includes(Prompt.LOGIN)) { /** * The login prompt instructs the authentication server to prompt the user for re-authentication, regardless of whether the user is already authenticated @@ -493,7 +253,7 @@ export async function GET(request: NextRequest) { try { let command: SendLoginnameCommand = { loginName: authRequest.loginHint, - authRequestId: authRequest.id, + requestId: authRequest.id, }; if (organization) { @@ -503,7 +263,7 @@ export async function GET(request: NextRequest) { const res = await sendLoginname(command); if (res && "redirect" in res && res?.redirect) { - const absoluteUrl = new URL(res.redirect, request.nextUrl); + const absoluteUrl = new URL(res.redirect, request.url); return NextResponse.redirect(absoluteUrl.toString()); } } catch (error) { @@ -511,39 +271,31 @@ export async function GET(request: NextRequest) { } } - const loginNameUrl = constructUrl(request, "/loginname"); - - const params = new URLSearchParams(); - + const loginNameUrl = new URL("/loginname", request.url); if (authRequest.id) { - params.append("authRequestId", authRequest.id); - // loginNameUrl.searchParams.set("authRequestId", authRequest.id); + loginNameUrl.searchParams.set("requestId", `oidc_${authRequest.id}`); } if (authRequest.loginHint) { - params.append("loginName", authRequest.loginHint); - // loginNameUrl.searchParams.set("loginName", authRequest.loginHint); + loginNameUrl.searchParams.set("loginName", authRequest.loginHint); } if (organization) { - params.append("organization", organization); - // loginNameUrl.searchParams.set("organization", organization); + loginNameUrl.searchParams.set("organization", organization); } if (suffix) { - params.append("suffix", suffix); - // loginNameUrl.searchParams.set("suffix", suffix); + loginNameUrl.searchParams.set("suffix", suffix); } - return NextResponse.redirect(loginNameUrl + "?" + params); + return NextResponse.redirect(loginNameUrl); } else if (authRequest.prompt.includes(Prompt.NONE)) { /** * With an OIDC none prompt, the authentication server must not display any authentication or consent user interface pages. * This means that the user should not be prompted to enter their password again. * Instead, the server attempts to silently authenticate the user using an existing session or other authentication mechanisms that do not require user interaction **/ - const selectedSession = await findValidSession( + const selectedSession = await findValidSession({ serviceUrl, - sessions, authRequest, - ); + }); if (!selectedSession || !selectedSession.id) { return NextResponse.json( @@ -570,9 +322,8 @@ export async function GET(request: NextRequest) { const { callbackUrl } = await createCallback({ serviceUrl, - req: create(CreateCallbackRequestSchema, { - authRequestId, + authRequestId: requestId.replace("oidc_", ""), callbackKind: { case: "session", value: create(SessionSchema, session), @@ -582,15 +333,18 @@ export async function GET(request: NextRequest) { return NextResponse.redirect(callbackUrl); } else { // check for loginHint, userId hint and valid sessions - let selectedSession = await findValidSession( + let selectedSession = await findValidSession({ serviceUrl, - sessions, authRequest, - ); + }); if (!selectedSession || !selectedSession.id) { - return gotoAccounts(); + return gotoAccounts({ + request, + requestId: `oidc_${authRequest.id}`, + organization, + }); } const cookie = sessionCookies.find( @@ -598,7 +352,11 @@ export async function GET(request: NextRequest) { ); if (!cookie || !cookie.id || !cookie.token) { - return gotoAccounts(); + return gotoAccounts({ + request, + requestId: `oidc_${authRequest.id}`, + organization, + }); } const session = { @@ -611,7 +369,7 @@ export async function GET(request: NextRequest) { serviceUrl, req: create(CreateCallbackRequestSchema, { - authRequestId, + authRequestId: requestId.replace("oidc_", ""), callbackKind: { case: "session", value: create(SessionSchema, session), @@ -624,36 +382,148 @@ export async function GET(request: NextRequest) { console.log( "could not create callback, redirect user to choose other account", ); - return gotoAccounts(); + return gotoAccounts({ + request, + organization, + requestId: `oidc_${authRequest.id}`, + }); } } catch (error) { console.error(error); - return gotoAccounts(); + return gotoAccounts({ + request, + requestId: `oidc_${authRequest.id}`, + organization, + }); } } } else { - const loginNameUrl = constructUrl(request, "/loginname"); + const loginNameUrl = new URL("/loginname", request.url); - const params = new URLSearchParams(); - params.set("authRequestId", authRequestId); - // loginNameUrl.searchParams.set("authRequestId", authRequestId); + loginNameUrl.searchParams.set("requestId", requestId); if (authRequest?.loginHint) { - params.set("loginName", authRequest.loginHint); - params.set("submit", "true"); // autosubmit - // loginNameUrl.searchParams.set("loginName", authRequest.loginHint); - // loginNameUrl.searchParams.set("submit", "true"); // autosubmit + loginNameUrl.searchParams.set("loginName", authRequest.loginHint); + loginNameUrl.searchParams.set("submit", "true"); // autosubmit } if (organization) { - params.set("organization", organization); + loginNameUrl.searchParams.append("organization", organization); // loginNameUrl.searchParams.set("organization", organization); } - return NextResponse.redirect(loginNameUrl + "?" + params); + return NextResponse.redirect(loginNameUrl); + } + } + // continue with SAML + else if (requestId && requestId.startsWith("saml_")) { + const { samlRequest } = await getSAMLRequest({ + serviceUrl, + samlRequestId: requestId.replace("saml_", ""), + }); + + if (!samlRequest) { + return NextResponse.json( + { error: "No samlRequest found" }, + { status: 400 }, + ); + } + + let selectedSession = await findValidSession({ + serviceUrl, + sessions, + samlRequest, + }); + + if (!selectedSession || !selectedSession.id) { + return gotoAccounts({ + request, + requestId: `saml_${samlRequest.id}`, + }); + } + + const cookie = sessionCookies.find( + (cookie) => cookie.id === selectedSession.id, + ); + + if (!cookie || !cookie.id || !cookie.token) { + return gotoAccounts({ + request, + requestId: `saml_${samlRequest.id}`, + // organization, + }); + } + + const session = { + sessionId: cookie.id, + sessionToken: cookie.token, + }; + + try { + const { url, binding } = await createResponse({ + serviceUrl, + req: create(CreateResponseRequestSchema, { + samlRequestId: requestId.replace("saml_", ""), + responseKind: { + case: "session", + value: session, + }, + }), + }); + if (url && binding.case === "redirect") { + return NextResponse.redirect(url); + } else if (url && binding.case === "post") { + const formData = { + key1: "value1", + key2: "value2", + }; + + // Convert form data to URL-encoded string + const formBody = Object.entries(formData) + .map( + ([key, value]) => + encodeURIComponent(key) + "=" + encodeURIComponent(value), + ) + .join("&"); + + // Make a POST request to the external URL with the form data + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: formBody, + }); + + // Handle the response from the external URL + if (response.ok) { + return NextResponse.json({ + message: "SAML request completed successfully", + }); + } else { + return NextResponse.json( + { error: "Failed to complete SAML request" }, + { status: response.status }, + ); + } + } else { + console.log( + "could not create response, redirect user to choose other account", + ); + return gotoAccounts({ + request, + requestId: `saml_${samlRequest.id}`, + }); + } + } catch (error) { + console.error(error); + return gotoAccounts({ + request, + requestId: `saml_${samlRequest.id}`, + }); } } else { return NextResponse.json( - { error: "No authRequestId provided" }, + { error: "No authRequest nor samlRequest provided" }, { status: 500 }, ); } diff --git a/apps/login/src/components/change-password-form.tsx b/apps/login/src/components/change-password-form.tsx index c581a40b8d..54aab7b3ca 100644 --- a/apps/login/src/components/change-password-form.tsx +++ b/apps/login/src/components/change-password-form.tsx @@ -35,7 +35,7 @@ type Props = { passwordComplexitySettings: PasswordComplexitySettings; sessionId: string; loginName: string; - authRequestId?: string; + requestId?: string; organization?: string; }; @@ -43,7 +43,7 @@ export function ChangePasswordForm({ passwordComplexitySettings, sessionId, loginName, - authRequestId, + requestId, organization, }: Props) { const t = useTranslations("password"); @@ -97,7 +97,7 @@ export function ChangePasswordForm({ checks: create(ChecksSchema, { password: { password: values.password }, }), - authRequestId, + requestId, }) .catch(() => { setError("Could not verify password"); diff --git a/apps/login/src/components/choose-second-factor-to-setup.tsx b/apps/login/src/components/choose-second-factor-to-setup.tsx index 1502d555a7..e56379e147 100644 --- a/apps/login/src/components/choose-second-factor-to-setup.tsx +++ b/apps/login/src/components/choose-second-factor-to-setup.tsx @@ -1,35 +1,44 @@ "use client"; +import { skipMFAAndContinueWithNextUrl } from "@/lib/server/session"; import { LoginSettings, SecondFactorType, } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { useTranslations } from "next-intl"; +import { useRouter } from "next/navigation"; import { EMAIL, SMS, TOTP, U2F } from "./auth-methods"; type Props = { + userId: string; loginName?: string; sessionId?: string; - authRequestId?: string; + requestId?: string; organization?: string; loginSettings: LoginSettings; userMethods: AuthenticationMethodType[]; checkAfter: boolean; phoneVerified: boolean; emailVerified: boolean; + force: boolean; }; export function ChooseSecondFactorToSetup({ + userId, loginName, sessionId, - authRequestId, + requestId, organization, loginSettings, userMethods, checkAfter, phoneVerified, emailVerified, + force, }: Props) { + const t = useTranslations("mfa"); + const router = useRouter(); const params = new URLSearchParams({}); if (loginName) { @@ -38,8 +47,8 @@ export function ChooseSecondFactorToSetup({ if (sessionId) { params.append("sessionId", sessionId); } - if (authRequestId) { - params.append("authRequestId", authRequestId); + if (requestId) { + params.append("requestId", requestId); } if (organization) { params.append("organization", organization); @@ -49,39 +58,63 @@ export function ChooseSecondFactorToSetup({ } return ( -
- {loginSettings.secondFactors.map((factor) => { - switch (factor) { - case SecondFactorType.OTP: - return TOTP( - userMethods.includes(AuthenticationMethodType.TOTP), - "/otp/time-based/set?" + params, - ); - case SecondFactorType.U2F: - return U2F( - userMethods.includes(AuthenticationMethodType.U2F), - "/u2f/set?" + params, - ); - case SecondFactorType.OTP_EMAIL: - return ( - emailVerified && - EMAIL( - userMethods.includes(AuthenticationMethodType.OTP_EMAIL), - "/otp/email/set?" + params, - ) - ); - case SecondFactorType.OTP_SMS: - return ( - phoneVerified && - SMS( - userMethods.includes(AuthenticationMethodType.OTP_SMS), - "/otp/sms/set?" + params, - ) - ); - default: - return null; - } - })} -
+ <> +
+ {loginSettings.secondFactors.map((factor) => { + switch (factor) { + case SecondFactorType.OTP: + return TOTP( + userMethods.includes(AuthenticationMethodType.TOTP), + "/otp/time-based/set?" + params, + ); + case SecondFactorType.U2F: + return U2F( + userMethods.includes(AuthenticationMethodType.U2F), + "/u2f/set?" + params, + ); + case SecondFactorType.OTP_EMAIL: + return ( + emailVerified && + EMAIL( + userMethods.includes(AuthenticationMethodType.OTP_EMAIL), + "/otp/email/set?" + params, + ) + ); + case SecondFactorType.OTP_SMS: + return ( + phoneVerified && + SMS( + userMethods.includes(AuthenticationMethodType.OTP_SMS), + "/otp/sms/set?" + params, + ) + ); + default: + return null; + } + })} +
+ {!force && ( + + )} + ); } diff --git a/apps/login/src/components/choose-second-factor.tsx b/apps/login/src/components/choose-second-factor.tsx index 3acf3e2214..6cd890f11d 100644 --- a/apps/login/src/components/choose-second-factor.tsx +++ b/apps/login/src/components/choose-second-factor.tsx @@ -6,7 +6,7 @@ import { EMAIL, SMS, TOTP, U2F } from "./auth-methods"; type Props = { loginName?: string; sessionId?: string; - authRequestId?: string; + requestId?: string; organization?: string; userMethods: AuthenticationMethodType[]; }; @@ -14,7 +14,7 @@ type Props = { export function ChooseSecondFactor({ loginName, sessionId, - authRequestId, + requestId, organization, userMethods, }: Props) { @@ -26,8 +26,8 @@ export function ChooseSecondFactor({ if (sessionId) { params.append("sessionId", sessionId); } - if (authRequestId) { - params.append("authRequestId", authRequestId); + if (requestId) { + params.append("requestId", requestId); } if (organization) { params.append("organization", organization); diff --git a/apps/login/src/components/idp-signin.tsx b/apps/login/src/components/idp-signin.tsx index c2f3fe40b3..a7c938e90c 100644 --- a/apps/login/src/components/idp-signin.tsx +++ b/apps/login/src/components/idp-signin.tsx @@ -13,13 +13,13 @@ type Props = { idpIntentId: string; idpIntentToken: string; }; - authRequestId?: string; + requestId?: string; }; export function IdpSignin({ userId, idpIntent: { idpIntentId, idpIntentToken }, - authRequestId, + requestId, }: Props) { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -33,7 +33,7 @@ export function IdpSignin({ idpIntentId, idpIntentToken, }, - authRequestId, + requestId, }) .then((response) => { if (response && "error" in response && response?.error) { diff --git a/apps/login/src/components/idps/pages/linking-success.tsx b/apps/login/src/components/idps/pages/linking-success.tsx index 66098ed6ff..f4faa8e1bf 100644 --- a/apps/login/src/components/idps/pages/linking-success.tsx +++ b/apps/login/src/components/idps/pages/linking-success.tsx @@ -6,7 +6,7 @@ import { IdpSignin } from "../../idp-signin"; export async function linkingSuccess( userId: string, idpIntent: { idpIntentId: string; idpIntentToken: string }, - authRequestId?: string, + requestId?: string, branding?: BrandingSettings, ) { const locale = getLocale(); @@ -21,7 +21,7 @@ export async function linkingSuccess( diff --git a/apps/login/src/components/idps/pages/login-success.tsx b/apps/login/src/components/idps/pages/login-success.tsx index 3a9a371995..6c884873f1 100644 --- a/apps/login/src/components/idps/pages/login-success.tsx +++ b/apps/login/src/components/idps/pages/login-success.tsx @@ -6,7 +6,7 @@ import { IdpSignin } from "../../idp-signin"; export async function loginSuccess( userId: string, idpIntent: { idpIntentId: string; idpIntentToken: string }, - authRequestId?: string, + requestId?: string, branding?: BrandingSettings, ) { const locale = getLocale(); @@ -21,7 +21,7 @@ export async function loginSuccess( diff --git a/apps/login/src/components/login-otp.tsx b/apps/login/src/components/login-otp.tsx index 7e6a4205fd..f8500f6909 100644 --- a/apps/login/src/components/login-otp.tsx +++ b/apps/login/src/components/login-otp.tsx @@ -21,7 +21,7 @@ type Props = { host: string | null; loginName?: string; sessionId?: string; - authRequestId?: string; + requestId?: string; organization?: string; method: string; code?: string; @@ -36,7 +36,7 @@ export function LoginOTP({ host, loginName, sessionId, - authRequestId, + requestId, organization, method, code, @@ -87,7 +87,7 @@ export function LoginOTP({ ? { urlTemplate: `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/otp/${method}?code={{.Code}}&userId={{.UserID}}&sessionId={{.SessionID}}` + - (authRequestId ? `&authRequestId=${authRequestId}` : ""), + (requestId ? `&requestId=${requestId}` : ""), } : {}, }, @@ -107,7 +107,7 @@ export function LoginOTP({ sessionId, organization, challenges, - authRequestId, + requestId, }) .catch(() => { setError("Could not request OTP challenge"); @@ -137,8 +137,8 @@ export function LoginOTP({ body.organization = organization; } - if (authRequestId) { - body.authRequestId = authRequestId; + if (requestId) { + body.requestId = requestId; } let checks; @@ -164,7 +164,7 @@ export function LoginOTP({ sessionId, organization, checks, - authRequestId, + requestId, }) .catch(() => { setError("Could not verify OTP code"); @@ -190,11 +190,11 @@ export function LoginOTP({ await new Promise((resolve) => setTimeout(resolve, 2000)); const url = - authRequestId && response.sessionId + requestId && response.sessionId ? await getNextUrl( { sessionId: response.sessionId, - authRequestId: authRequestId, + requestId: requestId, organization: response.factors?.user?.organizationId, }, loginSettings?.defaultRedirectUri, diff --git a/apps/login/src/components/login-passkey.tsx b/apps/login/src/components/login-passkey.tsx index a5beae7396..b3f0b1212f 100644 --- a/apps/login/src/components/login-passkey.tsx +++ b/apps/login/src/components/login-passkey.tsx @@ -21,7 +21,7 @@ import { Spinner } from "./spinner"; type Props = { loginName?: string; sessionId?: string; - authRequestId?: string; + requestId?: string; altPassword: boolean; login?: boolean; organization?: string; @@ -30,7 +30,7 @@ type Props = { export function LoginPasskey({ loginName, sessionId, - authRequestId, + requestId, altPassword, organization, login = true, @@ -96,7 +96,7 @@ export function LoginPasskey({ userVerificationRequirement, }, }), - authRequestId, + requestId, }) .catch(() => { setError("Could not request passkey challenge"); @@ -123,7 +123,7 @@ export function LoginPasskey({ checks: { webAuthN: { credentialAssertionData: data }, } as Checks, - authRequestId, + requestId, }) .catch(() => { setError("Could not verify passkey"); @@ -220,8 +220,8 @@ export function LoginPasskey({ params.sessionId = sessionId; } - if (authRequestId) { - params.authRequestId = authRequestId; + if (requestId) { + params.requestId = requestId; } if (organization) { diff --git a/apps/login/src/components/password-form.tsx b/apps/login/src/components/password-form.tsx index 2d623aa09e..17461644d8 100644 --- a/apps/login/src/components/password-form.tsx +++ b/apps/login/src/components/password-form.tsx @@ -22,7 +22,7 @@ type Props = { loginSettings: LoginSettings | undefined; loginName: string; organization?: string; - authRequestId?: string; + requestId?: string; isAlternative?: boolean; // whether password was requested as alternative auth method promptPasswordless?: boolean; }; @@ -31,7 +31,7 @@ export function PasswordForm({ loginSettings, loginName, organization, - authRequestId, + requestId, promptPasswordless, isAlternative, }: Props) { @@ -58,7 +58,7 @@ export function PasswordForm({ checks: create(ChecksSchema, { password: { password: values.password }, }), - authRequestId, + requestId, }) .catch(() => { setError("Could not verify password"); @@ -86,7 +86,7 @@ export function PasswordForm({ const response = await resetPassword({ loginName, organization, - authRequestId, + requestId, }) .catch(() => { setError("Could not reset password"); @@ -111,8 +111,8 @@ export function PasswordForm({ params.append("organization", organization); } - if (authRequestId) { - params.append("authRequestId", authRequestId); + if (requestId) { + params.append("requestId", requestId); } return router.push("/password/set?" + params); diff --git a/apps/login/src/components/register-form.tsx b/apps/login/src/components/register-form.tsx index c336b836c4..09e3f0b89b 100644 --- a/apps/login/src/components/register-form.tsx +++ b/apps/login/src/components/register-form.tsx @@ -36,7 +36,7 @@ type Props = { lastname?: string; email?: string; organization?: string; - authRequestId?: string; + requestId?: string; loginSettings?: LoginSettings; }; @@ -46,7 +46,7 @@ export function RegisterForm({ firstname, lastname, organization, - authRequestId, + requestId, loginSettings, }: Props) { const t = useTranslations("register"); @@ -73,7 +73,7 @@ export function RegisterForm({ firstName: values.firstname, lastName: values.lastname, organization: organization, - authRequestId: authRequestId, + requestId: requestId, }) .catch(() => { setError("Could not register user"); @@ -105,8 +105,8 @@ export function RegisterForm({ registerParams.organization = organization; } - if (authRequestId) { - registerParams.authRequestId = authRequestId; + if (requestId) { + registerParams.requestId = requestId; } // redirect user to /register/password if password is chosen diff --git a/apps/login/src/components/register-passkey.tsx b/apps/login/src/components/register-passkey.tsx index e737168678..163ab507b8 100644 --- a/apps/login/src/components/register-passkey.tsx +++ b/apps/login/src/components/register-passkey.tsx @@ -19,7 +19,7 @@ type Inputs = {}; type Props = { sessionId: string; isPrompt: boolean; - authRequestId?: string; + requestId?: string; organization?: string; }; @@ -27,7 +27,7 @@ export function RegisterPasskey({ sessionId, isPrompt, organization, - authRequestId, + requestId, }: Props) { const t = useTranslations("passkey"); @@ -161,8 +161,8 @@ export function RegisterPasskey({ params.set("organization", organization); } - if (authRequestId) { - params.set("authRequestId", authRequestId); + if (requestId) { + params.set("requestId", requestId); } params.set("sessionId", sessionId); diff --git a/apps/login/src/components/register-u2f.tsx b/apps/login/src/components/register-u2f.tsx index ba83de3627..753eae017d 100644 --- a/apps/login/src/components/register-u2f.tsx +++ b/apps/login/src/components/register-u2f.tsx @@ -16,7 +16,7 @@ import { Spinner } from "./spinner"; type Props = { loginName?: string; sessionId: string; - authRequestId?: string; + requestId?: string; organization?: string; checkAfter: boolean; loginSettings?: LoginSettings; @@ -26,7 +26,7 @@ export function RegisterU2f({ loginName, sessionId, organization, - authRequestId, + requestId, checkAfter, loginSettings, }: Props) { @@ -166,18 +166,18 @@ export function RegisterU2f({ if (organization) { paramsToContinue.append("organization", organization); } - if (authRequestId) { - paramsToContinue.append("authRequestId", authRequestId); + if (requestId) { + paramsToContinue.append("requestId", requestId); } return router.push(`/u2f?` + paramsToContinue); } else { const url = - authRequestId && sessionId + requestId && sessionId ? await getNextUrl( { sessionId: sessionId, - authRequestId: authRequestId, + requestId: requestId, organization: organization, }, loginSettings?.defaultRedirectUri, diff --git a/apps/login/src/components/session-item.tsx b/apps/login/src/components/session-item.tsx index c3c28a03fd..1274469f01 100644 --- a/apps/login/src/components/session-item.tsx +++ b/apps/login/src/components/session-item.tsx @@ -31,11 +31,11 @@ export function isSessionValid(session: Partial): { export function SessionItem({ session, reload, - authRequestId, + requestId, }: { session: Session; reload: () => void; - authRequestId?: string; + requestId?: string; }) { const [loading, setLoading] = useState(false); @@ -67,7 +67,7 @@ export function SessionItem({ if (valid && session?.factors?.user) { const resp = await continueWithSession({ ...session, - authRequestId: authRequestId, + requestId: requestId, }); if (resp?.redirect) { @@ -78,7 +78,7 @@ export function SessionItem({ const res = await sendLoginname({ loginName: session.factors?.user?.loginName, organization: session.factors.user.organizationId, - authRequestId: authRequestId, + requestId: requestId, }) .catch(() => { setError("An internal error occurred"); diff --git a/apps/login/src/components/sessions-list.tsx b/apps/login/src/components/sessions-list.tsx index 09393bae72..50f621a62d 100644 --- a/apps/login/src/components/sessions-list.tsx +++ b/apps/login/src/components/sessions-list.tsx @@ -9,10 +9,10 @@ import { SessionItem } from "./session-item"; type Props = { sessions: Session[]; - authRequestId?: string; + requestId?: string; }; -export function SessionsList({ sessions, authRequestId }: Props) { +export function SessionsList({ sessions, requestId }: Props) { const t = useTranslations("accounts"); const [list, setList] = useState(sessions); return sessions ? ( @@ -34,7 +34,7 @@ export function SessionsList({ sessions, authRequestId }: Props) { return ( { setList(list.filter((s) => s.id !== session.id)); }} diff --git a/apps/login/src/components/set-password-form.tsx b/apps/login/src/components/set-password-form.tsx index ec6cf3cc6b..08f5c7c4ef 100644 --- a/apps/login/src/components/set-password-form.tsx +++ b/apps/login/src/components/set-password-form.tsx @@ -39,14 +39,14 @@ type Props = { loginName: string; userId: string; organization?: string; - authRequestId?: string; + requestId?: string; codeRequired: boolean; }; export function SetPasswordForm({ passwordComplexitySettings, organization, - authRequestId, + requestId, loginName, userId, code, @@ -73,7 +73,7 @@ export function SetPasswordForm({ const response = await resetPassword({ loginName, organization, - authRequestId, + requestId, }) .catch(() => { setError("Could not reset password"); @@ -137,7 +137,7 @@ export function SetPasswordForm({ checks: create(ChecksSchema, { password: { password: values.password }, }), - authRequestId, + requestId, }) .catch(() => { setError("Could not verify password"); diff --git a/apps/login/src/components/set-register-password-form.tsx b/apps/login/src/components/set-register-password-form.tsx index 19bab38e10..3f38a408d0 100644 --- a/apps/login/src/components/set-register-password-form.tsx +++ b/apps/login/src/components/set-register-password-form.tsx @@ -32,7 +32,7 @@ type Props = { firstname: string; lastname: string; organization?: string; - authRequestId?: string; + requestId?: string; }; export function SetRegisterPasswordForm({ @@ -41,7 +41,7 @@ export function SetRegisterPasswordForm({ firstname, lastname, organization, - authRequestId, + requestId, }: Props) { const t = useTranslations("register"); @@ -66,7 +66,7 @@ export function SetRegisterPasswordForm({ firstName: firstname, lastName: lastname, organization: organization, - authRequestId: authRequestId, + requestId: requestId, password: values.password, }) .catch(() => { diff --git a/apps/login/src/components/sign-in-with-idp.tsx b/apps/login/src/components/sign-in-with-idp.tsx index 972f501cb1..5af5878759 100644 --- a/apps/login/src/components/sign-in-with-idp.tsx +++ b/apps/login/src/components/sign-in-with-idp.tsx @@ -20,14 +20,14 @@ import { SignInWithGoogle } from "./idps/sign-in-with-google"; export interface SignInWithIDPProps { children?: ReactNode; identityProviders: IdentityProvider[]; - authRequestId?: string; + requestId?: string; organization?: string; linkOnly?: boolean; } export function SignInWithIdp({ identityProviders, - authRequestId, + requestId, organization, linkOnly, }: Readonly) { @@ -40,7 +40,7 @@ export function SignInWithIdp({ setLoading(true); const params = new URLSearchParams(); if (linkOnly) params.set("link", "true"); - if (authRequestId) params.set("authRequestId", authRequestId); + if (requestId) params.set("requestId", requestId); if (organization) params.set("organization", organization); try { @@ -64,7 +64,7 @@ export function SignInWithIdp({ setLoading(false); } }, - [authRequestId, organization, linkOnly, router], + [requestId, organization, linkOnly, router], ); const renderIDPButton = (idp: IdentityProvider) => { diff --git a/apps/login/src/components/totp-register.tsx b/apps/login/src/components/totp-register.tsx index 40aa94a165..b5c81d8645 100644 --- a/apps/login/src/components/totp-register.tsx +++ b/apps/login/src/components/totp-register.tsx @@ -24,7 +24,7 @@ type Props = { secret: string; loginName?: string; sessionId?: string; - authRequestId?: string; + requestId?: string; organization?: string; checkAfter?: boolean; loginSettings?: LoginSettings; @@ -34,7 +34,7 @@ export function TotpRegister({ secret, loginName, sessionId, - authRequestId, + requestId, organization, checkAfter, loginSettings, @@ -63,8 +63,8 @@ export function TotpRegister({ if (loginName) { params.append("loginName", loginName); } - if (authRequestId) { - params.append("authRequestId", authRequestId); + if (requestId) { + params.append("requestId", requestId); } if (organization) { params.append("organization", organization); @@ -73,11 +73,11 @@ export function TotpRegister({ return router.push(`/otp/time-based?` + params); } else { const url = - authRequestId && sessionId + requestId && sessionId ? await getNextUrl( { sessionId: sessionId, - authRequestId: authRequestId, + requestId: requestId, organization: organization, }, loginSettings?.defaultRedirectUri, diff --git a/apps/login/src/components/user-avatar.tsx b/apps/login/src/components/user-avatar.tsx index b7644310eb..f2aa0bfed7 100644 --- a/apps/login/src/components/user-avatar.tsx +++ b/apps/login/src/components/user-avatar.tsx @@ -25,8 +25,8 @@ export function UserAvatar({ params.set("organization", searchParams.organization); } - if (searchParams?.authRequestId) { - params.set("authRequestId", searchParams.authRequestId); + if (searchParams?.requestId) { + params.set("requestId", searchParams.requestId); } if (searchParams?.loginName) { diff --git a/apps/login/src/components/username-form.tsx b/apps/login/src/components/username-form.tsx index 28193b451c..6801f6b274 100644 --- a/apps/login/src/components/username-form.tsx +++ b/apps/login/src/components/username-form.tsx @@ -18,7 +18,7 @@ type Inputs = { type Props = { loginName: string | undefined; - authRequestId: string | undefined; + requestId: string | undefined; loginSettings: LoginSettings | undefined; organization?: string; suffix?: string; @@ -29,7 +29,7 @@ type Props = { export function UsernameForm({ loginName, - authRequestId, + requestId, organization, suffix, loginSettings, @@ -56,7 +56,7 @@ export function UsernameForm({ const res = await sendLoginname({ loginName: values.loginName, organization, - authRequestId, + requestId, suffix, }) .catch(() => { @@ -117,8 +117,8 @@ export function UsernameForm({ if (organization) { registerParams.append("organization", organization); } - if (authRequestId) { - registerParams.append("authRequestId", authRequestId); + if (requestId) { + registerParams.append("requestId", requestId); } router.push("/register?" + registerParams); diff --git a/apps/login/src/components/verify-form.tsx b/apps/login/src/components/verify-form.tsx index 1982375ba1..e09642eecf 100644 --- a/apps/login/src/components/verify-form.tsx +++ b/apps/login/src/components/verify-form.tsx @@ -21,14 +21,14 @@ type Props = { organization?: string; code?: string; isInvite: boolean; - authRequestId?: string; + requestId?: string; }; export function VerifyForm({ userId, loginName, organization, - authRequestId, + requestId, code, isInvite, }: Props) { @@ -78,7 +78,7 @@ export function VerifyForm({ isInvite: isInvite, loginName: loginName, organization: organization, - authRequestId: authRequestId, + requestId: requestId, }) .catch(() => { setError("Could not verify user"); diff --git a/apps/login/src/components/verify-redirect-button.tsx b/apps/login/src/components/verify-redirect-button.tsx index 552e787ebc..009dda3ffd 100644 --- a/apps/login/src/components/verify-redirect-button.tsx +++ b/apps/login/src/components/verify-redirect-button.tsx @@ -15,13 +15,13 @@ import { Spinner } from "./spinner"; export function VerifyRedirectButton({ userId, loginName, - authRequestId, + requestId, authMethods, organization, }: { userId?: string; loginName?: string; - authRequestId: string; + requestId: string; authMethods: AuthenticationMethodType[] | null; organization?: string; }) { @@ -35,7 +35,7 @@ export function VerifyRedirectButton({ let command = { organization, - authRequestId, + requestId, } as SendVerificationRedirectWithoutCheckCommand; if (userId) { diff --git a/apps/login/src/lib/client.ts b/apps/login/src/lib/client.ts index 37d22dc83d..953d66e7ee 100644 --- a/apps/login/src/lib/client.ts +++ b/apps/login/src/lib/client.ts @@ -1,12 +1,12 @@ type FinishFlowCommand = | { sessionId: string; - authRequestId: string; + requestId: string; } | { loginName: string }; /** - * for client: redirects user back to OIDC application or to a success page when using authRequestId, check if a default redirect and redirect to it, or just redirect to a success page with the loginName + * for client: redirects user back to an OIDC or SAML application or to a success page when using requestId, check if a default redirect and redirect to it, or just redirect to a success page with the loginName * @param command * @returns */ @@ -14,10 +14,10 @@ export async function getNextUrl( command: FinishFlowCommand & { organization?: string }, defaultRedirectUri?: string, ): Promise { - if ("sessionId" in command && "authRequestId" in command) { + if ("sessionId" in command && "requestId" in command) { const params = new URLSearchParams({ sessionId: command.sessionId, - authRequest: command.authRequestId, + requestId: command.requestId, }); if (command.organization) { diff --git a/apps/login/src/lib/cookies.ts b/apps/login/src/lib/cookies.ts index 4d29b9e7d4..cf762b904f 100644 --- a/apps/login/src/lib/cookies.ts +++ b/apps/login/src/lib/cookies.ts @@ -15,7 +15,7 @@ export type Cookie = { creationTs: string; expirationTs: string; changeTs: string; - authRequestId?: string; // if its linked to an OIDC flow + requestId?: string; // if its linked to an OIDC flow }; type SessionCookie = Cookie & T; diff --git a/apps/login/src/lib/oidc.ts b/apps/login/src/lib/oidc.ts new file mode 100644 index 0000000000..fd74e57027 --- /dev/null +++ b/apps/login/src/lib/oidc.ts @@ -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 }); + } + } + } + } +} diff --git a/apps/login/src/lib/saml.ts b/apps/login/src/lib/saml.ts new file mode 100644 index 0000000000..d559c8e7df --- /dev/null +++ b/apps/login/src/lib/saml.ts @@ -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 }); + } + } + } + } +} diff --git a/apps/login/src/lib/server/cookie.ts b/apps/login/src/lib/server/cookie.ts index 154a052a7f..c5d9ced252 100644 --- a/apps/login/src/lib/server/cookie.ts +++ b/apps/login/src/lib/server/cookie.ts @@ -30,7 +30,7 @@ type CustomCookieData = { creationTs: string; expirationTs: string; changeTs: string; - authRequestId?: string; // if its linked to an OIDC flow + requestId?: string; // if its linked to an OIDC flow }; const passwordAttemptsHandler = (error: ConnectError) => { @@ -48,8 +48,7 @@ const passwordAttemptsHandler = (error: ConnectError) => { export async function createSessionAndUpdateCookie( checks: Checks, - challenges: RequestChallenges | undefined, - authRequestId: string | undefined, + requestId: string | undefined, lifetime?: Duration, ): Promise { const _headers = await headers(); @@ -57,9 +56,7 @@ export async function createSessionAndUpdateCookie( const createdSession = await createSessionFromChecks({ serviceUrl, - checks, - challenges, lifetime, }); @@ -86,8 +83,8 @@ export async function createSessionAndUpdateCookie( loginName: response.session.factors.user.loginName ?? "", }; - if (authRequestId) { - sessionCookie.authRequestId = authRequestId; + if (requestId) { + sessionCookie.requestId = requestId; } if (response.session.factors.user.organizationId) { @@ -113,7 +110,7 @@ export async function createSessionForIdpAndUpdateCookie( idpIntentId?: string | undefined; idpIntentToken?: string | undefined; }, - authRequestId: string | undefined, + requestId: string | undefined, lifetime?: Duration, ): Promise { const _headers = await headers(); @@ -165,8 +162,8 @@ export async function createSessionForIdpAndUpdateCookie( organization: session.factors.user.organizationId ?? "", }; - if (authRequestId) { - sessionCookie.authRequestId = authRequestId; + if (requestId) { + sessionCookie.requestId = requestId; } if (session.factors.user.organizationId) { @@ -186,7 +183,7 @@ export async function setSessionAndUpdateCookie( recentCookie: CustomCookieData, checks?: Checks, challenges?: RequestChallenges, - authRequestId?: string, + requestId?: string, lifetime?: Duration, ) { const _headers = await headers(); @@ -216,8 +213,8 @@ export async function setSessionAndUpdateCookie( organization: recentCookie.organization, }; - if (authRequestId) { - sessionCookie.authRequestId = authRequestId; + if (requestId) { + sessionCookie.requestId = requestId; } return getSession({ @@ -241,8 +238,8 @@ export async function setSessionAndUpdateCookie( organization: session.factors?.user?.organizationId ?? "", }; - if (sessionCookie.authRequestId) { - newCookie.authRequestId = sessionCookie.authRequestId; + if (sessionCookie.requestId) { + newCookie.requestId = sessionCookie.requestId; } return updateSessionCookie(sessionCookie.id, newCookie).then(() => { diff --git a/apps/login/src/lib/server/idp.ts b/apps/login/src/lib/server/idp.ts index edbeed2815..33d7d26bac 100644 --- a/apps/login/src/lib/server/idp.ts +++ b/apps/login/src/lib/server/idp.ts @@ -55,7 +55,7 @@ type CreateNewSessionCommand = { loginName?: string; password?: string; organization?: string; - authRequestId?: string; + requestId?: string; }; export async function createNewSessionFromIdpIntent( @@ -92,7 +92,7 @@ export async function createNewSessionFromIdpIntent( const session = await createSessionForIdpAndUpdateCookie( command.userId, command.idpIntent, - command.authRequestId, + command.requestId, loginSettings?.externalLoginCheckLifetime, ); @@ -110,7 +110,7 @@ export async function createNewSessionFromIdpIntent( session, humanUser, command.organization, - command.authRequestId, + command.requestId, ); if (emailVerificationCheck?.redirect) { @@ -118,16 +118,16 @@ export async function createNewSessionFromIdpIntent( } // TODO: check if user has MFA methods - // const mfaFactorCheck = checkMFAFactors(session, loginSettings, authMethods, organization, authRequestId); + // const mfaFactorCheck = checkMFAFactors(session, loginSettings, authMethods, organization, requestId); // if (mfaFactorCheck?.redirect) { // return mfaFactorCheck; // } const url = await getNextUrl( - command.authRequestId && session.id + command.requestId && session.id ? { sessionId: session.id, - authRequestId: command.authRequestId, + requestId: command.requestId, organization: session.factors.user.organizationId, } : { diff --git a/apps/login/src/lib/server/invite.ts b/apps/login/src/lib/server/invite.ts index ad5a7134e2..c54362889a 100644 --- a/apps/login/src/lib/server/invite.ts +++ b/apps/login/src/lib/server/invite.ts @@ -11,7 +11,7 @@ type InviteUserCommand = { lastName: string; password?: string; organization?: string; - authRequestId?: string; + requestId?: string; }; export type RegisterUserResponse = { diff --git a/apps/login/src/lib/server/loginname.ts b/apps/login/src/lib/server/loginname.ts index 658b6e4234..1739528dc2 100644 --- a/apps/login/src/lib/server/loginname.ts +++ b/apps/login/src/lib/server/loginname.ts @@ -25,7 +25,7 @@ import { createSessionAndUpdateCookie } from "./cookie"; export type SendLoginnameCommand = { loginName: string; - authRequestId?: string; + requestId?: string; organization?: string; suffix?: string; }; @@ -96,8 +96,8 @@ export async function sendLoginname(command: SendLoginnameCommand) { const params = new URLSearchParams(); - if (command.authRequestId) { - params.set("authRequestId", command.authRequestId); + if (command.requestId) { + params.set("requestId", command.requestId); } if (command.organization) { @@ -162,8 +162,8 @@ export async function sendLoginname(command: SendLoginnameCommand) { const params = new URLSearchParams({ userId }); - if (command.authRequestId) { - params.set("authRequestId", command.authRequestId); + if (command.requestId) { + params.set("requestId", command.requestId); } if (command.organization) { @@ -243,8 +243,7 @@ export async function sendLoginname(command: SendLoginnameCommand) { const session = await createSessionAndUpdateCookie( checks, - undefined, - command.authRequestId, + command.requestId, ); if (!session.factors?.user?.id) { @@ -269,7 +268,7 @@ export async function sendLoginname(command: SendLoginnameCommand) { session, humanUser, session.factors.user.organizationId, - command.authRequestId, + command.requestId, ); if (inviteCheck?.redirect) { @@ -288,8 +287,8 @@ export async function sendLoginname(command: SendLoginnameCommand) { ); } - if (command.authRequestId) { - paramsAuthenticatorSetup.append("authRequestId", command.authRequestId); + if (command.requestId) { + paramsAuthenticatorSetup.append("requestId", command.requestId); } return { redirect: "/authenticator/set?" + paramsAuthenticatorSetup }; @@ -317,8 +316,8 @@ export async function sendLoginname(command: SendLoginnameCommand) { command.organization ?? session.factors?.user?.organizationId; } - if (command.authRequestId) { - paramsPassword.authRequestId = command.authRequestId; + if (command.requestId) { + paramsPassword.requestId = command.requestId; } return { @@ -334,8 +333,8 @@ export async function sendLoginname(command: SendLoginnameCommand) { } const paramsPasskey: any = { loginName: command.loginName }; - if (command.authRequestId) { - paramsPasskey.authRequestId = command.authRequestId; + if (command.requestId) { + paramsPasskey.requestId = command.requestId; } if (command.organization || session.factors?.user?.organizationId) { @@ -353,8 +352,8 @@ export async function sendLoginname(command: SendLoginnameCommand) { altPassword: `${methods.authMethodTypes.includes(1)}`, // show alternative password option }; - if (command.authRequestId) { - passkeyParams.authRequestId = command.authRequestId; + if (command.requestId) { + passkeyParams.requestId = command.requestId; } if (command.organization || session.factors?.user?.organizationId) { @@ -373,8 +372,8 @@ export async function sendLoginname(command: SendLoginnameCommand) { // user has no passkey setup and login settings allow passkeys const paramsPasswordDefault: any = { loginName: command.loginName }; - if (command.authRequestId) { - paramsPasswordDefault.authRequestId = command.authRequestId; + if (command.requestId) { + paramsPasswordDefault.requestId = command.requestId; } if (command.organization || session.factors?.user?.organizationId) { @@ -437,8 +436,8 @@ export async function sendLoginname(command: SendLoginnameCommand) { if (orgToRegisterOn && !loginSettingsByContext?.ignoreUnknownUsernames) { const params = new URLSearchParams({ organization: orgToRegisterOn }); - if (command.authRequestId) { - params.set("authRequestId", command.authRequestId); + if (command.requestId) { + params.set("requestId", command.requestId); } if (command.loginName) { @@ -454,8 +453,8 @@ export async function sendLoginname(command: SendLoginnameCommand) { loginName: command.loginName, }); - if (command.authRequestId) { - paramsPasswordDefault.append("authRequestId", command.authRequestId); + if (command.requestId) { + paramsPasswordDefault.append("requestId", command.requestId); } if (command.organization) { diff --git a/apps/login/src/lib/server/otp.ts b/apps/login/src/lib/server/otp.ts index 5fb21872b0..11cf050bfd 100644 --- a/apps/login/src/lib/server/otp.ts +++ b/apps/login/src/lib/server/otp.ts @@ -20,7 +20,7 @@ export type SetOTPCommand = { loginName?: string; sessionId?: string; organization?: string; - authRequestId?: string; + requestId?: string; code: string; method: string; }; @@ -72,7 +72,7 @@ export async function setOTP(command: SetOTPCommand) { recentSession, checks, undefined, - command.authRequestId, + command.requestId, loginSettings?.secondFactorCheckLifetime, ).then((session) => { return { diff --git a/apps/login/src/lib/server/passkeys.ts b/apps/login/src/lib/server/passkeys.ts index 4de844c1be..7febf5b3ce 100644 --- a/apps/login/src/lib/server/passkeys.ts +++ b/apps/login/src/lib/server/passkeys.ts @@ -139,12 +139,12 @@ type SendPasskeyCommand = { sessionId?: string; organization?: string; checks?: Checks; - authRequestId?: string; + requestId?: string; lifetime?: Duration; }; export async function sendPasskey(command: SendPasskeyCommand) { - let { loginName, sessionId, organization, checks, authRequestId } = command; + let { loginName, sessionId, organization, checks, requestId } = command; const recentSession = sessionId ? await getSessionCookieById({ sessionId }) : loginName @@ -176,7 +176,7 @@ export async function sendPasskey(command: SendPasskeyCommand) { recentSession, checks, undefined, - authRequestId, + requestId, lifetime, ); @@ -203,7 +203,7 @@ export async function sendPasskey(command: SendPasskeyCommand) { session, humanUser, organization, - authRequestId, + requestId, ); if (emailVerificationCheck?.redirect) { @@ -211,11 +211,11 @@ export async function sendPasskey(command: SendPasskeyCommand) { } const url = - authRequestId && session.id + requestId && session.id ? await getNextUrl( { sessionId: session.id, - authRequestId: authRequestId, + requestId: requestId, organization: organization, }, loginSettings?.defaultRedirectUri, diff --git a/apps/login/src/lib/server/password.ts b/apps/login/src/lib/server/password.ts index 708d902fc7..92f3337af4 100644 --- a/apps/login/src/lib/server/password.ts +++ b/apps/login/src/lib/server/password.ts @@ -16,7 +16,7 @@ import { setPassword, setUserPassword, } from "@/lib/zitadel"; -import { create } from "@zitadel/client"; +import { ConnectError, create } from "@zitadel/client"; import { createServerTransport } from "@zitadel/client/node"; import { createUserServiceClient } from "@zitadel/client/v2"; import { @@ -42,7 +42,7 @@ import { type ResetPasswordCommand = { loginName: string; organization?: string; - authRequestId?: string; + requestId?: string; }; export async function resetPassword(command: ResetPasswordCommand) { @@ -77,7 +77,7 @@ export async function resetPassword(command: ResetPasswordCommand) { userId, urlTemplate: `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` + - (command.authRequestId ? `&authRequestId=${command.authRequestId}` : ""), + (command.requestId ? `&requestId=${command.requestId}` : ""), }); } @@ -85,7 +85,7 @@ export type UpdateSessionCommand = { loginName: string; organization?: string; checks: Checks; - authRequestId?: string; + requestId?: string; }; export async function sendPassword(command: UpdateSessionCommand) { @@ -128,8 +128,7 @@ export async function sendPassword(command: UpdateSessionCommand) { try { session = await createSessionAndUpdateCookie( checks, - undefined, - command.authRequestId, + command.requestId, loginSettings?.passwordCheckLifetime, ); } catch (error: any) { @@ -161,7 +160,7 @@ export async function sendPassword(command: UpdateSessionCommand) { sessionCookie, command.checks, undefined, - command.authRequestId, + command.requestId, loginSettings?.passwordCheckLifetime, ); } catch (error: any) { @@ -228,7 +227,7 @@ export async function sendPassword(command: UpdateSessionCommand) { session, humanUser, command.organization, - command.authRequestId, + command.requestId, ); if (passwordChangedCheck?.redirect) { @@ -245,7 +244,7 @@ export async function sendPassword(command: UpdateSessionCommand) { session, humanUser, command.organization, - command.authRequestId, + command.requestId, ); if (emailVerificationCheck?.redirect) { @@ -269,23 +268,24 @@ export async function sendPassword(command: UpdateSessionCommand) { return { error: "Could not verify password!" }; } - const mfaFactorCheck = checkMFAFactors( + const mfaFactorCheck = await checkMFAFactors( + serviceUrl, session, loginSettings, authMethods, command.organization, - command.authRequestId, + command.requestId, ); if (mfaFactorCheck?.redirect) { return mfaFactorCheck; } - if (command.authRequestId && session.id) { + if (command.requestId && session.id) { const nextUrl = await getNextUrl( { sessionId: session.id, - authRequestId: command.authRequestId, + requestId: command.requestId, organization: command.organization ?? session.factors?.user?.organizationId, }, @@ -435,7 +435,7 @@ export async function checkSessionAndSetPassword({ }, {}, ) - .catch((error) => { + .catch((error: ConnectError) => { console.log(error); if (error.code === 7) { return { error: "Session is not valid." }; diff --git a/apps/login/src/lib/server/register.ts b/apps/login/src/lib/server/register.ts index bd3d2ffc49..36103e9bf9 100644 --- a/apps/login/src/lib/server/register.ts +++ b/apps/login/src/lib/server/register.ts @@ -19,7 +19,7 @@ type RegisterUserCommand = { lastName: string; password?: string; organization?: string; - authRequestId?: string; + requestId?: string; }; export type RegisterUserResponse = { @@ -71,8 +71,7 @@ export async function registerUser(command: RegisterUserCommand) { const session = await createSessionAndUpdateCookie( checks, - undefined, - command.authRequestId, + command.requestId, command.password ? loginSettings?.passwordCheckLifetime : undefined, ); @@ -86,8 +85,8 @@ export async function registerUser(command: RegisterUserCommand) { organization: session.factors.user.organizationId, }); - if (command.authRequestId) { - params.append("authRequestId", command.authRequestId); + if (command.requestId) { + params.append("requestId", command.requestId); } return { redirect: "/passkey/set?" + params }; @@ -111,7 +110,7 @@ export async function registerUser(command: RegisterUserCommand) { session, humanUser, session.factors.user.organizationId, - command.authRequestId, + command.requestId, ); if (emailVerificationCheck?.redirect) { @@ -119,10 +118,10 @@ export async function registerUser(command: RegisterUserCommand) { } const url = await getNextUrl( - command.authRequestId && session.id + command.requestId && session.id ? { sessionId: session.id, - authRequestId: command.authRequestId, + requestId: command.requestId, organization: session.factors.user.organizationId, } : { diff --git a/apps/login/src/lib/server/session.ts b/apps/login/src/lib/server/session.ts index a55b560904..6a1aa5af9b 100644 --- a/apps/login/src/lib/server/session.ts +++ b/apps/login/src/lib/server/session.ts @@ -4,6 +4,7 @@ import { setSessionAndUpdateCookie } from "@/lib/server/cookie"; import { deleteSession, getLoginSettings, + humanMFAInitSkipped, listAuthenticationMethodTypes, } from "@/lib/zitadel"; import { Duration } from "@zitadel/client"; @@ -20,10 +21,57 @@ import { } from "../cookies"; import { getServiceUrlFromHeaders } from "../service"; +export async function skipMFAAndContinueWithNextUrl({ + userId, + requestId, + loginName, + sessionId, + organization, +}: { + userId: string; + loginName?: string; + sessionId?: string; + requestId?: string; + organization?: string; +}) { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + const loginSettings = await getLoginSettings({ + serviceUrl, + organization: organization, + }); + + await humanMFAInitSkipped({ serviceUrl, userId }); + + const url = + requestId && sessionId + ? await getNextUrl( + { + sessionId: sessionId, + requestId: requestId, + organization: organization, + }, + loginSettings?.defaultRedirectUri, + ) + : loginName + ? await getNextUrl( + { + loginName: loginName, + organization: organization, + }, + loginSettings?.defaultRedirectUri, + ) + : null; + if (url) { + return { redirect: url }; + } +} + export async function continueWithSession({ - authRequestId, + requestId, ...session -}: Session & { authRequestId?: string }) { +}: Session & { requestId?: string }) { const _headers = await headers(); const { serviceUrl } = getServiceUrlFromHeaders(_headers); @@ -34,11 +82,11 @@ export async function continueWithSession({ }); const url = - authRequestId && session.id && session.factors?.user + requestId && session.id && session.factors?.user ? await getNextUrl( { sessionId: session.id, - authRequestId: authRequestId, + requestId: requestId, organization: session.factors.user.organizationId, }, loginSettings?.defaultRedirectUri, @@ -62,20 +110,14 @@ export type UpdateSessionCommand = { sessionId?: string; organization?: string; checks?: Checks; - authRequestId?: string; + requestId?: string; challenges?: RequestChallenges; lifetime?: Duration; }; export async function updateSession(options: UpdateSessionCommand) { - let { - loginName, - sessionId, - organization, - checks, - authRequestId, - challenges, - } = options; + let { loginName, sessionId, organization, checks, requestId, challenges } = + options; const recentSession = sessionId ? await getSessionCookieById({ sessionId }) : loginName @@ -123,7 +165,7 @@ export async function updateSession(options: UpdateSessionCommand) { recentSession, checks, challenges, - authRequestId, + requestId, lifetime, ); diff --git a/apps/login/src/lib/server/verify.ts b/apps/login/src/lib/server/verify.ts index 43403d1519..765fda4b78 100644 --- a/apps/login/src/lib/server/verify.ts +++ b/apps/login/src/lib/server/verify.ts @@ -59,7 +59,7 @@ type VerifyUserByEmailCommand = { organization?: string; code: string; isInvite: boolean; - authRequestId?: string; + requestId?: string; }; export async function sendVerification(command: VerifyUserByEmailCommand) { @@ -155,11 +155,7 @@ export async function sendVerification(command: VerifyUserByEmailCommand) { }, }); - session = await createSessionAndUpdateCookie( - checks, - undefined, - command.authRequestId, - ); + session = await createSessionAndUpdateCookie(checks, command.requestId); } if (!session?.factors?.user?.id) { @@ -207,12 +203,13 @@ export async function sendVerification(command: VerifyUserByEmailCommand) { } // redirect to mfa factor if user has one, or redirect to set one up - const mfaFactorCheck = checkMFAFactors( + const mfaFactorCheck = await checkMFAFactors( + serviceUrl, session, loginSettings, authMethodResponse.authMethodTypes, command.organization, - command.authRequestId, + command.requestId, ); if (mfaFactorCheck?.redirect) { @@ -220,11 +217,11 @@ export async function sendVerification(command: VerifyUserByEmailCommand) { } // login user if no additional steps are required - if (command.authRequestId && session.id) { + if (command.requestId && session.id) { const nextUrl = await getNextUrl( { sessionId: session.id, - authRequestId: command.authRequestId, + requestId: command.requestId, organization: command.organization ?? session.factors?.user?.organizationId, }, @@ -248,7 +245,7 @@ export async function sendVerification(command: VerifyUserByEmailCommand) { type resendVerifyEmailCommand = { userId: string; isInvite: boolean; - authRequestId?: string; + requestId?: string; }; export async function resendVerification(command: resendVerifyEmailCommand) { @@ -269,9 +266,7 @@ export async function resendVerification(command: resendVerifyEmailCommand) { serviceUrl, urlTemplate: `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` + - (command.authRequestId - ? `&authRequestId=${command.authRequestId}` - : ""), + (command.requestId ? `&requestId=${command.requestId}` : ""), }); } @@ -292,7 +287,7 @@ export async function sendEmailCode(command: sendEmailCommand) { export type SendVerificationRedirectWithoutCheckCommand = { organization?: string; - authRequestId?: string; + requestId?: string; } & ( | { userId: string; loginName?: never } | { userId?: never; loginName: string } @@ -371,11 +366,7 @@ export async function sendVerificationRedirectWithoutCheck( }, }); - session = await createSessionAndUpdateCookie( - checks, - undefined, - command.authRequestId, - ); + session = await createSessionAndUpdateCookie(checks, command.requestId); } if (!session?.factors?.user?.id) { @@ -418,17 +409,17 @@ export async function sendVerificationRedirectWithoutCheck( const loginSettings = await getLoginSettings({ serviceUrl, - organization: user.details?.resourceOwner, }); // redirect to mfa factor if user has one, or redirect to set one up - const mfaFactorCheck = checkMFAFactors( + const mfaFactorCheck = await checkMFAFactors( + serviceUrl, session, loginSettings, authMethodResponse.authMethodTypes, command.organization, - command.authRequestId, + command.requestId, ); if (mfaFactorCheck?.redirect) { @@ -436,11 +427,11 @@ export async function sendVerificationRedirectWithoutCheck( } // login user if no additional steps are required - if (command.authRequestId && session.id) { + if (command.requestId && session.id) { const nextUrl = await getNextUrl( { sessionId: session.id, - authRequestId: command.authRequestId, + requestId: command.requestId, organization: command.organization ?? session.factors?.user?.organizationId, }, diff --git a/apps/login/src/lib/service.ts b/apps/login/src/lib/service.ts index 4f5f5a0cec..b6d320c0f4 100644 --- a/apps/login/src/lib/service.ts +++ b/apps/login/src/lib/service.ts @@ -3,6 +3,7 @@ import { createServerTransport } from "@zitadel/client/node"; import { IdentityProviderService } from "@zitadel/proto/zitadel/idp/v2/idp_service_pb"; import { OIDCService } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb"; import { OrganizationService } from "@zitadel/proto/zitadel/org/v2/org_service_pb"; +import { SAMLService } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb"; import { SessionService } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb"; import { UserService } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; @@ -15,7 +16,8 @@ type ServiceClass = | typeof OrganizationService | typeof SessionService | typeof OIDCService - | typeof SettingsService; + | typeof SettingsService + | typeof SAMLService; export async function createServiceForHost( service: T, diff --git a/apps/login/src/lib/session.ts b/apps/login/src/lib/session.ts index b734bf2b2c..f9eb0ceeb2 100644 --- a/apps/login/src/lib/session.ts +++ b/apps/login/src/lib/session.ts @@ -1,7 +1,15 @@ +import { timestampDate } from "@zitadel/client"; +import { AuthRequest } from "@zitadel/proto/zitadel/oidc/v2/authorization_pb"; +import { SAMLRequest } from "@zitadel/proto/zitadel/saml/v2/authorization_pb"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { GetSessionResponse } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; +import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { getMostRecentCookieWithLoginname } from "./cookies"; -import { getSession } from "./zitadel"; +import { + getLoginSettings, + getSession, + listAuthenticationMethodTypes, +} from "./zitadel"; type LoadMostRecentSessionParams = { serviceUrl: string; @@ -29,3 +37,160 @@ export async function loadMostRecentSession({ sessionToken: recent.token, }).then((resp: GetSessionResponse) => resp.session); } + +/** + * mfa is required, session is not valid anymore (e.g. session expired, user logged out, etc.) + * to check for mfa for automatically selected session -> const response = await listAuthenticationMethodTypes(userId); + **/ +export async function isSessionValid({ + serviceUrl, + session, +}: { + serviceUrl: string; + session: Session; +}): Promise { + // 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 { + 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; +} diff --git a/apps/login/src/lib/verify-helper.ts b/apps/login/src/lib/verify-helper.ts index 053d1cc71f..704d7bbef6 100644 --- a/apps/login/src/lib/verify-helper.ts +++ b/apps/login/src/lib/verify-helper.ts @@ -5,13 +5,14 @@ import { PasswordExpirySettings } from "@zitadel/proto/zitadel/settings/v2/passw import { HumanUser } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import moment from "moment"; +import { getUserByID } from "./zitadel"; export function checkPasswordChangeRequired( expirySettings: PasswordExpirySettings | undefined, session: Session, humanUser: HumanUser | undefined, organization?: string, - authRequestId?: string, + requestId?: string, ) { let isOutdated = false; if (expirySettings?.maxAgeDays && humanUser?.passwordChanged) { @@ -35,8 +36,8 @@ export function checkPasswordChangeRequired( ); } - if (authRequestId) { - params.append("authRequestId", authRequestId); + if (requestId) { + params.append("requestId", requestId); } return { redirect: "/password/change?" + params }; @@ -47,7 +48,7 @@ export function checkInvite( session: Session, humanUser?: HumanUser, organization?: string, - authRequestId?: string, + requestId?: string, ) { if (!humanUser?.email?.isVerified) { const paramsVerify = new URLSearchParams({ @@ -63,8 +64,8 @@ export function checkInvite( ); } - if (authRequestId) { - paramsVerify.append("authRequestId", authRequestId); + if (requestId) { + paramsVerify.append("requestId", requestId); } return { redirect: "/verify?" + paramsVerify }; @@ -75,7 +76,7 @@ export function checkEmailVerification( session: Session, humanUser?: HumanUser, organization?: string, - authRequestId?: string, + requestId?: string, ) { if ( !humanUser?.email?.isVerified && @@ -85,8 +86,8 @@ export function checkEmailVerification( loginName: session.factors?.user?.loginName as string, }); - if (authRequestId) { - params.append("authRequestId", authRequestId); + if (requestId) { + params.append("requestId", requestId); } if (organization || session.factors?.user?.organizationId) { @@ -100,12 +101,13 @@ export function checkEmailVerification( } } -export function checkMFAFactors( +export async function checkMFAFactors( + serviceUrl: string, session: Session, loginSettings: LoginSettings | undefined, authMethods: AuthenticationMethodType[], organization?: string, - authRequestId?: string, + requestId?: string, ) { const availableMultiFactors = authMethods?.filter( (m: AuthenticationMethodType) => @@ -128,8 +130,8 @@ export function checkMFAFactors( loginName: session.factors?.user?.loginName as string, }); - if (authRequestId) { - params.append("authRequestId", authRequestId); + if (requestId) { + params.append("requestId", requestId); } if (organization || session.factors?.user?.organizationId) { @@ -155,8 +157,8 @@ export function checkMFAFactors( loginName: session.factors?.user?.loginName as string, }); - if (authRequestId) { - params.append("authRequestId", authRequestId); + if (requestId) { + params.append("requestId", requestId); } if (organization || session.factors?.user?.organizationId) { @@ -177,8 +179,62 @@ export function checkMFAFactors( checkAfter: "true", // this defines if the check is directly made after the setup }); - if (authRequestId) { - params.append("authRequestId", authRequestId); + if (requestId) { + params.append("requestId", requestId); + } + + if (organization || session.factors?.user?.organizationId) { + params.append( + "organization", + organization ?? (session.factors?.user?.organizationId as string), + ); + } + + // TODO: provide a way to setup passkeys on mfa page? + return { redirect: `/mfa/set?` + params }; + } else if ( + loginSettings?.mfaInitSkipLifetime && + (loginSettings.mfaInitSkipLifetime.nanos > 0 || + loginSettings.mfaInitSkipLifetime.seconds > 0) && + !availableMultiFactors.length && + session?.factors?.user?.id + ) { + const userResponse = await getUserByID({ + serviceUrl, + userId: session.factors?.user?.id, + }); + + const humanUser = + userResponse?.user?.type.case === "human" + ? userResponse?.user.type.value + : undefined; + + if (humanUser?.mfaInitSkipped) { + const mfaInitSkippedTimestamp = timestampDate(humanUser.mfaInitSkipped); + + const mfaInitSkipLifetimeMillis = + Number(loginSettings.mfaInitSkipLifetime.seconds) * 1000 + + loginSettings.mfaInitSkipLifetime.nanos / 1000000; + const currentTime = Date.now(); + const mfaInitSkippedTime = mfaInitSkippedTimestamp.getTime(); + const timeDifference = currentTime - mfaInitSkippedTime; + + if (!(timeDifference > mfaInitSkipLifetimeMillis)) { + // if the time difference is smaller than the lifetime, skip the mfa setup + return; + } + } + + // the user has never skipped the mfa init but we have a setting so we redirect + + const params = new URLSearchParams({ + loginName: session.factors?.user?.loginName as string, + force: "false", // this defines if the mfa is not forced in the settings and can be skipped + checkAfter: "true", // this defines if the check is directly made after the setup + }); + + if (requestId) { + params.append("requestId", requestId); } if (organization || session.factors?.user?.organizationId) { @@ -191,28 +247,4 @@ export function checkMFAFactors( // TODO: provide a way to setup passkeys on mfa page? return { redirect: `/mfa/set?` + params }; } - - // TODO: implement passkey setup - - // else if ( - // submitted.factors && - // !submitted.factors.webAuthN && // if session was not verified with a passkey - // promptPasswordless && // if explicitly prompted due policy - // !isAlternative // escaped if password was used as an alternative method - // ) { - // const params = new URLSearchParams({ - // loginName: submitted.factors.user.loginName, - // prompt: "true", - // }); - - // if (authRequestId) { - // params.append("authRequestId", authRequestId); - // } - - // if (organization) { - // params.append("organization", organization); - // } - - // return router.push(`/passkey/set?` + params); - // } } diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index f4b062f5dd..5e3fdec323 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -8,6 +8,10 @@ import { } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb"; import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; import { OrganizationService } from "@zitadel/proto/zitadel/org/v2/org_service_pb"; +import { + CreateResponseRequest, + SAMLService, +} from "@zitadel/proto/zitadel/saml/v2/saml_service_pb"; import { RequestChallenges } from "@zitadel/proto/zitadel/session/v2/challenge_pb"; import { Checks, @@ -55,11 +59,9 @@ async function cacheWrapper(callback: Promise) { export async function getBrandingSettings({ serviceUrl, - organization, }: { serviceUrl: string; - organization?: string; }) { const settingsService: Client = @@ -74,11 +76,9 @@ export async function getBrandingSettings({ export async function getLoginSettings({ serviceUrl, - organization, }: { serviceUrl: string; - organization?: string; }) { const settingsService: Client = @@ -93,11 +93,9 @@ export async function getLoginSettings({ export async function getLockoutSettings({ serviceUrl, - orgId, }: { serviceUrl: string; - orgId?: string; }) { const settingsService: Client = @@ -112,11 +110,9 @@ export async function getLockoutSettings({ export async function getPasswordExpirySettings({ serviceUrl, - orgId, }: { serviceUrl: string; - orgId?: string; }) { const settingsService: Client = @@ -131,11 +127,9 @@ export async function getPasswordExpirySettings({ export async function listIDPLinks({ serviceUrl, - userId, }: { serviceUrl: string; - userId: string; }) { const userService: Client = await createServiceForHost( @@ -148,11 +142,9 @@ export async function listIDPLinks({ export async function addOTPEmail({ serviceUrl, - userId, }: { serviceUrl: string; - userId: string; }) { const userService: Client = await createServiceForHost( @@ -165,11 +157,9 @@ export async function addOTPEmail({ export async function addOTPSMS({ serviceUrl, - userId, }: { serviceUrl: string; - userId: string; }) { const userService: Client = await createServiceForHost( @@ -182,11 +172,9 @@ export async function addOTPSMS({ export async function registerTOTP({ serviceUrl, - userId, }: { serviceUrl: string; - userId: string; }) { const userService: Client = await createServiceForHost( @@ -214,11 +202,9 @@ export async function getGeneralSettings({ export async function getLegalAndSupportSettings({ serviceUrl, - organization, }: { serviceUrl: string; - organization?: string; }) { const settingsService: Client = @@ -233,11 +219,9 @@ export async function getLegalAndSupportSettings({ export async function getPasswordComplexitySettings({ serviceUrl, - organization, }: { serviceUrl: string; - organization?: string; }) { const settingsService: Client = @@ -252,32 +236,26 @@ export async function getPasswordComplexitySettings({ export async function createSessionFromChecks({ serviceUrl, - checks, - challenges, lifetime, }: { serviceUrl: string; - checks: Checks; - challenges: RequestChallenges | undefined; lifetime?: Duration; }) { const sessionService: Client = await createServiceForHost(SessionService, serviceUrl); - return sessionService.createSession({ checks, challenges, lifetime }, {}); + return sessionService.createSession({ checks, lifetime }, {}); } export async function createSessionForUserIdAndIdpIntent({ serviceUrl, - userId, idpIntent, lifetime, }: { serviceUrl: string; - userId: string; idpIntent: { idpIntentId?: string | undefined; @@ -304,7 +282,6 @@ export async function createSessionForUserIdAndIdpIntent({ export async function setSession({ serviceUrl, - sessionId, sessionToken, challenges, @@ -312,7 +289,6 @@ export async function setSession({ lifetime, }: { serviceUrl: string; - sessionId: string; sessionToken: string; challenges: RequestChallenges | undefined; @@ -337,12 +313,10 @@ export async function setSession({ export async function getSession({ serviceUrl, - sessionId, sessionToken, }: { serviceUrl: string; - sessionId: string; sessionToken: string; }) { @@ -354,12 +328,10 @@ export async function getSession({ export async function deleteSession({ serviceUrl, - sessionId, sessionToken, }: { serviceUrl: string; - sessionId: string; sessionToken: string; }) { @@ -371,7 +343,6 @@ export async function deleteSession({ type ListSessionsCommand = { serviceUrl: string; - ids: string[]; }; @@ -400,7 +371,6 @@ export async function listSessions({ export type AddHumanUserData = { serviceUrl: string; - firstName: string; lastName: string; email: string; @@ -410,7 +380,6 @@ export type AddHumanUserData = { export async function addHumanUser({ serviceUrl, - email, firstName, lastName, @@ -443,11 +412,9 @@ export async function addHumanUser({ export async function addHuman({ serviceUrl, - request, }: { serviceUrl: string; - request: AddHumanUserRequest; }) { const userService: Client = await createServiceForHost( @@ -460,12 +427,10 @@ export async function addHuman({ export async function verifyTOTPRegistration({ serviceUrl, - code, userId, }: { serviceUrl: string; - code: string; userId: string; }) { @@ -479,11 +444,9 @@ export async function verifyTOTPRegistration({ export async function getUserByID({ serviceUrl, - userId, }: { serviceUrl: string; - userId: string; }) { const userService: Client = await createServiceForHost( @@ -494,14 +457,27 @@ export async function getUserByID({ return userService.getUserByID({ userId }, {}); } +export async function humanMFAInitSkipped({ + serviceUrl, + userId, +}: { + serviceUrl: string; + userId: string; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + ); + + return userService.humanMFAInitSkipped({ userId }, {}); +} + export async function verifyInviteCode({ serviceUrl, - userId, verificationCode, }: { serviceUrl: string; - userId: string; verificationCode: string; }) { @@ -515,11 +491,9 @@ export async function verifyInviteCode({ export async function resendInviteCode({ serviceUrl, - userId, }: { serviceUrl: string; - userId: string; }) { const userService: Client = await createServiceForHost( @@ -532,12 +506,10 @@ export async function resendInviteCode({ export async function sendEmailCode({ serviceUrl, - userId, urlTemplate, }: { serviceUrl: string; - userId: string; urlTemplate: string; }) { @@ -563,12 +535,10 @@ export async function sendEmailCode({ export async function createInviteCode({ serviceUrl, - urlTemplate, userId, }: { serviceUrl: string; - urlTemplate: string; userId: string; }) { @@ -600,7 +570,6 @@ export async function createInviteCode({ export type ListUsersCommand = { serviceUrl: string; - loginName?: string; userName?: string; email?: string; @@ -610,7 +579,6 @@ export type ListUsersCommand = { export async function listUsers({ serviceUrl, - loginName, userName, phone, @@ -709,7 +677,6 @@ export async function listUsers({ export type SearchUsersCommand = { serviceUrl: string; - searchValue: string; loginSettings: LoginSettings; organizationId?: string; @@ -755,7 +722,6 @@ const EmailQuery = (searchValue: string) => * */ export async function searchUsers({ serviceUrl, - searchValue, loginSettings, organizationId, @@ -900,11 +866,9 @@ export async function getDefaultOrg({ export async function getOrgsByDomain({ serviceUrl, - domain, }: { serviceUrl: string; - domain: string; }) { const orgService: Client = @@ -927,7 +891,6 @@ export async function getOrgsByDomain({ export async function startIdentityProviderFlow({ serviceUrl, - idpId, urls, }: { @@ -952,7 +915,6 @@ export async function startIdentityProviderFlow({ export async function retrieveIdentityProviderInformation({ serviceUrl, - idpIntentId, idpIntentToken, }: { @@ -974,11 +936,9 @@ export async function retrieveIdentityProviderInformation({ export async function getAuthRequest({ serviceUrl, - authRequestId, }: { serviceUrl: string; - authRequestId: string; }) { const oidcService = await createServiceForHost(OIDCService, serviceUrl); @@ -990,11 +950,9 @@ export async function getAuthRequest({ export async function createCallback({ serviceUrl, - req, }: { serviceUrl: string; - req: CreateCallbackRequest; }) { const oidcService = await createServiceForHost(OIDCService, serviceUrl); @@ -1002,14 +960,38 @@ export async function createCallback({ return oidcService.createCallback(req); } +export async function getSAMLRequest({ + serviceUrl, + samlRequestId, +}: { + serviceUrl: string; + samlRequestId: string; +}) { + const samlService = await createServiceForHost(SAMLService, serviceUrl); + + return samlService.getSAMLRequest({ + samlRequestId, + }); +} + +export async function createResponse({ + serviceUrl, + req, +}: { + serviceUrl: string; + req: CreateResponseRequest; +}) { + const samlService = await createServiceForHost(SAMLService, serviceUrl); + + return samlService.createResponse(req); +} + export async function verifyEmail({ serviceUrl, - userId, verificationCode, }: { serviceUrl: string; - userId: string; verificationCode: string; }) { @@ -1029,12 +1011,10 @@ export async function verifyEmail({ export async function resendEmailCode({ serviceUrl, - userId, urlTemplate, }: { serviceUrl: string; - userId: string; urlTemplate: string; }) { @@ -1058,12 +1038,10 @@ export async function resendEmailCode({ export async function retrieveIDPIntent({ serviceUrl, - id, token, }: { serviceUrl: string; - id: string; token: string; }) { @@ -1080,11 +1058,9 @@ export async function retrieveIDPIntent({ export async function getIDPByID({ serviceUrl, - id, }: { serviceUrl: string; - id: string; }) { const idpService: Client = @@ -1095,12 +1071,10 @@ export async function getIDPByID({ export async function addIDPLink({ serviceUrl, - idp, userId, }: { serviceUrl: string; - idp: { id: string; userId: string; userName: string }; userId: string; }) { @@ -1124,12 +1098,10 @@ export async function addIDPLink({ export async function passwordReset({ serviceUrl, - userId, urlTemplate, }: { serviceUrl: string; - userId: string; urlTemplate?: string; }) { @@ -1161,14 +1133,12 @@ export async function passwordReset({ export async function setUserPassword({ serviceUrl, - userId, password, user, code, }: { serviceUrl: string; - userId: string; password: string; user: User; @@ -1224,11 +1194,9 @@ export async function setUserPassword({ export async function setPassword({ serviceUrl, - payload, }: { serviceUrl: string; - payload: SetPasswordRequest; }) { const userService: Client = await createServiceForHost( @@ -1247,11 +1215,9 @@ export async function setPassword({ */ export async function createPasskeyRegistrationLink({ serviceUrl, - userId, }: { serviceUrl: string; - userId: string; }) { const userService: Client = await createServiceForHost( @@ -1277,12 +1243,10 @@ export async function createPasskeyRegistrationLink({ */ export async function registerU2F({ serviceUrl, - userId, domain, }: { serviceUrl: string; - userId: string; domain: string; }) { @@ -1305,11 +1269,9 @@ export async function registerU2F({ */ export async function verifyU2FRegistration({ serviceUrl, - request, }: { serviceUrl: string; - request: VerifyU2FRegistrationRequest; }) { const userService: Client = await createServiceForHost( @@ -1329,12 +1291,10 @@ export async function verifyU2FRegistration({ */ export async function getActiveIdentityProviders({ serviceUrl, - orgId, linking_allowed, }: { serviceUrl: string; - orgId?: string; linking_allowed?: boolean; }) { @@ -1356,11 +1316,9 @@ export async function getActiveIdentityProviders({ */ export async function verifyPasskeyRegistration({ serviceUrl, - request, }: { serviceUrl: string; - request: VerifyPasskeyRegistrationRequest; }) { const userService: Client = await createServiceForHost( @@ -1381,13 +1339,11 @@ export async function verifyPasskeyRegistration({ */ export async function registerPasskey({ serviceUrl, - userId, code, domain, }: { serviceUrl: string; - userId: string; code: { id: string; code: string }; domain: string; @@ -1415,7 +1371,6 @@ export async function listAuthenticationMethodTypes({ userId, }: { serviceUrl: string; - userId: string; }) { const userService: Client = await createServiceForHost( diff --git a/packages/zitadel-client/src/v2.ts b/packages/zitadel-client/src/v2.ts index 6ada77c7c5..49cf901734 100644 --- a/packages/zitadel-client/src/v2.ts +++ b/packages/zitadel-client/src/v2.ts @@ -4,6 +4,7 @@ import { IdentityProviderService } from "@zitadel/proto/zitadel/idp/v2/idp_servi import { RequestContextSchema } from "@zitadel/proto/zitadel/object/v2/object_pb.js"; import { OIDCService } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb.js"; import { OrganizationService } from "@zitadel/proto/zitadel/org/v2/org_service_pb.js"; +import { SAMLService } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb.js"; import { SessionService } from "@zitadel/proto/zitadel/session/v2/session_service_pb.js"; import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb.js"; import { UserService } from "@zitadel/proto/zitadel/user/v2/user_service_pb.js"; @@ -14,6 +15,7 @@ export const createUserServiceClient = createClientFor(UserService); export const createSettingsServiceClient = createClientFor(SettingsService); export const createSessionServiceClient = createClientFor(SessionService); export const createOIDCServiceClient = createClientFor(OIDCService); +export const createSAMLServiceClient = createClientFor(SAMLService); export const createOrganizationServiceClient = createClientFor(OrganizationService); export const createFeatureServiceClient = createClientFor(FeatureService); export const createIdpServiceClient = createClientFor(IdentityProviderService);