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

103 lines
2.6 KiB
TypeScript

import type { NextResponse } from "next/server"
/** 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
}
export function sessionCookieOptions() {
return {
httpOnly: true,
sameSite: "lax" as const,
path: "/",
maxAge: SESSION_MAX_AGE_SEC,
secure: process.env.NODE_ENV === "production",
}
}
export function computeExpiresAt(expiresIn: number): number {
return Date.now() + expiresIn * 1000
}
export function isAccessTokenValid(
accessToken: string | undefined,
expiresAtRaw: string | undefined
): boolean {
if (!accessToken || !expiresAtRaw) return false
const expiresAt = Number(expiresAtRaw)
return Number.isFinite(expiresAt) && Date.now() < expiresAt
}
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" },
body,
})
if (!res.ok) {
throw new Error(`refresh_failed:${res.status}`)
}
return (await res.json()) as TokenResponse
}
export function resolveBearerToken(tokens: TokenResponse): string {
const bearer = tokens.id_token ?? tokens.access_token
if (!bearer) {
throw new Error("no_token_in_response")
}
return bearer
}
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
)
}