123 lines
3.3 KiB
TypeScript
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"
|
|
}
|