Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- 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.
118 lines
3.8 KiB
TypeScript
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,
|
|
}
|
|
}
|