Neah/lib/cookies.ts
2025-05-02 11:41:43 +02:00

193 lines
5.3 KiB
TypeScript

import { serialize, parse } from 'cookie';
import { IncomingMessage } from 'http';
import { NextApiRequestCookies } from 'next/dist/server/api-utils';
export interface CookieOptions {
maxAge?: number;
expires?: Date;
path?: string;
domain?: string;
secure?: boolean;
httpOnly?: boolean;
sameSite?: 'strict' | 'lax' | 'none';
}
// Default cookie options for auth-related cookies
export const DEFAULT_AUTH_COOKIE_OPTIONS: CookieOptions = {
maxAge: 30 * 24 * 60 * 60, // 30 days
path: '/',
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax'
};
// Cookie names
export const COOKIE_NAMES = {
// NextAuth cookies
AUTH_TOKEN: 'next-auth.session-token',
AUTH_CSRF_TOKEN: 'next-auth.csrf-token',
AUTH_CALLBACK_URL: 'next-auth.callback-url',
AUTH_PKCE_CODE_CHALLENGE: 'next-auth.pkce.code_challenge',
// Keycloak cookies
KEYCLOAK_SESSION: 'KEYCLOAK_SESSION',
KEYCLOAK_IDENTITY: 'KEYCLOAK_IDENTITY',
KEYCLOAK_REMEMBER_ME: 'KEYCLOAK_REMEMBER_ME',
KC_RESTART: 'KC_RESTART',
// Custom cookies
USER_PREFERENCES: 'user-preferences',
THEME: 'theme',
// Function to create a namespaced cookie name
namespaced: (name: string) => `neah-front9.${name}`
};
/**
* Set a cookie with the specified options
*/
export function setCookie(
name: string,
value: string,
options: CookieOptions = {}
): string {
// Merge with default options
const cookieOptions = {
...DEFAULT_AUTH_COOKIE_OPTIONS,
...options
};
// For security, ensure secure flag is set if sameSite is 'none'
if (cookieOptions.sameSite === 'none' && cookieOptions.secure === undefined) {
cookieOptions.secure = true;
}
// Create the cookie string
return serialize(name, value, cookieOptions as any);
}
/**
* Get all cookies from the request
*/
export function getCookies(req: {
headers: { cookie?: string };
}): Record<string, string> {
const cookie = req.headers?.cookie;
return parse(cookie || '');
}
/**
* Get a specific cookie value
*/
export function getCookie(
req: { headers: { cookie?: string } },
name: string
): string | undefined {
const cookies = getCookies(req);
return cookies[name];
}
/**
* Delete a cookie by setting its expiration to the past
*/
export function deleteCookie(
name: string,
options: CookieOptions = {}
): string {
return setCookie(name, '', {
...options,
maxAge: 0,
expires: new Date(0)
});
}
/**
* Helper to generate SetCookie headers for multiple cookies
*/
export function createCookieHeaders(cookieStrings: string[]): [string, string][] {
return cookieStrings.map(cookie => ['Set-Cookie', cookie]);
}
/**
* Clear all auth-related cookies
*/
export function getAuthCookieClearingHeaders(): [string, string][] {
const authCookies = [
COOKIE_NAMES.AUTH_TOKEN,
COOKIE_NAMES.AUTH_CSRF_TOKEN,
COOKIE_NAMES.AUTH_CALLBACK_URL,
COOKIE_NAMES.AUTH_PKCE_CODE_CHALLENGE,
COOKIE_NAMES.KEYCLOAK_SESSION,
COOKIE_NAMES.KEYCLOAK_IDENTITY,
COOKIE_NAMES.KEYCLOAK_REMEMBER_ME,
COOKIE_NAMES.KC_RESTART,
// Also clear secure variants
`__Secure-${COOKIE_NAMES.AUTH_TOKEN}`,
`__Host-${COOKIE_NAMES.AUTH_TOKEN}`
];
// Create clearing headers for root path
const cookieHeaders = authCookies.flatMap(name => {
return [
deleteCookie(name, { path: '/' }),
deleteCookie(name, { path: '/auth' }),
deleteCookie(name, { path: '/api' })
];
});
return createCookieHeaders(cookieHeaders);
}
/**
* Client-side function to clear all auth cookies
*/
export function clearAuthCookiesClient(): void {
const authCookiePrefixes = [
'next-auth.',
'__Secure-next-auth.',
'__Host-next-auth.',
'KEYCLOAK_',
'KC_'
];
const specificCookies = [
COOKIE_NAMES.KEYCLOAK_SESSION,
COOKIE_NAMES.KEYCLOAK_IDENTITY,
COOKIE_NAMES.KEYCLOAK_REMEMBER_ME,
COOKIE_NAMES.KC_RESTART
];
const cookies = document.cookie.split(';');
for (const cookie of cookies) {
const [name] = cookie.split('=');
const trimmedName = name.trim();
const isAuthCookie =
authCookiePrefixes.some(prefix => trimmedName.startsWith(prefix)) ||
specificCookies.includes(trimmedName);
if (isAuthCookie) {
// Clear cookie for different paths and domains
const paths = ['/', '/auth', '/api'];
for (const path of paths) {
// Basic deletion
document.cookie = `${trimmedName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path};`;
// With Secure and SameSite
document.cookie = `${trimmedName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}; SameSite=None; Secure;`;
// Try with domain
const domain = window.location.hostname;
document.cookie = `${trimmedName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}; domain=${domain};`;
document.cookie = `${trimmedName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}; domain=${domain}; SameSite=None; Secure;`;
// Try with root domain
const rootDomain = `.${domain.split('.').slice(-2).join('.')}`;
document.cookie = `${trimmedName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}; domain=${rootDomain};`;
document.cookie = `${trimmedName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}; domain=${rootDomain}; SameSite=None; Secure;`;
}
}
}
}