ultisuite-client/lib/auth/session.ts
R3D347HR4Y 5304790ed5
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
feat(auth): enhance session management and identity provider settings
- Added SessionGuard component to manage session expiration and online status.
- Updated AuthProvider to streamline session fetching and handling.
- Introduced IdentityProvidersSection for managing OAuth, SAML, and LDAP identity providers.
- Implemented identity provider guides for easier configuration.
- Enhanced mail settings with infinite scroll option for improved user experience.
- Updated global styles and layout components for better consistency across the application.
2026-06-09 09:36:46 +02:00

127 lines
3.3 KiB
TypeScript

import type { NextResponse } from "next/server"
import { decodeJwtPayload } from "@/lib/auth/jwt-claims"
import { oidcServerFetchHeaders } from "@/lib/auth/oidc-config"
/** Ultimail session lifetime — independent of short-lived OIDC access tokens. */
export const SESSION_MAX_AGE_SEC = 60 * 60 * 24 * 365
export const SESSION_COOKIE_NAMES = {
session: "ulti_session",
accessToken: "ulti_access_token",
refreshToken: "ulti_refresh_token",
expiresAt: "ulti_expires_at",
} as const
export type TokenResponse = {
access_token: string
refresh_token?: string
expires_in?: number
id_token?: string
token_type?: string
}
function sessionCookieSecure(): boolean {
if (process.env.COOKIE_SECURE === "true") return true
if (process.env.COOKIE_SECURE === "false") return false
const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? ""
try {
return new URL(appUrl).protocol === "https:"
} catch {
return false
}
}
export function sessionCookieOptions() {
return {
httpOnly: true,
sameSite: "lax" as const,
path: "/",
maxAge: SESSION_MAX_AGE_SEC,
secure: sessionCookieSecure(),
}
}
export function computeExpiresAt(expiresIn: number): number {
return Date.now() + expiresIn * 1000
}
export function isIdTokenJwtValid(accessToken: string | undefined): boolean {
if (!accessToken) return false
const claims = decodeJwtPayload(accessToken)
const exp = claims?.exp
if (typeof exp !== "number") return false
return Date.now() < exp * 1000
}
export function isAccessTokenValid(
accessToken: string | undefined,
expiresAtRaw: string | undefined
): boolean {
if (!accessToken) return false
const expiresAt = Number(expiresAtRaw)
if (Number.isFinite(expiresAt) && Date.now() < expiresAt) return true
return isIdTokenJwtValid(accessToken)
}
type OidcTokenConfig = {
tokenEndpoint: string
clientId: string
clientSecret: string
}
export async function exchangeRefreshToken(
refreshToken: string,
cfg: OidcTokenConfig
): Promise<TokenResponse> {
const body = new URLSearchParams({
grant_type: "refresh_token",
client_id: cfg.clientId,
client_secret: cfg.clientSecret,
refresh_token: refreshToken,
})
const res = await fetch(cfg.tokenEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
...oidcServerFetchHeaders(),
},
body,
})
if (!res.ok) {
throw new Error(`refresh_failed:${res.status}`)
}
return (await res.json()) as TokenResponse
}
export function resolveBearerToken(tokens: TokenResponse): string {
if (!tokens.id_token) {
throw new Error("no_id_token_in_response")
}
return tokens.id_token
}
export function applySessionCookies(
response: NextResponse,
tokens: TokenResponse,
bearer?: string
) {
const token = bearer ?? resolveBearerToken(tokens)
const expiresIn = tokens.expires_in ?? 3600
const opts = sessionCookieOptions()
response.cookies.set(SESSION_COOKIE_NAMES.session, "1", opts)
response.cookies.set(SESSION_COOKIE_NAMES.accessToken, token, opts)
if (tokens.refresh_token) {
response.cookies.set(
SESSION_COOKIE_NAMES.refreshToken,
tokens.refresh_token,
opts
)
}
response.cookies.set(
SESSION_COOKIE_NAMES.expiresAt,
String(computeExpiresAt(expiresIn)),
opts
)
}