From 6fad38ec939a9f6baf4e018fa513187476ce02ed Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 4 Feb 2025 09:38:33 +0100 Subject: [PATCH 01/19] helpers --- apps/login/src/app/login/route.ts | 832 ++++++++++++------------------ apps/login/src/lib/oidc.ts | 136 +++++ apps/login/src/lib/saml.ts | 134 +++++ apps/login/src/lib/service.ts | 4 +- apps/login/src/lib/session.ts | 166 +++++- apps/login/src/lib/zitadel.ts | 22 + packages/zitadel-client/src/v2.ts | 2 + 7 files changed, 785 insertions(+), 511 deletions(-) create mode 100644 apps/login/src/lib/oidc.ts create mode 100644 apps/login/src/lib/saml.ts diff --git a/apps/login/src/app/login/route.ts b/apps/login/src/app/login/route.ts index 2cea4b3b0b..ad89312fcf 100644 --- a/apps/login/src/app/login/route.ts +++ b/apps/login/src/app/login/route.ts @@ -1,18 +1,19 @@ 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, getActiveIdentityProviders, getAuthRequest, - getLoginSettings, getOrgsByDomain, - listAuthenticationMethodTypes, listSessions, startIdentityProviderFlow, } from "@/lib/zitadel"; -import { create, timestampDate } from "@zitadel/client"; +import { create } from "@zitadel/client"; import { AuthRequest, Prompt, @@ -22,7 +23,6 @@ import { SessionSchema, } from "@zitadel/proto/zitadel/oidc/v2/oidc_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,6 +30,29 @@ export const dynamic = "force-dynamic"; export const revalidate = false; export const fetchCache = "default-no-store"; +const gotoAccounts = ({ + request, + authRequest, + organization, + idPrefix, +}: { + request: NextRequest; + authRequest: AuthRequest; + organization: string; + idPrefix: string; +}): NextResponse => { + const accountsUrl = new URL("/accounts", request.url); + + if (authRequest?.id) { + accountsUrl.searchParams.set("requestId", `${idPrefix}${authRequest.id}`); + } + if (organization) { + accountsUrl.searchParams.set("organization", organization); + } + + return NextResponse.redirect(accountsUrl); +}; + async function loadSessions({ serviceUrl, serviceRegion, @@ -52,162 +75,19 @@ 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, - serviceRegion: 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, - serviceRegion, - 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, - serviceRegion, - 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, - serviceRegion: 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, serviceRegion, session)) { - return session; - } - } - - return undefined; -} - 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, serviceRegion } = getServiceUrlFromHeaders(_headers); + const searchParams = request.nextUrl.searchParams; + + const oidcRequestId = searchParams.get("authRequest"); // oidc initiated request + const samlRequestId = searchParams.get("samlRequest"); // saml initiated request + + const requestId = searchParams.get("requestId"); // internal request id which combines authRequest and samlRequest with the prefix oidc_ or saml_ + + 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) { @@ -221,402 +101,336 @@ export async function GET(request: NextRequest) { sessions = await loadSessions({ serviceUrl, serviceRegion, 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( + if (requestId && sessionId) { + if (requestId.startsWith("oidc_")) { + // this finishes the login process for OIDC + await loginWithOIDCandSession({ serviceUrl, serviceRegion, - selectedSession, - ); + authRequest: requestId.replace("oidc_", ""), + sessionId, + sessions, + sessionCookies, + request, + }); + } else if (requestId.startsWith("saml_")) { + // this finishes the login process for SAML + await loginWithSAMLandSession({ + serviceUrl, + serviceRegion, + samlRequest: requestId.replace("saml_", ""), + sessionId, + sessions, + sessionCookies, + request, + }); + } - console.log("Session is valid:", isValid); + if (requestId) { + const { authRequest } = await getAuthRequest({ + serviceUrl, + serviceRegion, + authRequestId: requestId, + }); - 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, - }; + let organization = ""; + let suffix = ""; + let idpId = ""; - const res = await sendLoginname(command); + if (authRequest?.scope) { + const orgScope = authRequest.scope.find((s: string) => + ORG_SCOPE_REGEX.test(s), + ); - if (res && "redirect" in res && res?.redirect) { - const absoluteUrl = new URL(res.redirect, request.url); - return NextResponse.redirect(absoluteUrl.toString()); + const idpScope = authRequest.scope.find((s: string) => + IDP_SCOPE_REGEX.test(s), + ); + + if (orgScope) { + const matched = ORG_SCOPE_REGEX.exec(orgScope); + organization = matched?.[1] ?? ""; + } else { + const orgDomainScope = authRequest.scope.find((s: string) => + ORG_DOMAIN_SCOPE_REGEX.test(s), + ); + + if (orgDomainScope) { + const matched = ORG_DOMAIN_SCOPE_REGEX.exec(orgDomainScope); + const orgDomain = matched?.[1] ?? ""; + if (orgDomain) { + const orgs = await getOrgsByDomain({ + serviceUrl, + serviceRegion, + domain: orgDomain, + }); + if (orgs.result && orgs.result.length === 1) { + organization = orgs.result[0].id ?? ""; + suffix = orgDomain; + } + } + } + } + + if (idpScope) { + const matched = IDP_SCOPE_REGEX.exec(idpScope); + idpId = matched?.[1] ?? ""; + + const identityProviders = await getActiveIdentityProviders({ + serviceUrl, + serviceRegion, + orgId: organization ? organization : undefined, + }).then((resp) => { + return resp.identityProviders; + }); + + const idp = identityProviders.find((idp) => idp.id === idpId); + + if (idp) { + const origin = request.nextUrl.origin; + + const identityProviderType = identityProviders[0].type; + let provider = idpTypeToSlug(identityProviderType); + + const params = new URLSearchParams(); + + if (requestId) { + params.set("requestId", `oidc_${requestId}`); + } + + if (organization) { + params.set("organization", organization); + } + + return startIdentityProviderFlow({ + serviceUrl, + serviceRegion, + idpId, + urls: { + successUrl: + `${origin}/idp/${provider}/success?` + + new URLSearchParams(params), + failureUrl: + `${origin}/idp/${provider}/failure?` + + new URLSearchParams(params), + }, + }).then((resp) => { + if ( + resp.nextStep.value && + typeof resp.nextStep.value === "string" + ) { + return NextResponse.redirect(resp.nextStep.value); + } + }); + } } } - const cookie = sessionCookies.find( - (cookie) => cookie.id === selectedSession?.id, - ); + if (authRequest && authRequest.prompt.includes(Prompt.CREATE)) { + const registerUrl = new URL("/register", request.url); + if (authRequest.id) { + registerUrl.searchParams.set("requestId", `oidc_${authRequest.id}`); + } + if (organization) { + registerUrl.searchParams.set("organization", organization); + } - if (cookie && cookie.id && cookie.token) { - const session = { - sessionId: cookie?.id, - sessionToken: cookie?.token, - }; + 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({ + request, + authRequest, + organization, + idPrefix: "oidc_", + }); + } 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 + */ + + // if a hint is provided, skip loginname page and jump to the next page + if (authRequest.loginHint) { + try { + let command: SendLoginnameCommand = { + loginName: authRequest.loginHint, + authRequestId: authRequest.id, + }; + + if (organization) { + command = { ...command, organization }; + } + + 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()); + } + } catch (error) { + console.error("Failed to execute sendLoginname:", error); + } + } + + const loginNameUrl = new URL("/loginname", request.url); + if (authRequest.id) { + loginNameUrl.searchParams.set( + "requestId", + `oidc_${authRequest.id}`, + ); + } + if (authRequest.loginHint) { + loginNameUrl.searchParams.set("loginName", authRequest.loginHint); + } + if (organization) { + loginNameUrl.searchParams.set("organization", organization); + } + if (suffix) { + loginNameUrl.searchParams.set("suffix", suffix); + } + 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({ + serviceUrl, + serviceRegion, + sessions, + authRequest, + }); + + if (!selectedSession || !selectedSession.id) { + return NextResponse.json( + { error: "No active session found" }, + { status: 400 }, + ); + } + + const cookie = sessionCookies.find( + (cookie) => cookie.id === selectedSession.id, + ); + + if (!cookie || !cookie.id || !cookie.token) { + return NextResponse.json( + { error: "No active session found" }, + { status: 400 }, + ); + } + + const session = { + sessionId: cookie.id, + sessionToken: cookie.token, + }; - // works not with _rsc request - try { const { callbackUrl } = await createCallback({ serviceUrl, serviceRegion, req: create(CreateCallbackRequestSchema, { - authRequestId, + authRequestId: requestId, 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, - serviceRegion, - 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 }); - } - } - } - } - } - - if (authRequestId) { - const { authRequest } = await getAuthRequest({ - serviceUrl, - serviceRegion, - authRequestId, - }); - - let organization = ""; - let suffix = ""; - let idpId = ""; - - if (authRequest?.scope) { - const orgScope = authRequest.scope.find((s: string) => - ORG_SCOPE_REGEX.test(s), - ); - - const idpScope = authRequest.scope.find((s: string) => - IDP_SCOPE_REGEX.test(s), - ); - - if (orgScope) { - const matched = ORG_SCOPE_REGEX.exec(orgScope); - organization = matched?.[1] ?? ""; - } else { - const orgDomainScope = authRequest.scope.find((s: string) => - ORG_DOMAIN_SCOPE_REGEX.test(s), - ); - - if (orgDomainScope) { - const matched = ORG_DOMAIN_SCOPE_REGEX.exec(orgDomainScope); - const orgDomain = matched?.[1] ?? ""; - if (orgDomain) { - const orgs = await getOrgsByDomain({ - serviceUrl, - serviceRegion, - domain: orgDomain, - }); - if (orgs.result && orgs.result.length === 1) { - organization = orgs.result[0].id ?? ""; - suffix = orgDomain; - } - } - } - } - - if (idpScope) { - const matched = IDP_SCOPE_REGEX.exec(idpScope); - idpId = matched?.[1] ?? ""; - - const identityProviders = await getActiveIdentityProviders({ - serviceUrl, - serviceRegion, - orgId: organization ? organization : undefined, - }).then((resp) => { - return resp.identityProviders; - }); - - const idp = identityProviders.find((idp) => idp.id === idpId); - - if (idp) { - const origin = request.nextUrl.origin; - - const identityProviderType = identityProviders[0].type; - let provider = idpTypeToSlug(identityProviderType); - - const params = new URLSearchParams(); - - if (authRequestId) { - params.set("authRequestId", authRequestId); - } - - if (organization) { - params.set("organization", organization); - } - - return startIdentityProviderFlow({ + return NextResponse.redirect(callbackUrl); + } else { + // check for loginHint, userId hint and valid sessions + let selectedSession = await findValidSession({ serviceUrl, serviceRegion, - idpId, - urls: { - successUrl: - `${origin}/idp/${provider}/success?` + - new URLSearchParams(params), - failureUrl: - `${origin}/idp/${provider}/failure?` + - new URLSearchParams(params), - }, - }).then((resp) => { - if ( - resp.nextStep.value && - typeof resp.nextStep.value === "string" - ) { - return NextResponse.redirect(resp.nextStep.value); - } + sessions, + authRequest, }); - } - } - } - const gotoAccounts = (): NextResponse => { - const accountsUrl = new URL("/accounts", request.url); - if (authRequest?.id) { - accountsUrl.searchParams.set("authRequestId", authRequest?.id); - } - if (organization) { - accountsUrl.searchParams.set("organization", organization); - } + if (!selectedSession || !selectedSession.id) { + return gotoAccounts({ + request, + authRequest, + organization, + idPrefix: "oidc_", + }); + } - return NextResponse.redirect(accountsUrl); - }; + const cookie = sessionCookies.find( + (cookie) => cookie.id === selectedSession.id, + ); - if (authRequest && authRequest.prompt.includes(Prompt.CREATE)) { - const registerUrl = new URL("/register", request.url); - if (authRequest.id) { - registerUrl.searchParams.set("authRequestId", authRequest.id); - } - if (organization) { - registerUrl.searchParams.set("organization", organization); - } + if (!cookie || !cookie.id || !cookie.token) { + return gotoAccounts({ + request, + authRequest, + organization, + idPrefix: "oidc_", + }); + } - return NextResponse.redirect(registerUrl); - } + const session = { + sessionId: cookie.id, + sessionToken: cookie.token, + }; - // 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(); - } 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 - */ - - // if a hint is provided, skip loginname page and jump to the next page - if (authRequest.loginHint) { try { - let command: SendLoginnameCommand = { - loginName: authRequest.loginHint, - authRequestId: authRequest.id, - }; - - if (organization) { - command = { ...command, organization }; - } - - 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 { callbackUrl } = await createCallback({ + serviceUrl, + serviceRegion, + req: create(CreateCallbackRequestSchema, { + authRequestId: requestId, + callbackKind: { + case: "session", + value: create(SessionSchema, session), + }, + }), + }); + if (callbackUrl) { + return NextResponse.redirect(callbackUrl); + } else { + console.log( + "could not create callback, redirect user to choose other account", + ); + return gotoAccounts({ + request, + authRequest, + organization, + idPrefix: "oidc_", + }); } } catch (error) { - console.error("Failed to execute sendLoginname:", error); + console.error(error); + return gotoAccounts({ + request, + authRequest, + organization, + idPrefix: "oidc_", + }); } } - + } else { const loginNameUrl = new URL("/loginname", request.url); - if (authRequest.id) { - loginNameUrl.searchParams.set("authRequestId", authRequest.id); - } - if (authRequest.loginHint) { + + loginNameUrl.searchParams.set("requestId", `oidc_${requestId}`); + if (authRequest?.loginHint) { loginNameUrl.searchParams.set("loginName", authRequest.loginHint); + loginNameUrl.searchParams.set("submit", "true"); // autosubmit } + if (organization) { loginNameUrl.searchParams.set("organization", organization); } - if (suffix) { - loginNameUrl.searchParams.set("suffix", suffix); - } + 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( - serviceUrl, - serviceRegion, - sessions, - authRequest, - ); - - if (!selectedSession || !selectedSession.id) { - return NextResponse.json( - { error: "No active session found" }, - { status: 400 }, - ); - } - - const cookie = sessionCookies.find( - (cookie) => cookie.id === selectedSession.id, - ); - - if (!cookie || !cookie.id || !cookie.token) { - return NextResponse.json( - { error: "No active session found" }, - { status: 400 }, - ); - } - - const session = { - sessionId: cookie.id, - sessionToken: cookie.token, - }; - - const { callbackUrl } = await createCallback({ - serviceUrl, - serviceRegion, - req: create(CreateCallbackRequestSchema, { - authRequestId, - callbackKind: { - case: "session", - value: create(SessionSchema, session), - }, - }), - }); - return NextResponse.redirect(callbackUrl); - } else { - // check for loginHint, userId hint and valid sessions - let selectedSession = await findValidSession( - serviceUrl, - serviceRegion, - sessions, - authRequest, - ); - - if (!selectedSession || !selectedSession.id) { - return gotoAccounts(); - } - - const cookie = sessionCookies.find( - (cookie) => cookie.id === selectedSession.id, - ); - - if (!cookie || !cookie.id || !cookie.token) { - return gotoAccounts(); - } - - const session = { - sessionId: cookie.id, - sessionToken: cookie.token, - }; - - try { - const { callbackUrl } = await createCallback({ - serviceUrl, - serviceRegion, - req: create(CreateCallbackRequestSchema, { - authRequestId, - callbackKind: { - case: "session", - value: create(SessionSchema, session), - }, - }), - }); - if (callbackUrl) { - return NextResponse.redirect(callbackUrl); - } else { - console.log( - "could not create callback, redirect user to choose other account", - ); - return gotoAccounts(); - } - } catch (error) { - console.error(error); - return gotoAccounts(); - } } } else { - const loginNameUrl = new URL("/loginname", request.url); - - loginNameUrl.searchParams.set("authRequestId", authRequestId); - if (authRequest?.loginHint) { - loginNameUrl.searchParams.set("loginName", authRequest.loginHint); - loginNameUrl.searchParams.set("submit", "true"); // autosubmit - } - - if (organization) { - loginNameUrl.searchParams.set("organization", organization); - } - - return NextResponse.redirect(loginNameUrl); + return NextResponse.json( + { error: "No authRequest nor samlRequest provided" }, + { status: 500 }, + ); } - } else { - return NextResponse.json( - { error: "No authRequestId provided" }, - { status: 500 }, - ); } } diff --git a/apps/login/src/lib/oidc.ts b/apps/login/src/lib/oidc.ts new file mode 100644 index 0000000000..b7cca43d8c --- /dev/null +++ b/apps/login/src/lib/oidc.ts @@ -0,0 +1,136 @@ +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; + serviceRegion: string; + authRequest: string; + sessionId: string; + sessions: Session[]; + sessionCookies: Cookie[]; + request: NextRequest; +}; +export async function loginWithOIDCandSession({ + serviceUrl, + serviceRegion, + 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, + serviceRegion, + 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, + authRequestId: 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, + serviceRegion, + 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, + serviceRegion, + 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..d06dd59ef1 --- /dev/null +++ b/apps/login/src/lib/saml.ts @@ -0,0 +1,134 @@ +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; + serviceRegion: string; + samlRequest: string; + sessionId: string; + sessions: Session[]; + sessionCookies: Cookie[]; + request: NextRequest; +}; + +export async function loginWithSAMLandSession({ + serviceUrl, + serviceRegion, + 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, + serviceRegion, + 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, + authRequestId: 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, + serviceRegion, + 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, + serviceRegion, + 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/service.ts b/apps/login/src/lib/service.ts index e543f35467..03bf68645b 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 58fe1d20df..23f2801e7d 100644 --- a/apps/login/src/lib/session.ts +++ b/apps/login/src/lib/session.ts @@ -1,7 +1,14 @@ +import { timestampDate } from "@zitadel/client"; +import { AuthRequest } from "@zitadel/proto/zitadel/oidc/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 +36,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, + serviceRegion, + session, +}: { + serviceUrl: string; + serviceRegion: 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, + serviceRegion, + 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, + serviceRegion, + 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, + serviceRegion, + sessions, + authRequest, +}: { + serviceUrl: string; + serviceRegion: 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, serviceRegion, session })) { + return session; + } + } + + return undefined; +} diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index 535f4fd4cb..ec628b8102 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, @@ -1030,6 +1034,24 @@ export async function createCallback({ return oidcService.createCallback(req); } +export async function createResponse({ + serviceUrl, + serviceRegion, + req, +}: { + serviceUrl: string; + serviceRegion: string; + req: CreateResponseRequest; +}) { + const samlService = await createServiceForHost( + SAMLService, + serviceUrl, + serviceRegion, + ); + + return samlService.createResponse(req); +} + export async function verifyEmail({ serviceUrl, serviceRegion, diff --git a/packages/zitadel-client/src/v2.ts b/packages/zitadel-client/src/v2.ts index 532a722f49..27dea14c4e 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"; 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"; @@ -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); From c7c054da4803e996657ce707c61dfc398f607742 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 5 Feb 2025 09:13:43 +0100 Subject: [PATCH 02/19] request id param --- apps/login/src/app/(login)/accounts/page.tsx | 8 ++-- .../app/(login)/authenticator/set/page.tsx | 8 ++-- .../(login)/idp/[provider]/success/page.tsx | 10 ++--- apps/login/src/app/(login)/idp/page.tsx | 4 +- apps/login/src/app/(login)/loginname/page.tsx | 6 +-- apps/login/src/app/(login)/mfa/page.tsx | 4 +- apps/login/src/app/(login)/mfa/set/page.tsx | 12 ++---- .../src/app/(login)/otp/[method]/page.tsx | 4 +- .../src/app/(login)/otp/[method]/set/page.tsx | 18 ++++---- apps/login/src/app/(login)/passkey/page.tsx | 4 +- .../src/app/(login)/passkey/set/page.tsx | 5 +-- .../src/app/(login)/password/change/page.tsx | 4 +- apps/login/src/app/(login)/password/page.tsx | 4 +- .../src/app/(login)/password/set/page.tsx | 4 +- apps/login/src/app/(login)/register/page.tsx | 5 +-- .../app/(login)/register/password/page.tsx | 5 +-- apps/login/src/app/(login)/signedin/page.tsx | 32 +++++++++++--- apps/login/src/app/(login)/u2f/page.tsx | 4 +- apps/login/src/app/(login)/u2f/set/page.tsx | 4 +- apps/login/src/app/(login)/verify/page.tsx | 14 +++---- apps/login/src/app/login/route.ts | 8 ++-- .../src/components/change-password-form.tsx | 6 +-- .../choose-second-factor-to-setup.tsx | 8 ++-- .../src/components/choose-second-factor.tsx | 8 ++-- apps/login/src/components/idp-signin.tsx | 6 +-- .../components/idps/pages/linking-success.tsx | 4 +- .../components/idps/pages/login-success.tsx | 4 +- apps/login/src/components/login-otp.tsx | 18 ++++---- apps/login/src/components/login-passkey.tsx | 12 +++--- apps/login/src/components/password-form.tsx | 12 +++--- apps/login/src/components/register-form.tsx | 10 ++--- .../login/src/components/register-passkey.tsx | 8 ++-- apps/login/src/components/register-u2f.tsx | 12 +++--- apps/login/src/components/session-item.tsx | 8 ++-- apps/login/src/components/sessions-list.tsx | 6 +-- .../src/components/set-password-form.tsx | 8 ++-- .../components/set-register-password-form.tsx | 6 +-- .../login/src/components/sign-in-with-idp.tsx | 8 ++-- apps/login/src/components/totp-register.tsx | 12 +++--- apps/login/src/components/user-avatar.tsx | 4 +- apps/login/src/components/username-form.tsx | 10 ++--- apps/login/src/components/verify-form.tsx | 6 +-- .../src/components/verify-redirect-button.tsx | 6 +-- apps/login/src/lib/client.ts | 8 ++-- apps/login/src/lib/cookies.ts | 2 +- apps/login/src/lib/oidc.ts | 2 +- apps/login/src/lib/saml.ts | 2 +- apps/login/src/lib/server/cookie.ts | 24 +++++------ apps/login/src/lib/server/idp.ts | 12 +++--- apps/login/src/lib/server/invite.ts | 2 +- apps/login/src/lib/server/loginname.ts | 42 +++++++++---------- apps/login/src/lib/server/otp.ts | 4 +- apps/login/src/lib/server/passkeys.ts | 12 +++--- apps/login/src/lib/server/password.ts | 20 ++++----- apps/login/src/lib/server/register.ts | 14 +++---- apps/login/src/lib/server/session.ts | 22 ++++------ apps/login/src/lib/server/verify.ts | 26 ++++++------ apps/login/src/lib/verify-helper.ts | 36 ++++++++-------- 58 files changed, 291 insertions(+), 286 deletions(-) diff --git a/apps/login/src/app/(login)/accounts/page.tsx b/apps/login/src/app/(login)/accounts/page.tsx index bc63d990c9..f0156aab8c 100644 --- a/apps/login/src/app/(login)/accounts/page.tsx +++ b/apps/login/src/app/(login)/accounts/page.tsx @@ -42,7 +42,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(); @@ -69,8 +69,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) { @@ -84,7 +84,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 634b116815..3977b8dbb0 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, serviceRegion } = 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 425e9f0caf..4522733ecf 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, ); } @@ -179,7 +179,7 @@ export default async function Page(props: { return linkingSuccess( foundUser.userId, { idpIntentId: id, idpIntentToken: token }, - authRequestId, + requestId, branding, ); } @@ -245,7 +245,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 80829557ec..055e79cc07 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 7f8cb92812..ed9ec59490 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"; @@ -73,7 +73,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 53fc650788..42d9707aae 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, serviceRegion } = 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 64e9cd7605..e37d24e55d 100644 --- a/apps/login/src/app/(login)/mfa/set/page.tsx +++ b/apps/login/src/app/(login)/mfa/set/page.tsx @@ -42,14 +42,8 @@ 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, serviceRegion } = getServiceUrlFromHeaders(_headers); @@ -157,7 +151,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 e2f34ae830..ab9dcf102b 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, serviceRegion } = 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 28f77a4b6d..35409cecc6 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 b9b1756813..c5136e941c 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, serviceRegion } = getServiceUrlFromHeaders(_headers); @@ -94,7 +94,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 aeda4d56f6..a588cd2e2e 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, serviceRegion } = getServiceUrlFromHeaders(_headers); @@ -80,7 +79,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 f689713479..8308ddd210 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, serviceRegion: string, loginName: string, - authRequestId?: string, + requestId?: string, ) { const recent = await getMostRecentCookieWithLoginname({ loginName }); - if (authRequestId) { + if (requestId && requestId.startsWith("oidc_")) { return createCallback({ serviceUrl, serviceRegion, req: create(CreateCallbackRequestSchema, { - authRequestId, + authRequestId: requestId, callbackKind: { case: "session", value: create(SessionSchema, { @@ -45,7 +47,25 @@ async function loadSession( }).then(({ callbackUrl }) => { return redirect(callbackUrl); }); + } else if (requestId && requestId.startsWith("saml_")) { + return createResponse({ + serviceUrl, + serviceRegion, + 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, serviceRegion, @@ -66,12 +86,12 @@ export default async function Page(props: { searchParams: Promise }) { const _headers = await headers(); const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); - const { loginName, authRequestId, organization } = searchParams; + const { loginName, requestId, organization } = searchParams; const sessionFactors = await loadSession( serviceUrl, serviceRegion, loginName, - authRequestId, + requestId, ); const branding = await getBrandingSettings({ @@ -81,7 +101,7 @@ export default async function Page(props: { searchParams: Promise }) { }); let loginSettings; - if (!authRequestId) { + if (!requestId) { loginSettings = await getLoginSettings({ serviceUrl, serviceRegion, diff --git a/apps/login/src/app/(login)/u2f/page.tsx b/apps/login/src/app/(login)/u2f/page.tsx index e0a21103a8..930538c834 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, serviceRegion } = 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 628d07f36f..e55d589ec8 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(); @@ -63,7 +63,7 @@ export default async function Page(props: { searchParams: Promise }) { userId: sessionFactors?.factors?.user?.id, urlTemplate: `${host.includes("localhost") ? "http://" : "https://"}${host}/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}/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 ad89312fcf..adf7658723 100644 --- a/apps/login/src/app/login/route.ts +++ b/apps/login/src/app/login/route.ts @@ -126,11 +126,11 @@ export async function GET(request: NextRequest) { }); } - if (requestId) { + if (requestId && requestId.startsWith("oidc_")) { const { authRequest } = await getAuthRequest({ serviceUrl, serviceRegion, - authRequestId: requestId, + authRequestId: requestId.replace("oidc_", ""), }); let organization = ""; @@ -257,7 +257,7 @@ export async function GET(request: NextRequest) { try { let command: SendLoginnameCommand = { loginName: authRequest.loginHint, - authRequestId: authRequest.id, + requestId: authRequest.id, }; if (organization) { @@ -426,6 +426,8 @@ export async function GET(request: NextRequest) { return NextResponse.redirect(loginNameUrl); } + } else if (requestId && requestId.startsWith("saml_")) { + // handle saml request } else { return NextResponse.json( { error: "No authRequest nor samlRequest provided" }, 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..21f7aff8a6 100644 --- a/apps/login/src/components/choose-second-factor-to-setup.tsx +++ b/apps/login/src/components/choose-second-factor-to-setup.tsx @@ -10,7 +10,7 @@ import { EMAIL, SMS, TOTP, U2F } from "./auth-methods"; type Props = { loginName?: string; sessionId?: string; - authRequestId?: string; + requestId?: string; organization?: string; loginSettings: LoginSettings; userMethods: AuthenticationMethodType[]; @@ -22,7 +22,7 @@ type Props = { export function ChooseSecondFactorToSetup({ loginName, sessionId, - authRequestId, + requestId, organization, loginSettings, userMethods, @@ -38,8 +38,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); 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 c5be74d252..0829f00391 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, @@ -85,7 +85,7 @@ export function LoginOTP({ ? { urlTemplate: `${host.includes("localhost") ? "http://" : "https://"}${host}/otp/${method}?code={{.Code}}&userId={{.UserID}}&sessionId={{.SessionID}}` + - (authRequestId ? `&authRequestId=${authRequestId}` : ""), + (requestId ? `&requestId=${requestId}` : ""), } : {}, }, @@ -105,7 +105,7 @@ export function LoginOTP({ sessionId, organization, challenges, - authRequestId, + requestId, }) .catch(() => { setError("Could not request OTP challenge"); @@ -135,8 +135,8 @@ export function LoginOTP({ body.organization = organization; } - if (authRequestId) { - body.authRequestId = authRequestId; + if (requestId) { + body.requestId = requestId; } let checks; @@ -162,7 +162,7 @@ export function LoginOTP({ sessionId, organization, checks, - authRequestId, + requestId, }) .catch(() => { setError("Could not verify OTP code"); @@ -188,11 +188,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 index b7cca43d8c..221c3549b8 100644 --- a/apps/login/src/lib/oidc.ts +++ b/apps/login/src/lib/oidc.ts @@ -51,7 +51,7 @@ export async function loginWithOIDCandSession({ const command: SendLoginnameCommand = { loginName: selectedSession.factors.user?.loginName, organization: selectedSession.factors?.user?.organizationId, - authRequestId: authRequest, + requestId: `oidc_${authRequest}`, }; const res = await sendLoginname(command); diff --git a/apps/login/src/lib/saml.ts b/apps/login/src/lib/saml.ts index d06dd59ef1..67e34c57b6 100644 --- a/apps/login/src/lib/saml.ts +++ b/apps/login/src/lib/saml.ts @@ -49,7 +49,7 @@ export async function loginWithSAMLandSession({ const command: SendLoginnameCommand = { loginName: selectedSession.factors.user?.loginName, organization: selectedSession.factors?.user?.organizationId, - authRequestId: samlRequest, + requestId: `saml_${samlRequest}`, }; const res = await sendLoginname(command); diff --git a/apps/login/src/lib/server/cookie.ts b/apps/login/src/lib/server/cookie.ts index 03a421674d..b4d6572375 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) => { @@ -49,7 +49,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(); @@ -86,8 +86,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 +113,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 +165,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 +186,7 @@ export async function setSessionAndUpdateCookie( recentCookie: CustomCookieData, checks?: Checks, challenges?: RequestChallenges, - authRequestId?: string, + requestId?: string, lifetime?: Duration, ) { const _headers = await headers(); @@ -216,8 +216,8 @@ export async function setSessionAndUpdateCookie( organization: recentCookie.organization, }; - if (authRequestId) { - sessionCookie.authRequestId = authRequestId; + if (requestId) { + sessionCookie.requestId = requestId; } return getSession({ @@ -241,8 +241,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 c12f518fd3..299da47681 100644 --- a/apps/login/src/lib/server/idp.ts +++ b/apps/login/src/lib/server/idp.ts @@ -54,7 +54,7 @@ type CreateNewSessionCommand = { loginName?: string; password?: string; organization?: string; - authRequestId?: string; + requestId?: string; }; export async function createNewSessionFromIdpIntent( @@ -91,7 +91,7 @@ export async function createNewSessionFromIdpIntent( const session = await createSessionForIdpAndUpdateCookie( command.userId, command.idpIntent, - command.authRequestId, + command.requestId, loginSettings?.externalLoginCheckLifetime, ); @@ -109,7 +109,7 @@ export async function createNewSessionFromIdpIntent( session, humanUser, command.organization, - command.authRequestId, + command.requestId, ); if (emailVerificationCheck?.redirect) { @@ -117,16 +117,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 864c91540e..e11858ce15 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 18070ab76c..7d04f6e6a9 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) { @@ -161,8 +161,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) { @@ -242,7 +242,7 @@ export async function sendLoginname(command: SendLoginnameCommand) { const session = await createSessionAndUpdateCookie( checks, undefined, - command.authRequestId, + command.requestId, ); if (!session.factors?.user?.id) { @@ -267,7 +267,7 @@ export async function sendLoginname(command: SendLoginnameCommand) { session, humanUser, session.factors.user.organizationId, - command.authRequestId, + command.requestId, ); if (inviteCheck?.redirect) { @@ -286,8 +286,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 }; @@ -315,8 +315,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 { @@ -332,8 +332,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) { @@ -351,8 +351,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) { @@ -371,8 +371,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) { @@ -435,8 +435,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) { @@ -452,8 +452,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 6d56d0c538..323ea7f907 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 819f319bd4..87f15e6d6d 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 9a464e22d8..23efc126f8 100644 --- a/apps/login/src/lib/server/password.ts +++ b/apps/login/src/lib/server/password.ts @@ -42,7 +42,7 @@ import { type ResetPasswordCommand = { loginName: string; organization?: string; - authRequestId?: string; + requestId?: string; }; export async function resetPassword(command: ResetPasswordCommand) { @@ -76,7 +76,7 @@ export async function resetPassword(command: ResetPasswordCommand) { userId, urlTemplate: `${host.includes("localhost") ? "http://" : "https://"}${host}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` + - (command.authRequestId ? `&authRequestId=${command.authRequestId}` : ""), + (command.requestId ? `&requestId=${command.requestId}` : ""), }); } @@ -84,7 +84,7 @@ export type UpdateSessionCommand = { loginName: string; organization?: string; checks: Checks; - authRequestId?: string; + requestId?: string; }; export async function sendPassword(command: UpdateSessionCommand) { @@ -128,7 +128,7 @@ export async function sendPassword(command: UpdateSessionCommand) { session = await createSessionAndUpdateCookie( checks, undefined, - command.authRequestId, + command.requestId, loginSettings?.passwordCheckLifetime, ); } catch (error: any) { @@ -160,7 +160,7 @@ export async function sendPassword(command: UpdateSessionCommand) { sessionCookie, command.checks, undefined, - command.authRequestId, + command.requestId, loginSettings?.passwordCheckLifetime, ); } catch (error: any) { @@ -227,7 +227,7 @@ export async function sendPassword(command: UpdateSessionCommand) { session, humanUser, command.organization, - command.authRequestId, + command.requestId, ); if (passwordChangedCheck?.redirect) { @@ -244,7 +244,7 @@ export async function sendPassword(command: UpdateSessionCommand) { session, humanUser, command.organization, - command.authRequestId, + command.requestId, ); if (emailVerificationCheck?.redirect) { @@ -273,18 +273,18 @@ export async function sendPassword(command: UpdateSessionCommand) { 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, }, diff --git a/apps/login/src/lib/server/register.ts b/apps/login/src/lib/server/register.ts index 2a23af3073..3adf3c5a0a 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 = { @@ -72,7 +72,7 @@ export async function registerUser(command: RegisterUserCommand) { const session = await createSessionAndUpdateCookie( checks, undefined, - command.authRequestId, + command.requestId, command.password ? loginSettings?.passwordCheckLifetime : undefined, ); @@ -86,8 +86,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 +111,7 @@ export async function registerUser(command: RegisterUserCommand) { session, humanUser, session.factors.user.organizationId, - command.authRequestId, + command.requestId, ); if (emailVerificationCheck?.redirect) { @@ -119,10 +119,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 7f71ec8f14..f1c40e94bd 100644 --- a/apps/login/src/lib/server/session.ts +++ b/apps/login/src/lib/server/session.ts @@ -21,9 +21,9 @@ import { import { getServiceUrlFromHeaders } from "../service"; export async function continueWithSession({ - authRequestId, + requestId, ...session -}: Session & { authRequestId?: string }) { +}: Session & { requestId?: string }) { const _headers = await headers(); const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); @@ -34,11 +34,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 +62,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 +117,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 2ab94a9252..5a62a3948e 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) { @@ -158,7 +158,7 @@ export async function sendVerification(command: VerifyUserByEmailCommand) { session = await createSessionAndUpdateCookie( checks, undefined, - command.authRequestId, + command.requestId, ); } @@ -212,7 +212,7 @@ export async function sendVerification(command: VerifyUserByEmailCommand) { loginSettings, authMethodResponse.authMethodTypes, command.organization, - command.authRequestId, + command.requestId, ); if (mfaFactorCheck?.redirect) { @@ -220,11 +220,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 +248,7 @@ export async function sendVerification(command: VerifyUserByEmailCommand) { type resendVerifyEmailCommand = { userId: string; isInvite: boolean; - authRequestId?: string; + requestId?: string; }; export async function resendVerification(command: resendVerifyEmailCommand) { @@ -268,9 +268,7 @@ export async function resendVerification(command: resendVerifyEmailCommand) { serviceRegion, urlTemplate: `${host.includes("localhost") ? "http://" : "https://"}${host}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` + - (command.authRequestId - ? `&authRequestId=${command.authRequestId}` - : ""), + (command.requestId ? `&requestId=${command.requestId}` : ""), }); } @@ -292,7 +290,7 @@ export async function sendEmailCode(command: sendEmailCommand) { export type SendVerificationRedirectWithoutCheckCommand = { organization?: string; - authRequestId?: string; + requestId?: string; } & ( | { userId: string; loginName?: never } | { userId?: never; loginName: string } @@ -374,7 +372,7 @@ export async function sendVerificationRedirectWithoutCheck( session = await createSessionAndUpdateCookie( checks, undefined, - command.authRequestId, + command.requestId, ); } @@ -428,7 +426,7 @@ export async function sendVerificationRedirectWithoutCheck( loginSettings, authMethodResponse.authMethodTypes, command.organization, - command.authRequestId, + command.requestId, ); if (mfaFactorCheck?.redirect) { @@ -436,11 +434,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/verify-helper.ts b/apps/login/src/lib/verify-helper.ts index 053d1cc71f..e8afef6890 100644 --- a/apps/login/src/lib/verify-helper.ts +++ b/apps/login/src/lib/verify-helper.ts @@ -11,7 +11,7 @@ export function checkPasswordChangeRequired( session: Session, humanUser: HumanUser | undefined, organization?: string, - authRequestId?: string, + requestId?: string, ) { let isOutdated = false; if (expirySettings?.maxAgeDays && humanUser?.passwordChanged) { @@ -35,8 +35,8 @@ export function checkPasswordChangeRequired( ); } - if (authRequestId) { - params.append("authRequestId", authRequestId); + if (requestId) { + params.append("requestId", requestId); } return { redirect: "/password/change?" + params }; @@ -47,7 +47,7 @@ export function checkInvite( session: Session, humanUser?: HumanUser, organization?: string, - authRequestId?: string, + requestId?: string, ) { if (!humanUser?.email?.isVerified) { const paramsVerify = new URLSearchParams({ @@ -63,8 +63,8 @@ export function checkInvite( ); } - if (authRequestId) { - paramsVerify.append("authRequestId", authRequestId); + if (requestId) { + paramsVerify.append("requestId", requestId); } return { redirect: "/verify?" + paramsVerify }; @@ -75,7 +75,7 @@ export function checkEmailVerification( session: Session, humanUser?: HumanUser, organization?: string, - authRequestId?: string, + requestId?: string, ) { if ( !humanUser?.email?.isVerified && @@ -85,8 +85,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) { @@ -105,7 +105,7 @@ export function checkMFAFactors( loginSettings: LoginSettings | undefined, authMethods: AuthenticationMethodType[], organization?: string, - authRequestId?: string, + requestId?: string, ) { const availableMultiFactors = authMethods?.filter( (m: AuthenticationMethodType) => @@ -128,8 +128,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 +155,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 +177,8 @@ 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) { @@ -205,8 +205,8 @@ export function checkMFAFactors( // prompt: "true", // }); - // if (authRequestId) { - // params.append("authRequestId", authRequestId); + // if (requestId) { + // params.append("requestId", requestId); // } // if (organization) { From 9516a3a59ad698203871c7031a5683dff6c4bc1c Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 5 Feb 2025 10:01:29 +0100 Subject: [PATCH 03/19] serializing / deserializing authrequest --- apps/login/src/app/login/route.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/apps/login/src/app/login/route.ts b/apps/login/src/app/login/route.ts index adf7658723..4150eab303 100644 --- a/apps/login/src/app/login/route.ts +++ b/apps/login/src/app/login/route.ts @@ -84,7 +84,11 @@ export async function GET(request: NextRequest) { const oidcRequestId = searchParams.get("authRequest"); // oidc initiated request const samlRequestId = searchParams.get("samlRequest"); // saml initiated request - const requestId = searchParams.get("requestId"); // internal request id which combines authRequest and samlRequest with the prefix oidc_ or saml_ + // 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"); @@ -194,7 +198,7 @@ export async function GET(request: NextRequest) { const params = new URLSearchParams(); if (requestId) { - params.set("requestId", `oidc_${requestId}`); + params.set("requestId", requestId); } if (organization) { @@ -332,7 +336,7 @@ export async function GET(request: NextRequest) { serviceUrl, serviceRegion, req: create(CreateCallbackRequestSchema, { - authRequestId: requestId, + authRequestId: requestId.replace("oidc_", ""), callbackKind: { case: "session", value: create(SessionSchema, session), @@ -381,7 +385,7 @@ export async function GET(request: NextRequest) { serviceUrl, serviceRegion, req: create(CreateCallbackRequestSchema, { - authRequestId: requestId, + authRequestId: requestId.replace("oidc_", ""), callbackKind: { case: "session", value: create(SessionSchema, session), @@ -414,7 +418,7 @@ export async function GET(request: NextRequest) { } else { const loginNameUrl = new URL("/loginname", request.url); - loginNameUrl.searchParams.set("requestId", `oidc_${requestId}`); + loginNameUrl.searchParams.set("requestId", requestId); if (authRequest?.loginHint) { loginNameUrl.searchParams.set("loginName", authRequest.loginHint); loginNameUrl.searchParams.set("submit", "true"); // autosubmit From 7a83345428686977e2322f2a435800aaf2168aa5 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 5 Feb 2025 11:33:21 +0100 Subject: [PATCH 04/19] saml req --- apps/login/src/app/login/route.ts | 8 ++++++++ apps/login/src/lib/zitadel.ts | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/apps/login/src/app/login/route.ts b/apps/login/src/app/login/route.ts index 4150eab303..ce82eab6ea 100644 --- a/apps/login/src/app/login/route.ts +++ b/apps/login/src/app/login/route.ts @@ -10,6 +10,7 @@ import { getActiveIdentityProviders, getAuthRequest, getOrgsByDomain, + getSAMLRequest, listSessions, startIdentityProviderFlow, } from "@/lib/zitadel"; @@ -432,6 +433,13 @@ export async function GET(request: NextRequest) { } } else if (requestId && requestId.startsWith("saml_")) { // handle saml request + const { samlRequest } = await getSAMLRequest({ + serviceUrl, + serviceRegion, + samlRequestId: requestId.replace("saml_", ""), + }); + + samlRequest?. } else { return NextResponse.json( { error: "No authRequest nor samlRequest provided" }, diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index ec628b8102..72fd015f88 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -1034,6 +1034,26 @@ export async function createCallback({ return oidcService.createCallback(req); } +export async function getSAMLRequest({ + serviceUrl, + serviceRegion, + samlRequestId, +}: { + serviceUrl: string; + serviceRegion: string; + samlRequestId: string; +}) { + const samlService = await createServiceForHost( + SAMLService, + serviceUrl, + serviceRegion, + ); + + return samlService.getSAMLRequest({ + samlRequestId, + }); +} + export async function createResponse({ serviceUrl, serviceRegion, From ba3c3d596dcbad7c5fc5083a31d86e11493011fa Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Fri, 14 Feb 2025 16:38:03 +0100 Subject: [PATCH 05/19] form post --- apps/login/src/app/login/route.ts | 140 ++++++++++++++++++++++++------ apps/login/src/lib/session.ts | 14 ++- 2 files changed, 124 insertions(+), 30 deletions(-) diff --git a/apps/login/src/app/login/route.ts b/apps/login/src/app/login/route.ts index 4ae2be17b3..973e2142d6 100644 --- a/apps/login/src/app/login/route.ts +++ b/apps/login/src/app/login/route.ts @@ -7,6 +7,7 @@ import { getServiceUrlFromHeaders } from "@/lib/service"; import { findValidSession } from "@/lib/session"; import { createCallback, + createResponse, getActiveIdentityProviders, getAuthRequest, getOrgsByDomain, @@ -15,14 +16,12 @@ import { startIdentityProviderFlow, } from "@/lib/zitadel"; import { create } from "@zitadel/client"; -import { - AuthRequest, - Prompt, -} from "@zitadel/proto/zitadel/oidc/v2/authorization_pb"; +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 { headers } from "next/headers"; import { NextRequest, NextResponse } from "next/server"; @@ -33,19 +32,17 @@ export const fetchCache = "default-no-store"; const gotoAccounts = ({ request, - authRequest, + requestId, organization, - idPrefix, }: { request: NextRequest; - authRequest: AuthRequest; - organization: string; - idPrefix: string; + requestId: string; + organization?: string; }): NextResponse => { const accountsUrl = new URL("/accounts", request.url); - if (authRequest?.id) { - accountsUrl.searchParams.set("requestId", `${idPrefix}${authRequest.id}`); + if (requestId) { + accountsUrl.searchParams.set("requestId", requestId); } if (organization) { accountsUrl.searchParams.set("organization", organization); @@ -59,7 +56,6 @@ async function loadSessions({ ids, }: { serviceUrl: string; - ids: string[]; }): Promise { const response = await listSessions({ @@ -177,7 +173,6 @@ export async function GET(request: NextRequest) { const identityProviders = await getActiveIdentityProviders({ serviceUrl, - orgId: organization ? organization : undefined, }).then((resp) => { return resp.identityProviders; @@ -203,7 +198,6 @@ export async function GET(request: NextRequest) { return startIdentityProviderFlow({ serviceUrl, - idpId, urls: { successUrl: @@ -243,9 +237,8 @@ export async function GET(request: NextRequest) { if (authRequest.prompt.includes(Prompt.SELECT_ACCOUNT)) { return gotoAccounts({ request, - authRequest, + requestId: `oidc_${authRequest.id}`, organization, - idPrefix: "oidc_", }); } else if (authRequest.prompt.includes(Prompt.LOGIN)) { /** @@ -300,7 +293,6 @@ export async function GET(request: NextRequest) { **/ const selectedSession = await findValidSession({ serviceUrl, - sessions, authRequest, }); @@ -330,7 +322,6 @@ export async function GET(request: NextRequest) { const { callbackUrl } = await createCallback({ serviceUrl, - req: create(CreateCallbackRequestSchema, { authRequestId: requestId.replace("oidc_", ""), callbackKind: { @@ -351,9 +342,8 @@ export async function GET(request: NextRequest) { if (!selectedSession || !selectedSession.id) { return gotoAccounts({ request, - authRequest, + requestId: `oidc_${authRequest.id}`, organization, - idPrefix: "oidc_", }); } @@ -364,9 +354,8 @@ export async function GET(request: NextRequest) { if (!cookie || !cookie.id || !cookie.token) { return gotoAccounts({ request, - authRequest, + requestId: `oidc_${authRequest.id}`, organization, - idPrefix: "oidc_", }); } @@ -395,18 +384,16 @@ export async function GET(request: NextRequest) { ); return gotoAccounts({ request, - authRequest, organization, - idPrefix: "oidc_", + requestId: `oidc_${authRequest.id}`, }); } } catch (error) { console.error(error); return gotoAccounts({ request, - authRequest, + requestId: `oidc_${authRequest.id}`, organization, - idPrefix: "oidc_", }); } } @@ -432,6 +419,107 @@ export async function GET(request: NextRequest) { 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 authRequest nor samlRequest provided" }, diff --git a/apps/login/src/lib/session.ts b/apps/login/src/lib/session.ts index d3d5c303c1..f9eb0ceeb2 100644 --- a/apps/login/src/lib/session.ts +++ b/apps/login/src/lib/session.ts @@ -1,5 +1,6 @@ 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"; @@ -150,21 +151,26 @@ export async function isSessionValid({ export async function findValidSession({ serviceUrl, - sessions, authRequest, + samlRequest, }: { serviceUrl: string; sessions: Session[]; - authRequest: AuthRequest; + authRequest?: AuthRequest; + samlRequest?: SAMLRequest; }): Promise { const sessionsWithHint = sessions.filter((s) => { - if (authRequest.hintUserId) { + if (authRequest && authRequest.hintUserId) { return s.factors?.user?.id === authRequest.hintUserId; } - if (authRequest.loginHint) { + if (authRequest && authRequest.loginHint) { return s.factors?.user?.loginName === authRequest.loginHint; } + if (samlRequest) { + // TODO: do whatever + return true; + } return true; }); From 734426c116c22b44f589614036b17169c0a20459 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 17 Feb 2025 09:09:55 +0100 Subject: [PATCH 06/19] fix build --- apps/login/src/app/(login)/signedin/page.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/login/src/app/(login)/signedin/page.tsx b/apps/login/src/app/(login)/signedin/page.tsx index 78f89dbcc4..c6b747befa 100644 --- a/apps/login/src/app/(login)/signedin/page.tsx +++ b/apps/login/src/app/(login)/signedin/page.tsx @@ -50,7 +50,6 @@ async function loadSession( } else if (requestId && requestId.startsWith("saml_")) { return createResponse({ serviceUrl, - serviceRegion, req: create(CreateResponseRequestSchema, { samlRequestId: requestId.replace("saml_", ""), responseKind: { From ee898c447d1b280572d017667cfd00e309016c5b Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 17 Feb 2025 09:23:46 +0100 Subject: [PATCH 07/19] login route handler --- apps/login/src/app/login/route.ts | 688 +++++++++++++++--------------- 1 file changed, 344 insertions(+), 344 deletions(-) diff --git a/apps/login/src/app/login/route.ts b/apps/login/src/app/login/route.ts index 973e2142d6..63c85df4b9 100644 --- a/apps/login/src/app/login/route.ts +++ b/apps/login/src/app/login/route.ts @@ -100,6 +100,7 @@ export async function GET(request: NextRequest) { sessions = await loadSessions({ serviceUrl, ids }); } + // complete flow if session and request id are provided if (requestId && sessionId) { if (requestId.startsWith("oidc_")) { // this finishes the login process for OIDC @@ -122,206 +123,251 @@ export async function GET(request: NextRequest) { request, }); } + } - if (requestId && requestId.startsWith("oidc_")) { - const { authRequest } = await getAuthRequest({ - serviceUrl, - authRequestId: requestId.replace("oidc_", ""), - }); + // continue with OIDC + if (requestId && requestId.startsWith("oidc_")) { + const { authRequest } = await getAuthRequest({ + serviceUrl, + authRequestId: requestId.replace("oidc_", ""), + }); - let organization = ""; - let suffix = ""; - let idpId = ""; + let organization = ""; + let suffix = ""; + let idpId = ""; - if (authRequest?.scope) { - const orgScope = authRequest.scope.find((s: string) => - ORG_SCOPE_REGEX.test(s), + if (authRequest?.scope) { + const orgScope = authRequest.scope.find((s: string) => + ORG_SCOPE_REGEX.test(s), + ); + + const idpScope = authRequest.scope.find((s: string) => + IDP_SCOPE_REGEX.test(s), + ); + + if (orgScope) { + const matched = ORG_SCOPE_REGEX.exec(orgScope); + organization = matched?.[1] ?? ""; + } else { + const orgDomainScope = authRequest.scope.find((s: string) => + ORG_DOMAIN_SCOPE_REGEX.test(s), ); - const idpScope = authRequest.scope.find((s: string) => - IDP_SCOPE_REGEX.test(s), - ); + if (orgDomainScope) { + const matched = ORG_DOMAIN_SCOPE_REGEX.exec(orgDomainScope); + const orgDomain = matched?.[1] ?? ""; + if (orgDomain) { + const orgs = await getOrgsByDomain({ + serviceUrl, - if (orgScope) { - const matched = ORG_SCOPE_REGEX.exec(orgScope); - organization = matched?.[1] ?? ""; - } else { - const orgDomainScope = authRequest.scope.find((s: string) => - ORG_DOMAIN_SCOPE_REGEX.test(s), - ); - - if (orgDomainScope) { - const matched = ORG_DOMAIN_SCOPE_REGEX.exec(orgDomainScope); - const orgDomain = matched?.[1] ?? ""; - if (orgDomain) { - const orgs = await getOrgsByDomain({ - serviceUrl, - - domain: orgDomain, - }); - if (orgs.result && orgs.result.length === 1) { - organization = orgs.result[0].id ?? ""; - suffix = orgDomain; - } + domain: orgDomain, + }); + if (orgs.result && orgs.result.length === 1) { + organization = orgs.result[0].id ?? ""; + suffix = orgDomain; } } } + } - if (idpScope) { - const matched = IDP_SCOPE_REGEX.exec(idpScope); - idpId = matched?.[1] ?? ""; + if (idpScope) { + const matched = IDP_SCOPE_REGEX.exec(idpScope); + idpId = matched?.[1] ?? ""; - const identityProviders = await getActiveIdentityProviders({ + const identityProviders = await getActiveIdentityProviders({ + serviceUrl, + orgId: organization ? organization : undefined, + }).then((resp) => { + return resp.identityProviders; + }); + + const idp = identityProviders.find((idp) => idp.id === idpId); + + if (idp) { + const origin = request.nextUrl.origin; + + const identityProviderType = identityProviders[0].type; + let provider = idpTypeToSlug(identityProviderType); + + const params = new URLSearchParams(); + + if (requestId) { + params.set("requestId", requestId); + } + + if (organization) { + params.set("organization", organization); + } + + return startIdentityProviderFlow({ serviceUrl, - orgId: organization ? organization : undefined, + idpId, + urls: { + successUrl: + `${origin}/idp/${provider}/success?` + + new URLSearchParams(params), + failureUrl: + `${origin}/idp/${provider}/failure?` + + new URLSearchParams(params), + }, }).then((resp) => { - return resp.identityProviders; - }); - - const idp = identityProviders.find((idp) => idp.id === idpId); - - if (idp) { - const origin = request.nextUrl.origin; - - const identityProviderType = identityProviders[0].type; - let provider = idpTypeToSlug(identityProviderType); - - const params = new URLSearchParams(); - - if (requestId) { - params.set("requestId", requestId); + if ( + resp.nextStep.value && + typeof resp.nextStep.value === "string" + ) { + return NextResponse.redirect(resp.nextStep.value); } + }); + } + } + } + + if (authRequest && authRequest.prompt.includes(Prompt.CREATE)) { + const registerUrl = new URL("/register", request.url); + if (authRequest.id) { + registerUrl.searchParams.set("requestId", `oidc_${authRequest.id}`); + } + if (organization) { + registerUrl.searchParams.set("organization", organization); + } + + 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({ + 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 + */ + + // if a hint is provided, skip loginname page and jump to the next page + if (authRequest.loginHint) { + try { + let command: SendLoginnameCommand = { + loginName: authRequest.loginHint, + requestId: authRequest.id, + }; if (organization) { - params.set("organization", organization); + command = { ...command, organization }; } - return startIdentityProviderFlow({ - serviceUrl, - idpId, - urls: { - successUrl: - `${origin}/idp/${provider}/success?` + - new URLSearchParams(params), - failureUrl: - `${origin}/idp/${provider}/failure?` + - new URLSearchParams(params), - }, - }).then((resp) => { - if ( - resp.nextStep.value && - typeof resp.nextStep.value === "string" - ) { - return NextResponse.redirect(resp.nextStep.value); - } - }); + 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()); + } + } catch (error) { + console.error("Failed to execute sendLoginname:", error); } } - } - if (authRequest && authRequest.prompt.includes(Prompt.CREATE)) { - const registerUrl = new URL("/register", request.url); + const loginNameUrl = new URL("/loginname", request.url); if (authRequest.id) { - registerUrl.searchParams.set("requestId", `oidc_${authRequest.id}`); + loginNameUrl.searchParams.set("requestId", `oidc_${authRequest.id}`); + } + if (authRequest.loginHint) { + loginNameUrl.searchParams.set("loginName", authRequest.loginHint); } if (organization) { - registerUrl.searchParams.set("organization", organization); + loginNameUrl.searchParams.set("organization", organization); + } + if (suffix) { + loginNameUrl.searchParams.set("suffix", suffix); + } + 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({ + serviceUrl, + sessions, + authRequest, + }); + + if (!selectedSession || !selectedSession.id) { + return NextResponse.json( + { error: "No active session found" }, + { status: 400 }, + ); } - return NextResponse.redirect(registerUrl); - } + const cookie = sessionCookies.find( + (cookie) => cookie.id === selectedSession.id, + ); - // 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)) { + if (!cookie || !cookie.id || !cookie.token) { + return NextResponse.json( + { error: "No active session found" }, + { status: 400 }, + ); + } + + const session = { + sessionId: cookie.id, + sessionToken: cookie.token, + }; + + const { callbackUrl } = await createCallback({ + serviceUrl, + req: create(CreateCallbackRequestSchema, { + authRequestId: requestId.replace("oidc_", ""), + callbackKind: { + case: "session", + value: create(SessionSchema, session), + }, + }), + }); + return NextResponse.redirect(callbackUrl); + } else { + // check for loginHint, userId hint and valid sessions + let selectedSession = await findValidSession({ + serviceUrl, + sessions, + authRequest, + }); + + if (!selectedSession || !selectedSession.id) { 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 - */ + } - // if a hint is provided, skip loginname page and jump to the next page - if (authRequest.loginHint) { - try { - let command: SendLoginnameCommand = { - loginName: authRequest.loginHint, - requestId: authRequest.id, - }; + const cookie = sessionCookies.find( + (cookie) => cookie.id === selectedSession.id, + ); - if (organization) { - command = { ...command, organization }; - } - - 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()); - } - } catch (error) { - console.error("Failed to execute sendLoginname:", error); - } - } - - const loginNameUrl = new URL("/loginname", request.url); - if (authRequest.id) { - loginNameUrl.searchParams.set( - "requestId", - `oidc_${authRequest.id}`, - ); - } - if (authRequest.loginHint) { - loginNameUrl.searchParams.set("loginName", authRequest.loginHint); - } - if (organization) { - loginNameUrl.searchParams.set("organization", organization); - } - if (suffix) { - loginNameUrl.searchParams.set("suffix", suffix); - } - 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({ - serviceUrl, - sessions, - authRequest, + if (!cookie || !cookie.id || !cookie.token) { + return gotoAccounts({ + request, + requestId: `oidc_${authRequest.id}`, + organization, }); + } - if (!selectedSession || !selectedSession.id) { - return NextResponse.json( - { error: "No active session found" }, - { status: 400 }, - ); - } - - const cookie = sessionCookies.find( - (cookie) => cookie.id === selectedSession.id, - ); - - if (!cookie || !cookie.id || !cookie.token) { - return NextResponse.json( - { error: "No active session found" }, - { status: 400 }, - ); - } - - const session = { - sessionId: cookie.id, - sessionToken: cookie.token, - }; + const session = { + sessionId: cookie.id, + sessionToken: cookie.token, + }; + try { const { callbackUrl } = await createCallback({ serviceUrl, + req: create(CreateCallbackRequestSchema, { authRequestId: requestId.replace("oidc_", ""), callbackKind: { @@ -330,201 +376,155 @@ export async function GET(request: NextRequest) { }, }), }); - return NextResponse.redirect(callbackUrl); - } else { - // check for loginHint, userId hint and valid sessions - let selectedSession = await findValidSession({ - serviceUrl, - sessions, - authRequest, - }); - - if (!selectedSession || !selectedSession.id) { - return gotoAccounts({ - request, - requestId: `oidc_${authRequest.id}`, - organization, - }); - } - - const cookie = sessionCookies.find( - (cookie) => cookie.id === selectedSession.id, - ); - - if (!cookie || !cookie.id || !cookie.token) { - return gotoAccounts({ - request, - requestId: `oidc_${authRequest.id}`, - organization, - }); - } - - const session = { - sessionId: cookie.id, - sessionToken: cookie.token, - }; - - try { - const { callbackUrl } = await createCallback({ - serviceUrl, - - req: create(CreateCallbackRequestSchema, { - authRequestId: requestId.replace("oidc_", ""), - callbackKind: { - case: "session", - value: create(SessionSchema, session), - }, - }), - }); - if (callbackUrl) { - return NextResponse.redirect(callbackUrl); - } else { - console.log( - "could not create callback, redirect user to choose other account", - ); - return gotoAccounts({ - request, - organization, - requestId: `oidc_${authRequest.id}`, - }); - } - } catch (error) { - console.error(error); - return gotoAccounts({ - request, - requestId: `oidc_${authRequest.id}`, - organization, - }); - } - } - } else { - const loginNameUrl = new URL("/loginname", request.url); - - loginNameUrl.searchParams.set("requestId", requestId); - if (authRequest?.loginHint) { - loginNameUrl.searchParams.set("loginName", authRequest.loginHint); - loginNameUrl.searchParams.set("submit", "true"); // autosubmit - } - - if (organization) { - loginNameUrl.searchParams.append("organization", organization); - // loginNameUrl.searchParams.set("organization", organization); - } - - return NextResponse.redirect(loginNameUrl); - } - } else if (requestId && requestId.startsWith("saml_")) { - // handle saml request - 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", - }); + if (callbackUrl) { + return NextResponse.redirect(callbackUrl); } else { - return NextResponse.json( - { error: "Failed to complete SAML request" }, - { status: response.status }, + console.log( + "could not create callback, redirect user to choose other account", ); + return gotoAccounts({ + request, + organization, + requestId: `oidc_${authRequest.id}`, + }); } - } else { - console.log( - "could not create response, redirect user to choose other account", - ); + } catch (error) { + console.error(error); return gotoAccounts({ request, - requestId: `saml_${samlRequest.id}`, + requestId: `oidc_${authRequest.id}`, + organization, }); } - } catch (error) { - console.error(error); + } + } else { + const loginNameUrl = new URL("/loginname", request.url); + + loginNameUrl.searchParams.set("requestId", requestId); + if (authRequest?.loginHint) { + loginNameUrl.searchParams.set("loginName", authRequest.loginHint); + loginNameUrl.searchParams.set("submit", "true"); // autosubmit + } + + if (organization) { + loginNameUrl.searchParams.append("organization", organization); + // loginNameUrl.searchParams.set("organization", organization); + } + + 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}`, }); } - } else { - return NextResponse.json( - { error: "No authRequest nor samlRequest provided" }, - { status: 500 }, - ); + } catch (error) { + console.error(error); + return gotoAccounts({ + request, + requestId: `saml_${samlRequest.id}`, + }); } + } else { + return NextResponse.json( + { error: "No authRequest nor samlRequest provided" }, + { status: 500 }, + ); } } From 762993bd414b5351135264209a24ca05a88f2cf1 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 17 Feb 2025 09:51:26 +0100 Subject: [PATCH 08/19] cleanup --- apps/login/src/lib/zitadel.ts | 90 ----------------------------------- 1 file changed, 90 deletions(-) diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index 322dca4efd..4afb429a20 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -59,11 +59,9 @@ async function cacheWrapper(callback: Promise) { export async function getBrandingSettings({ serviceUrl, - organization, }: { serviceUrl: string; - organization?: string; }) { const settingsService: Client = @@ -78,11 +76,9 @@ export async function getBrandingSettings({ export async function getLoginSettings({ serviceUrl, - organization, }: { serviceUrl: string; - organization?: string; }) { const settingsService: Client = @@ -97,11 +93,9 @@ export async function getLoginSettings({ export async function getLockoutSettings({ serviceUrl, - orgId, }: { serviceUrl: string; - orgId?: string; }) { const settingsService: Client = @@ -116,11 +110,9 @@ export async function getLockoutSettings({ export async function getPasswordExpirySettings({ serviceUrl, - orgId, }: { serviceUrl: string; - orgId?: string; }) { const settingsService: Client = @@ -135,11 +127,9 @@ export async function getPasswordExpirySettings({ export async function listIDPLinks({ serviceUrl, - userId, }: { serviceUrl: string; - userId: string; }) { const userService: Client = await createServiceForHost( @@ -152,11 +142,9 @@ export async function listIDPLinks({ export async function addOTPEmail({ serviceUrl, - userId, }: { serviceUrl: string; - userId: string; }) { const userService: Client = await createServiceForHost( @@ -169,11 +157,9 @@ export async function addOTPEmail({ export async function addOTPSMS({ serviceUrl, - userId, }: { serviceUrl: string; - userId: string; }) { const userService: Client = await createServiceForHost( @@ -186,11 +172,9 @@ export async function addOTPSMS({ export async function registerTOTP({ serviceUrl, - userId, }: { serviceUrl: string; - userId: string; }) { const userService: Client = await createServiceForHost( @@ -218,11 +202,9 @@ export async function getGeneralSettings({ export async function getLegalAndSupportSettings({ serviceUrl, - organization, }: { serviceUrl: string; - organization?: string; }) { const settingsService: Client = @@ -237,11 +219,9 @@ export async function getLegalAndSupportSettings({ export async function getPasswordComplexitySettings({ serviceUrl, - organization, }: { serviceUrl: string; - organization?: string; }) { const settingsService: Client = @@ -256,13 +236,11 @@ export async function getPasswordComplexitySettings({ export async function createSessionFromChecks({ serviceUrl, - checks, challenges, lifetime, }: { serviceUrl: string; - checks: Checks; challenges: RequestChallenges | undefined; lifetime?: Duration; @@ -275,13 +253,11 @@ export async function createSessionFromChecks({ export async function createSessionForUserIdAndIdpIntent({ serviceUrl, - userId, idpIntent, lifetime, }: { serviceUrl: string; - userId: string; idpIntent: { idpIntentId?: string | undefined; @@ -308,7 +284,6 @@ export async function createSessionForUserIdAndIdpIntent({ export async function setSession({ serviceUrl, - sessionId, sessionToken, challenges, @@ -316,7 +291,6 @@ export async function setSession({ lifetime, }: { serviceUrl: string; - sessionId: string; sessionToken: string; challenges: RequestChallenges | undefined; @@ -341,12 +315,10 @@ export async function setSession({ export async function getSession({ serviceUrl, - sessionId, sessionToken, }: { serviceUrl: string; - sessionId: string; sessionToken: string; }) { @@ -358,12 +330,10 @@ export async function getSession({ export async function deleteSession({ serviceUrl, - sessionId, sessionToken, }: { serviceUrl: string; - sessionId: string; sessionToken: string; }) { @@ -375,7 +345,6 @@ export async function deleteSession({ type ListSessionsCommand = { serviceUrl: string; - ids: string[]; }; @@ -404,7 +373,6 @@ export async function listSessions({ export type AddHumanUserData = { serviceUrl: string; - firstName: string; lastName: string; email: string; @@ -414,7 +382,6 @@ export type AddHumanUserData = { export async function addHumanUser({ serviceUrl, - email, firstName, lastName, @@ -447,11 +414,9 @@ export async function addHumanUser({ export async function addHuman({ serviceUrl, - request, }: { serviceUrl: string; - request: AddHumanUserRequest; }) { const userService: Client = await createServiceForHost( @@ -464,12 +429,10 @@ export async function addHuman({ export async function verifyTOTPRegistration({ serviceUrl, - code, userId, }: { serviceUrl: string; - code: string; userId: string; }) { @@ -483,11 +446,9 @@ export async function verifyTOTPRegistration({ export async function getUserByID({ serviceUrl, - userId, }: { serviceUrl: string; - userId: string; }) { const userService: Client = await createServiceForHost( @@ -500,12 +461,10 @@ export async function getUserByID({ export async function verifyInviteCode({ serviceUrl, - userId, verificationCode, }: { serviceUrl: string; - userId: string; verificationCode: string; }) { @@ -519,11 +478,9 @@ export async function verifyInviteCode({ export async function resendInviteCode({ serviceUrl, - userId, }: { serviceUrl: string; - userId: string; }) { const userService: Client = await createServiceForHost( @@ -536,12 +493,10 @@ export async function resendInviteCode({ export async function sendEmailCode({ serviceUrl, - userId, urlTemplate, }: { serviceUrl: string; - userId: string; urlTemplate: string; }) { @@ -567,12 +522,10 @@ export async function sendEmailCode({ export async function createInviteCode({ serviceUrl, - urlTemplate, userId, }: { serviceUrl: string; - urlTemplate: string; userId: string; }) { @@ -604,7 +557,6 @@ export async function createInviteCode({ export type ListUsersCommand = { serviceUrl: string; - loginName?: string; userName?: string; email?: string; @@ -614,7 +566,6 @@ export type ListUsersCommand = { export async function listUsers({ serviceUrl, - loginName, userName, phone, @@ -713,7 +664,6 @@ export async function listUsers({ export type SearchUsersCommand = { serviceUrl: string; - searchValue: string; loginSettings: LoginSettings; organizationId?: string; @@ -759,7 +709,6 @@ const EmailQuery = (searchValue: string) => * */ export async function searchUsers({ serviceUrl, - searchValue, loginSettings, organizationId, @@ -904,11 +853,9 @@ export async function getDefaultOrg({ export async function getOrgsByDomain({ serviceUrl, - domain, }: { serviceUrl: string; - domain: string; }) { const orgService: Client = @@ -931,7 +878,6 @@ export async function getOrgsByDomain({ export async function startIdentityProviderFlow({ serviceUrl, - idpId, urls, }: { @@ -956,7 +902,6 @@ export async function startIdentityProviderFlow({ export async function retrieveIdentityProviderInformation({ serviceUrl, - idpIntentId, idpIntentToken, }: { @@ -978,11 +923,9 @@ export async function retrieveIdentityProviderInformation({ export async function getAuthRequest({ serviceUrl, - authRequestId, }: { serviceUrl: string; - authRequestId: string; }) { const oidcService = await createServiceForHost(OIDCService, serviceUrl); @@ -994,11 +937,9 @@ export async function getAuthRequest({ export async function createCallback({ serviceUrl, - req, }: { serviceUrl: string; - req: CreateCallbackRequest; }) { const oidcService = await createServiceForHost(OIDCService, serviceUrl); @@ -1008,11 +949,9 @@ export async function createCallback({ export async function getSAMLRequest({ serviceUrl, - samlRequestId, }: { serviceUrl: string; - samlRequestId: string; }) { const samlService = await createServiceForHost(SAMLService, serviceUrl); @@ -1036,12 +975,10 @@ export async function createResponse({ export async function verifyEmail({ serviceUrl, - userId, verificationCode, }: { serviceUrl: string; - userId: string; verificationCode: string; }) { @@ -1061,12 +998,10 @@ export async function verifyEmail({ export async function resendEmailCode({ serviceUrl, - userId, urlTemplate, }: { serviceUrl: string; - userId: string; urlTemplate: string; }) { @@ -1090,12 +1025,10 @@ export async function resendEmailCode({ export async function retrieveIDPIntent({ serviceUrl, - id, token, }: { serviceUrl: string; - id: string; token: string; }) { @@ -1112,11 +1045,9 @@ export async function retrieveIDPIntent({ export async function getIDPByID({ serviceUrl, - id, }: { serviceUrl: string; - id: string; }) { const idpService: Client = @@ -1127,12 +1058,10 @@ export async function getIDPByID({ export async function addIDPLink({ serviceUrl, - idp, userId, }: { serviceUrl: string; - idp: { id: string; userId: string; userName: string }; userId: string; }) { @@ -1156,12 +1085,10 @@ export async function addIDPLink({ export async function passwordReset({ serviceUrl, - userId, urlTemplate, }: { serviceUrl: string; - userId: string; urlTemplate?: string; }) { @@ -1193,14 +1120,12 @@ export async function passwordReset({ export async function setUserPassword({ serviceUrl, - userId, password, user, code, }: { serviceUrl: string; - userId: string; password: string; user: User; @@ -1256,11 +1181,9 @@ export async function setUserPassword({ export async function setPassword({ serviceUrl, - payload, }: { serviceUrl: string; - payload: SetPasswordRequest; }) { const userService: Client = await createServiceForHost( @@ -1279,11 +1202,9 @@ export async function setPassword({ */ export async function createPasskeyRegistrationLink({ serviceUrl, - userId, }: { serviceUrl: string; - userId: string; }) { const userService: Client = await createServiceForHost( @@ -1309,12 +1230,10 @@ export async function createPasskeyRegistrationLink({ */ export async function registerU2F({ serviceUrl, - userId, domain, }: { serviceUrl: string; - userId: string; domain: string; }) { @@ -1337,11 +1256,9 @@ export async function registerU2F({ */ export async function verifyU2FRegistration({ serviceUrl, - request, }: { serviceUrl: string; - request: VerifyU2FRegistrationRequest; }) { const userService: Client = await createServiceForHost( @@ -1361,12 +1278,10 @@ export async function verifyU2FRegistration({ */ export async function getActiveIdentityProviders({ serviceUrl, - orgId, linking_allowed, }: { serviceUrl: string; - orgId?: string; linking_allowed?: boolean; }) { @@ -1388,11 +1303,9 @@ export async function getActiveIdentityProviders({ */ export async function verifyPasskeyRegistration({ serviceUrl, - request, }: { serviceUrl: string; - request: VerifyPasskeyRegistrationRequest; }) { const userService: Client = await createServiceForHost( @@ -1413,13 +1326,11 @@ export async function verifyPasskeyRegistration({ */ export async function registerPasskey({ serviceUrl, - userId, code, domain, }: { serviceUrl: string; - userId: string; code: { id: string; code: string }; domain: string; @@ -1447,7 +1358,6 @@ export async function listAuthenticationMethodTypes({ userId, }: { serviceUrl: string; - userId: string; }) { const userService: Client = await createServiceForHost( From f2000e130272e1b5568ea839a0a87f442f546b75 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 19 Feb 2025 10:11:10 +0100 Subject: [PATCH 09/19] cleanup fcn --- apps/login/src/lib/server/cookie.ts | 3 --- apps/login/src/lib/server/loginname.ts | 1 - apps/login/src/lib/server/password.ts | 1 - apps/login/src/lib/server/register.ts | 1 - apps/login/src/lib/server/verify.ts | 12 ++---------- apps/login/src/lib/zitadel.ts | 4 +--- 6 files changed, 3 insertions(+), 19 deletions(-) diff --git a/apps/login/src/lib/server/cookie.ts b/apps/login/src/lib/server/cookie.ts index 6aa2e4427c..c5d9ced252 100644 --- a/apps/login/src/lib/server/cookie.ts +++ b/apps/login/src/lib/server/cookie.ts @@ -48,7 +48,6 @@ const passwordAttemptsHandler = (error: ConnectError) => { export async function createSessionAndUpdateCookie( checks: Checks, - challenges: RequestChallenges | undefined, requestId: string | undefined, lifetime?: Duration, ): Promise { @@ -57,9 +56,7 @@ export async function createSessionAndUpdateCookie( const createdSession = await createSessionFromChecks({ serviceUrl, - checks, - challenges, lifetime, }); diff --git a/apps/login/src/lib/server/loginname.ts b/apps/login/src/lib/server/loginname.ts index 9a34431eaa..382f9a409a 100644 --- a/apps/login/src/lib/server/loginname.ts +++ b/apps/login/src/lib/server/loginname.ts @@ -241,7 +241,6 @@ export async function sendLoginname(command: SendLoginnameCommand) { const session = await createSessionAndUpdateCookie( checks, - undefined, command.requestId, ); diff --git a/apps/login/src/lib/server/password.ts b/apps/login/src/lib/server/password.ts index 0cdc24845e..98c34a3de6 100644 --- a/apps/login/src/lib/server/password.ts +++ b/apps/login/src/lib/server/password.ts @@ -127,7 +127,6 @@ export async function sendPassword(command: UpdateSessionCommand) { try { session = await createSessionAndUpdateCookie( checks, - undefined, command.requestId, loginSettings?.passwordCheckLifetime, ); diff --git a/apps/login/src/lib/server/register.ts b/apps/login/src/lib/server/register.ts index 79a14e87e4..36103e9bf9 100644 --- a/apps/login/src/lib/server/register.ts +++ b/apps/login/src/lib/server/register.ts @@ -71,7 +71,6 @@ export async function registerUser(command: RegisterUserCommand) { const session = await createSessionAndUpdateCookie( checks, - undefined, command.requestId, command.password ? loginSettings?.passwordCheckLifetime : undefined, ); diff --git a/apps/login/src/lib/server/verify.ts b/apps/login/src/lib/server/verify.ts index cde12b4f24..38bddb9428 100644 --- a/apps/login/src/lib/server/verify.ts +++ b/apps/login/src/lib/server/verify.ts @@ -155,11 +155,7 @@ export async function sendVerification(command: VerifyUserByEmailCommand) { }, }); - session = await createSessionAndUpdateCookie( - checks, - undefined, - command.requestId, - ); + session = await createSessionAndUpdateCookie(checks, command.requestId); } if (!session?.factors?.user?.id) { @@ -368,11 +364,7 @@ export async function sendVerificationRedirectWithoutCheck( }, }); - session = await createSessionAndUpdateCookie( - checks, - undefined, - command.requestId, - ); + session = await createSessionAndUpdateCookie(checks, command.requestId); } if (!session?.factors?.user?.id) { diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index 4afb429a20..77d656beda 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -237,18 +237,16 @@ 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({ From 8944e3885150471fd64865941836fe89cb56065a Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Thu, 20 Feb 2025 10:21:39 +0100 Subject: [PATCH 10/19] same file callback --- apps/login/src/app/login/route.ts | 127 ++++++++++++++++++++++++++++- apps/login/src/lib/oidc.ts | 131 ------------------------------ 2 files changed, 124 insertions(+), 134 deletions(-) delete mode 100644 apps/login/src/lib/oidc.ts diff --git a/apps/login/src/app/login/route.ts b/apps/login/src/app/login/route.ts index 63c85df4b9..9d9870fdae 100644 --- a/apps/login/src/app/login/route.ts +++ b/apps/login/src/app/login/route.ts @@ -1,15 +1,15 @@ -import { getAllSessions } from "@/lib/cookies"; +import { Cookie, 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 { findValidSession, isSessionValid } from "@/lib/session"; import { createCallback, createResponse, getActiveIdentityProviders, getAuthRequest, + getLoginSettings, getOrgsByDomain, getSAMLRequest, listSessions, @@ -528,3 +528,124 @@ export async function GET(request: NextRequest) { ); } } + +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/oidc.ts b/apps/login/src/lib/oidc.ts deleted file mode 100644 index fd74e57027..0000000000 --- a/apps/login/src/lib/oidc.ts +++ /dev/null @@ -1,131 +0,0 @@ -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 }); - } - } - } - } -} From 0311238dcf8bd892b35de7749f77d9909ed2dcd1 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Thu, 20 Feb 2025 10:22:21 +0100 Subject: [PATCH 11/19] Revert "same file callback" This reverts commit 8944e3885150471fd64865941836fe89cb56065a. --- apps/login/src/app/login/route.ts | 127 +---------------------------- apps/login/src/lib/oidc.ts | 131 ++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 124 deletions(-) create mode 100644 apps/login/src/lib/oidc.ts diff --git a/apps/login/src/app/login/route.ts b/apps/login/src/app/login/route.ts index 9d9870fdae..63c85df4b9 100644 --- a/apps/login/src/app/login/route.ts +++ b/apps/login/src/app/login/route.ts @@ -1,15 +1,15 @@ -import { Cookie, getAllSessions } from "@/lib/cookies"; +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, isSessionValid } from "@/lib/session"; +import { findValidSession } from "@/lib/session"; import { createCallback, createResponse, getActiveIdentityProviders, getAuthRequest, - getLoginSettings, getOrgsByDomain, getSAMLRequest, listSessions, @@ -528,124 +528,3 @@ export async function GET(request: NextRequest) { ); } } - -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/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 }); + } + } + } + } +} From 52a99d3840f00411c5f6366169535eee92193056 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Thu, 20 Feb 2025 10:23:20 +0100 Subject: [PATCH 12/19] return statement --- apps/login/src/app/login/route.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/login/src/app/login/route.ts b/apps/login/src/app/login/route.ts index 63c85df4b9..dd91519ee4 100644 --- a/apps/login/src/app/login/route.ts +++ b/apps/login/src/app/login/route.ts @@ -104,7 +104,7 @@ export async function GET(request: NextRequest) { if (requestId && sessionId) { if (requestId.startsWith("oidc_")) { // this finishes the login process for OIDC - await loginWithOIDCandSession({ + return loginWithOIDCandSession({ serviceUrl, authRequest: requestId.replace("oidc_", ""), sessionId, @@ -114,7 +114,7 @@ export async function GET(request: NextRequest) { }); } else if (requestId.startsWith("saml_")) { // this finishes the login process for SAML - await loginWithSAMLandSession({ + return loginWithSAMLandSession({ serviceUrl, samlRequest: requestId.replace("saml_", ""), sessionId, From 2e5e4f87d509f46e444bb513d6e3cfc0a8d8e5b5 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 26 Feb 2025 09:48:52 +0100 Subject: [PATCH 13/19] implement mfa init prompt --- apps/login/locales/de.json | 3 +- apps/login/locales/en.json | 3 +- apps/login/locales/es.json | 3 +- apps/login/locales/it.json | 3 +- apps/login/locales/zh.json | 3 +- apps/login/src/app/(login)/mfa/set/page.tsx | 6 ++ apps/login/src/lib/server/password.ts | 7 ++- apps/login/src/lib/server/verify.ts | 7 ++- apps/login/src/lib/verify-helper.ts | 63 +++++++++++++-------- 9 files changed, 62 insertions(+), 36 deletions(-) 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/src/app/(login)/mfa/set/page.tsx b/apps/login/src/app/(login)/mfa/set/page.tsx index a885a36c7a..a8f16f23f2 100644 --- a/apps/login/src/app/(login)/mfa/set/page.tsx +++ b/apps/login/src/app/(login)/mfa/set/page.tsx @@ -161,6 +161,12 @@ export default async function Page(props: { > )} + {force !== "true" && ( +
+

{t("set.skip")}

+
+ )} +
diff --git a/apps/login/src/lib/server/password.ts b/apps/login/src/lib/server/password.ts index 98c34a3de6..6fd3f67bcd 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 { @@ -267,7 +267,8 @@ export async function sendPassword(command: UpdateSessionCommand) { return { error: "Could not verify password!" }; } - const mfaFactorCheck = checkMFAFactors( + const mfaFactorCheck = await checkMFAFactors( + serviceUrl, session, loginSettings, authMethods, @@ -433,7 +434,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/verify.ts b/apps/login/src/lib/server/verify.ts index 38bddb9428..4de697e772 100644 --- a/apps/login/src/lib/server/verify.ts +++ b/apps/login/src/lib/server/verify.ts @@ -203,7 +203,8 @@ 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, @@ -407,12 +408,12 @@ 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, diff --git a/apps/login/src/lib/verify-helper.ts b/apps/login/src/lib/verify-helper.ts index e8afef6890..fcc46e175c 100644 --- a/apps/login/src/lib/verify-helper.ts +++ b/apps/login/src/lib/verify-helper.ts @@ -5,6 +5,7 @@ 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, @@ -100,7 +101,8 @@ export function checkEmailVerification( } } -export function checkMFAFactors( +export async function checkMFAFactors( + serviceUrl: string, session: Session, loginSettings: LoginSettings | undefined, authMethods: AuthenticationMethodType[], @@ -188,31 +190,42 @@ export function checkMFAFactors( ); } + // 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 user = await getUserByID({ + serviceUrl, + userId: session.factors?.user?.id, + }); + if ( + user.user?.type?.case === "human" && + user.user?.type?.value.mfaInitSkipped + ) { + } + 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) { + params.append( + "organization", + organization ?? (session.factors?.user?.organizationId as string), + ); + } + // TODO: provide a way to setup passkeys on mfa page? return { redirect: `/mfa/set?` + params }; } - - // TODO: implement passkey setup - - // else if ( - // submitted.factors && - // !submitted.factors.webAuthN && // if session was not verified with a passkey - // promptPasswordless && // if explicitly prompted due policy - // !isAlternative // escaped if password was used as an alternative method - // ) { - // const params = new URLSearchParams({ - // loginName: submitted.factors.user.loginName, - // prompt: "true", - // }); - - // if (requestId) { - // params.append("requestId", requestId); - // } - - // if (organization) { - // params.append("organization", organization); - // } - - // return router.push(`/passkey/set?` + params); - // } } From 546edee64ffbc6e06f5346240d04224c43907510 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 26 Feb 2025 16:37:52 +0100 Subject: [PATCH 14/19] fix time check --- apps/login/src/lib/verify-helper.ts | 50 ++++++++++++++++++----------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/apps/login/src/lib/verify-helper.ts b/apps/login/src/lib/verify-helper.ts index fcc46e175c..41a537cde1 100644 --- a/apps/login/src/lib/verify-helper.ts +++ b/apps/login/src/lib/verify-helper.ts @@ -203,29 +203,43 @@ export async function checkMFAFactors( serviceUrl, userId: session.factors?.user?.id, }); + if ( user.user?.type?.case === "human" && user.user?.type?.value.mfaInitSkipped ) { - } - 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) { - params.append( - "organization", - organization ?? (session.factors?.user?.organizationId as string), + const mfaInitSkippedTimestamp = timestampDate( + user.user.type.value.mfaInitSkipped, ); - } - // TODO: provide a way to setup passkeys on mfa page? - return { redirect: `/mfa/set?` + params }; + 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) { + 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) { + 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 }; + } + } } } From 83df30e525a5c8e0c6ca11f8bf178191a84424a8 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Fri, 28 Feb 2025 15:22:56 +0100 Subject: [PATCH 15/19] skip button and server action --- apps/login/src/app/(login)/mfa/set/page.tsx | 29 ++--- .../choose-second-factor-to-setup.tsx | 101 ++++++++++++------ apps/login/src/lib/server/session.ts | 48 +++++++++ apps/login/src/lib/verify-helper.ts | 61 ++++++----- apps/login/src/lib/zitadel.ts | 15 +++ 5 files changed, 173 insertions(+), 81 deletions(-) diff --git a/apps/login/src/app/(login)/mfa/set/page.tsx b/apps/login/src/app/(login)/mfa/set/page.tsx index a8f16f23f2..498b063428 100644 --- a/apps/login/src/app/(login)/mfa/set/page.tsx +++ b/apps/login/src/app/(login)/mfa/set/page.tsx @@ -49,10 +49,10 @@ export default async function Page(props: { 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) { @@ -80,7 +80,6 @@ export default async function Page(props: { } async function loadSessionByLoginname( - host: string, loginName?: string, organization?: string, ) { @@ -92,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); }); } @@ -147,8 +141,10 @@ export default async function Page(props: { {isSessionValid(sessionWithData).valid && loginSettings && - sessionWithData && ( + sessionWithData && + sessionWithData.factors?.user?.id && ( )} - {force !== "true" && ( -
-

{t("set.skip")}

-
- )} -
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 21f7aff8a6..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,13 +1,17 @@ "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; requestId?: string; @@ -17,9 +21,11 @@ type Props = { checkAfter: boolean; phoneVerified: boolean; emailVerified: boolean; + force: boolean; }; export function ChooseSecondFactorToSetup({ + userId, loginName, sessionId, requestId, @@ -29,7 +35,10 @@ export function ChooseSecondFactorToSetup({ checkAfter, phoneVerified, emailVerified, + force, }: Props) { + const t = useTranslations("mfa"); + const router = useRouter(); const params = new URLSearchParams({}); if (loginName) { @@ -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/lib/server/session.ts b/apps/login/src/lib/server/session.ts index 7ba37011ad..440a3290eb 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,6 +21,53 @@ 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, + }); + + const skip = 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({ requestId, ...session diff --git a/apps/login/src/lib/verify-helper.ts b/apps/login/src/lib/verify-helper.ts index 41a537cde1..704d7bbef6 100644 --- a/apps/login/src/lib/verify-helper.ts +++ b/apps/login/src/lib/verify-helper.ts @@ -199,18 +199,18 @@ export async function checkMFAFactors( !availableMultiFactors.length && session?.factors?.user?.id ) { - const user = await getUserByID({ + const userResponse = await getUserByID({ serviceUrl, userId: session.factors?.user?.id, }); - if ( - user.user?.type?.case === "human" && - user.user?.type?.value.mfaInitSkipped - ) { - const mfaInitSkippedTimestamp = timestampDate( - user.user.type.value.mfaInitSkipped, - ); + 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 + @@ -219,27 +219,32 @@ export async function checkMFAFactors( const mfaInitSkippedTime = mfaInitSkippedTimestamp.getTime(); const timeDifference = currentTime - mfaInitSkippedTime; - if (timeDifference > mfaInitSkipLifetimeMillis) { - 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) { - 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 }; + 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) { + 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 }; } } diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index 77d656beda..5e3fdec323 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -457,6 +457,21 @@ 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, From 9299a065916f0481b29292fd9d9e53998601b040 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Fri, 28 Feb 2025 16:42:32 +0100 Subject: [PATCH 16/19] cleanup --- apps/login/src/lib/server/password.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/login/src/lib/server/password.ts b/apps/login/src/lib/server/password.ts index 6fd3f67bcd..e5a2f58562 100644 --- a/apps/login/src/lib/server/password.ts +++ b/apps/login/src/lib/server/password.ts @@ -72,7 +72,6 @@ export async function resetPassword(command: ResetPasswordCommand) { return passwordReset({ serviceUrl, - userId, urlTemplate: `${host.includes("localhost") ? "http://" : "https://"}${host}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` + From 87dcefce1a0e0227f9df76fdc0b827ed22ff9e56 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Fri, 28 Feb 2025 16:45:38 +0100 Subject: [PATCH 17/19] cleanup --- apps/login/src/lib/server/session.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/login/src/lib/server/session.ts b/apps/login/src/lib/server/session.ts index 440a3290eb..6a1aa5af9b 100644 --- a/apps/login/src/lib/server/session.ts +++ b/apps/login/src/lib/server/session.ts @@ -42,7 +42,7 @@ export async function skipMFAAndContinueWithNextUrl({ organization: organization, }); - const skip = await humanMFAInitSkipped({ serviceUrl, userId }); + await humanMFAInitSkipped({ serviceUrl, userId }); const url = requestId && sessionId From 5819dc509d95dc48b02743a865e392005c0b0030 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 3 Mar 2025 08:43:33 +0100 Subject: [PATCH 18/19] edit zitadel default setting for tests --- acceptance/zitadel.yaml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) 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" From 5432a506a8f9c35550510a8cef4f29146c9333e1 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 3 Mar 2025 08:53:37 +0100 Subject: [PATCH 19/19] update readme --- apps/login/readme.md | 1 - 1 file changed, 1 deletion(-) 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.