From ee898c447d1b280572d017667cfd00e309016c5b Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 17 Feb 2025 09:23:46 +0100 Subject: [PATCH] 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 }, + ); } }