ultisuite-client/app/api/auth/login/route.web.ts
R3D347HR4Y 9ea2d3325d
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
feat(auth): enhance authentication flows with embedded support and UI improvements
- Updated login and signup components to utilize AuthCard for better user experience during redirection.
- Introduced AuthentikEmbedDialog for seamless integration of Authentik's identity portal within the application.
- Enhanced password recovery and signup flows with dynamic theme handling and improved loading states.
- Refactored existing components to streamline authentication processes and improve maintainability.
2026-06-21 00:12:45 +02:00

118 lines
3.8 KiB
TypeScript

import { cookies } from "next/headers"
import { NextResponse } from "next/server"
import { createPkcePair, randomString } from "@/lib/auth/pkce"
import { platformUserFromToken } from "@/lib/auth/jwt-claims"
import { resolveOidcConfig, getAppOrigin } from "@/lib/auth/oidc-config"
import { sessionCookieOptions } from "@/lib/auth/session"
const PKCE_COOKIE = "ulti_pkce_verifier"
const STATE_COOKIE = "ulti_oauth_state"
const INTENT_COOKIE = "ulti_auth_intent"
const PREVIOUS_SUB_COOKIE = "ulti_auth_previous_sub"
const COOKIE_MAX_AGE = 600
function oauthCookieOptions() {
return {
...sessionCookieOptions(),
maxAge: COOKIE_MAX_AGE,
}
}
export async function GET(request: Request) {
let cfg
try {
cfg = await resolveOidcConfig()
} catch (err) {
const message =
err instanceof Error ? err.message : "oidc_discovery_failed"
return NextResponse.redirect(
new URL(
`/login?error=${encodeURIComponent(message)}`,
getAppOrigin()
)
)
}
const { verifier, challenge } = await createPkcePair()
const state = randomString(16)
const requestUrl = new URL(request.url)
const returnTo = requestUrl.searchParams.get("returnTo") ?? "/mail/inbox"
const intent = requestUrl.searchParams.get("intent")
const bridge = requestUrl.searchParams.get("bridge") === "1"
// Embedded mode: the browser drives Authentik's flow executor (same origin) and authenticates
// the session in place, then navigates to the authorize URL. We just hand back the URL + the
// executor base, set the PKCE/state cookies, and never force a prompt.
const embedded = requestUrl.searchParams.get("embedded") === "1"
const promptParam = requestUrl.searchParams.get("prompt")
const prompt =
promptParam ??
(bridge || embedded
? null
: intent === "add_account"
? "login select_account"
: "select_account")
const jar = await cookies()
const existingUser = platformUserFromToken(
jar.get("ulti_access_token")?.value ?? ""
)
const params = new URLSearchParams({
client_id: cfg.clientId,
redirect_uri: cfg.redirectUri,
response_type: "code",
scope: "openid profile email offline_access",
state,
code_challenge: challenge,
code_challenge_method: "S256",
})
if (prompt) {
params.set("prompt", prompt)
}
const authorizeUrl = `${cfg.authorizationEndpoint}?${params.toString()}`
const response = embedded
? NextResponse.json(buildEmbeddedContext(authorizeUrl))
: NextResponse.redirect(authorizeUrl)
const cookieOpts = oauthCookieOptions()
response.cookies.set(PKCE_COOKIE, verifier, cookieOpts)
response.cookies.set(STATE_COOKIE, state, cookieOpts)
response.cookies.set("ulti_auth_return", returnTo, cookieOpts)
if (intent === "add_account") {
response.cookies.set(INTENT_COOKIE, "add_account", cookieOpts)
if (existingUser?.sub) {
response.cookies.set(PREVIOUS_SUB_COOKIE, existingUser.sub, cookieOpts)
}
}
return response
}
/**
* Build the same-origin flow executor base + `next` query from a public authorize URL.
* The browser drives the flow at `${executorBase}/${slug}/?query=${flowQuery}`, then navigates
* to the returned `to` (the authorize URL) to obtain the code with its authenticated session.
*/
function buildEmbeddedContext(authorizeUrl: string): {
authorizeUrl: string
flowQuery: string
executorBase: string
} {
let executorBase = "/auth/api/v3/flows/executor"
let next = authorizeUrl
try {
const parsed = new URL(authorizeUrl)
const idx = parsed.pathname.indexOf("/application/")
const prefix = idx >= 0 ? parsed.pathname.slice(0, idx) : "/auth"
executorBase = `${prefix}/api/v3/flows/executor`
next = `${parsed.pathname}${parsed.search}`
} catch {
// keep defaults
}
return {
authorizeUrl,
flowQuery: `next=${encodeURIComponent(next)}`,
executorBase,
}
}