176 lines
4.8 KiB
TypeScript
176 lines
4.8 KiB
TypeScript
import { SignJWT, jwtVerify, type JWTPayload } from 'jose';
|
|
import { serialize, parse } from 'cookie';
|
|
import { config } from 'dotenv';
|
|
|
|
// Load environment variables
|
|
config();
|
|
|
|
// Environment variables - use runtime access for server-side
|
|
function getEnv(key: string): string {
|
|
const value = process.env[key];
|
|
if (!value) {
|
|
throw new Error(`Missing environment variable: ${key}`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
const SECRET_KEY = new TextEncoder().encode(getEnv('OIDC_CLIENT_SECRET'));
|
|
const SESSION_DURATION = 6 * 60 * 60; // 6 hours in seconds
|
|
|
|
export interface SessionData {
|
|
userId: string;
|
|
authenticated: boolean;
|
|
exp: number;
|
|
}
|
|
|
|
// Create a signed JWT session token
|
|
export async function createSession(userId: string): Promise<string> {
|
|
const exp = Math.floor(Date.now() / 1000) + SESSION_DURATION;
|
|
|
|
return await new SignJWT({
|
|
userId,
|
|
authenticated: true,
|
|
exp
|
|
})
|
|
.setProtectedHeader({ alg: 'HS256' })
|
|
.setExpirationTime(exp)
|
|
.sign(SECRET_KEY);
|
|
}
|
|
|
|
// Verify and decode a session token
|
|
export async function verifySession(token: string): Promise<SessionData | null> {
|
|
try {
|
|
const { payload } = await jwtVerify(token, SECRET_KEY);
|
|
|
|
// Validate payload structure and cast properly
|
|
if (
|
|
typeof payload.userId === 'string' &&
|
|
typeof payload.authenticated === 'boolean' &&
|
|
typeof payload.exp === 'number'
|
|
) {
|
|
return {
|
|
userId: payload.userId,
|
|
authenticated: payload.authenticated,
|
|
exp: payload.exp
|
|
};
|
|
}
|
|
|
|
return null;
|
|
} catch (error) {
|
|
console.log('Session verification failed:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Get session from request cookies
|
|
export function getSessionFromRequest(request: Request): string | null {
|
|
const cookieHeader = request.headers.get('cookie');
|
|
if (!cookieHeader) return null;
|
|
|
|
const cookies = parse(cookieHeader);
|
|
return cookies.session || null;
|
|
}
|
|
|
|
// Create session cookie
|
|
export function createSessionCookie(token: string): string {
|
|
const publicBaseUrl = getEnv('PUBLIC_BASE_URL');
|
|
const isSecure = publicBaseUrl.startsWith('https://');
|
|
|
|
return serialize('session', token, {
|
|
httpOnly: true,
|
|
secure: isSecure,
|
|
sameSite: 'lax',
|
|
maxAge: SESSION_DURATION,
|
|
path: '/'
|
|
});
|
|
}
|
|
|
|
// Clear session cookie
|
|
export function clearSessionCookie(): string {
|
|
const publicBaseUrl = getEnv('PUBLIC_BASE_URL');
|
|
const isSecure = publicBaseUrl.startsWith('https://');
|
|
|
|
return serialize('session', '', {
|
|
httpOnly: true,
|
|
secure: isSecure,
|
|
sameSite: 'lax',
|
|
maxAge: 0,
|
|
path: '/'
|
|
});
|
|
}
|
|
|
|
// Generate OIDC authorization URL
|
|
export function generateAuthUrl(state: string): string {
|
|
const oidcEndpoint = getEnv('OIDC_ENDPOINT');
|
|
const clientId = getEnv('OIDC_CLIENT_ID');
|
|
const publicBaseUrl = getEnv('PUBLIC_BASE_URL');
|
|
|
|
const params = new URLSearchParams({
|
|
response_type: 'code',
|
|
client_id: clientId,
|
|
redirect_uri: `${publicBaseUrl}/auth/callback`,
|
|
scope: 'openid profile email',
|
|
state: state
|
|
});
|
|
|
|
return `${oidcEndpoint}/apps/oidc/authorize?${params.toString()}`;
|
|
}
|
|
|
|
// Exchange authorization code for tokens
|
|
export async function exchangeCodeForTokens(code: string): Promise<any> {
|
|
const oidcEndpoint = getEnv('OIDC_ENDPOINT');
|
|
const clientId = getEnv('OIDC_CLIENT_ID');
|
|
const clientSecret = getEnv('OIDC_CLIENT_SECRET');
|
|
const publicBaseUrl = getEnv('PUBLIC_BASE_URL');
|
|
|
|
const response = await fetch(`${oidcEndpoint}/apps/oidc/token`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'Authorization': `Basic ${btoa(`${clientId}:${clientSecret}`)}`
|
|
},
|
|
body: new URLSearchParams({
|
|
grant_type: 'authorization_code',
|
|
code: code,
|
|
redirect_uri: `${publicBaseUrl}/auth/callback`
|
|
})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.text();
|
|
console.error('Token exchange failed:', error);
|
|
throw new Error('Failed to exchange authorization code');
|
|
}
|
|
|
|
return await response.json();
|
|
}
|
|
|
|
// Get user info from OIDC provider
|
|
export async function getUserInfo(accessToken: string): Promise<any> {
|
|
const oidcEndpoint = getEnv('OIDC_ENDPOINT');
|
|
|
|
const response = await fetch(`${oidcEndpoint}/apps/oidc/userinfo`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${accessToken}`
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.text();
|
|
console.error('Userinfo request failed:', error);
|
|
throw new Error('Failed to get user info');
|
|
}
|
|
|
|
return await response.json();
|
|
}
|
|
|
|
// Generate random state for CSRF protection
|
|
export function generateState(): string {
|
|
return crypto.randomUUID();
|
|
}
|
|
|
|
// Log authentication events for debugging
|
|
export function logAuthEvent(event: string, details?: any) {
|
|
const timestamp = new Date().toISOString();
|
|
console.log(`[AUTH ${timestamp}] ${event}`, details ? JSON.stringify(details) : '');
|
|
} |