ultisuite-client/lib/auth/oidc-config.ts
2026-05-25 13:52:40 +02:00

123 lines
3.3 KiB
TypeScript

/** OIDC settings for local dev (Authentik blueprints in ulti-backend). */
function trimSlash(url: string) {
return url.endsWith("/") ? url : `${url}/`
}
/** Public app origin for OAuth redirects and post-login navigation (never 0.0.0.0). */
export function getAppOrigin(): string {
const raw = (
process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000"
).replace(/\/$/, "")
try {
const url = new URL(raw)
if (url.hostname === "0.0.0.0") {
url.hostname = "localhost"
}
return url.origin
} catch {
return "http://localhost:3000"
}
}
/** Base URL Authentik (sans segment application OIDC). */
export function getAuthentikBase(): string {
const issuer = trimSlash(
process.env.NEXT_PUBLIC_OIDC_ISSUER ??
"http://localhost/auth/application/o/ulti/"
)
return issuer.replace(/application\/o\/[^/]+\/?$/, "")
}
export function getAuthentikEnrollmentUrl(): string {
return `${getAuthentikBase()}if/flow/ulti-enrollment/`
}
type OidcDiscovery = {
authorization_endpoint: string
token_endpoint: string
end_session_endpoint?: string
}
type OidcConfig = {
issuer: string
clientId: string
appUrl: string
redirectUri: string
authorizationEndpoint: string
tokenEndpoint: string
endSessionEndpoint: string
}
let discoveryCache: { issuer: string; doc: OidcDiscovery; at: number } | null =
null
const DISCOVERY_TTL_MS = 5 * 60 * 1000
export function getPublicOidcConfig(): OidcConfig {
const issuer = trimSlash(
process.env.NEXT_PUBLIC_OIDC_ISSUER ??
"http://localhost/auth/application/o/ulti/"
)
const clientId =
process.env.NEXT_PUBLIC_OIDC_CLIENT_ID ?? "ulti-backend"
const appUrl = getAppOrigin()
return {
issuer,
clientId,
appUrl,
redirectUri: `${appUrl}/api/auth/callback`,
authorizationEndpoint: "",
tokenEndpoint: "",
endSessionEndpoint: `${issuer}end-session/`,
}
}
/** Resolve authorize/token URLs from issuer discovery (Authentik uses shared /o/ endpoints). */
export async function resolveOidcConfig(): Promise<OidcConfig> {
const base = getPublicOidcConfig()
const now = Date.now()
if (
discoveryCache &&
discoveryCache.issuer === base.issuer &&
now - discoveryCache.at < DISCOVERY_TTL_MS
) {
return applyDiscovery(base, discoveryCache.doc)
}
const res = await fetch(
`${base.issuer}.well-known/openid-configuration`,
{ next: { revalidate: 300 } }
)
if (!res.ok) {
throw new Error(`OIDC discovery failed (${res.status}) for ${base.issuer}`)
}
const doc = (await res.json()) as OidcDiscovery
discoveryCache = { issuer: base.issuer, doc, at: now }
return applyDiscovery(base, doc)
}
function applyDiscovery(base: OidcConfig, doc: OidcDiscovery): OidcConfig {
return {
...base,
authorizationEndpoint: doc.authorization_endpoint,
tokenEndpoint: doc.token_endpoint,
endSessionEndpoint:
doc.end_session_endpoint ?? base.endSessionEndpoint,
}
}
export async function resolveServerOidcConfig(): Promise<
OidcConfig & { clientSecret: string }
> {
const pub = await resolveOidcConfig()
const clientSecret = process.env.OIDC_CLIENT_SECRET ?? "changeme"
return { ...pub, clientSecret }
}
/** OIDC login enabled unless explicitly disabled (default: on for local Authentik stack). */
export function isOidcConfigured() {
return process.env.NEXT_PUBLIC_OIDC_DISABLED !== "true"
}