first ai implementation, oidc done
This commit is contained in:
176
src/utils/auth.ts
Normal file
176
src/utils/auth.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
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) : '');
|
||||
}
|
||||
40
src/utils/serverAuth.ts
Normal file
40
src/utils/serverAuth.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { AstroGlobal } from 'astro';
|
||||
import { getSessionFromRequest, verifySession, type SessionData } from './auth.js';
|
||||
|
||||
export interface AuthContext {
|
||||
authenticated: boolean;
|
||||
session: SessionData | null;
|
||||
}
|
||||
|
||||
// Check authentication status for server-side pages
|
||||
export async function getAuthContext(Astro: AstroGlobal): Promise<AuthContext> {
|
||||
try {
|
||||
const sessionToken = getSessionFromRequest(Astro.request);
|
||||
|
||||
if (!sessionToken) {
|
||||
return { authenticated: false, session: null };
|
||||
}
|
||||
|
||||
const session = await verifySession(sessionToken);
|
||||
|
||||
return {
|
||||
authenticated: session !== null,
|
||||
session
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to get auth context:', error);
|
||||
return { authenticated: false, session: null };
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect to login if not authenticated
|
||||
export function requireAuth(authContext: AuthContext, currentUrl: string): Response | null {
|
||||
if (!authContext.authenticated) {
|
||||
const loginUrl = `/api/auth/login?returnTo=${encodeURIComponent(currentUrl)}`;
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: { 'Location': loginUrl }
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user