consolidation and unification
This commit is contained in:
@@ -3,6 +3,7 @@ import { SignJWT, jwtVerify, type JWTPayload } from 'jose';
|
||||
import { serialize, parse } from 'cookie';
|
||||
import { config } from 'dotenv';
|
||||
import type { AstroGlobal } from 'astro';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
// Load environment variables
|
||||
config();
|
||||
@@ -31,12 +32,25 @@ export interface SessionData {
|
||||
}
|
||||
|
||||
export interface UserInfo {
|
||||
sub?: string;
|
||||
sub: string;
|
||||
preferred_username?: string;
|
||||
email?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface AuthContext {
|
||||
authenticated: boolean;
|
||||
session: SessionData | null;
|
||||
userEmail: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export interface AuthStateData {
|
||||
state: string;
|
||||
returnTo: string;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Create a signed JWT session token with email
|
||||
export async function createSession(userId: string, email: string): Promise<string> {
|
||||
const exp = Math.floor(Date.now() / 1000) + SESSION_DURATION;
|
||||
@@ -97,7 +111,7 @@ export function createSessionCookie(token: string): string {
|
||||
return serialize('session', token, {
|
||||
httpOnly: true,
|
||||
secure: isSecure,
|
||||
sameSite: 'strict', // More secure than 'lax'
|
||||
sameSite: 'lax',
|
||||
maxAge: SESSION_DURATION,
|
||||
path: '/'
|
||||
});
|
||||
@@ -199,13 +213,91 @@ export function getUserEmail(userInfo: UserInfo): string {
|
||||
`${userInfo.preferred_username || userInfo.sub || 'unknown'}@cc24.dev`;
|
||||
}
|
||||
|
||||
// === CONSOLIDATION: Server-side Auth Helpers ===
|
||||
/**
|
||||
* CONSOLIDATED: Parse and validate auth state from cookies
|
||||
* Replaces duplicated cookie parsing in callback.ts and process.ts
|
||||
*/
|
||||
export function parseAuthState(request: Request): {
|
||||
isValid: boolean;
|
||||
stateData: AuthStateData | null;
|
||||
error?: string
|
||||
} {
|
||||
try {
|
||||
const cookieHeader = request.headers.get('cookie');
|
||||
const cookies = cookieHeader ? parse(cookieHeader) : {};
|
||||
|
||||
if (!cookies.auth_state) {
|
||||
return { isValid: false, stateData: null, error: 'No auth state cookie' };
|
||||
}
|
||||
|
||||
const stateData = JSON.parse(decodeURIComponent(cookies.auth_state));
|
||||
|
||||
if (!stateData.state || !stateData.returnTo) {
|
||||
return { isValid: false, stateData: null, error: 'Invalid state data structure' };
|
||||
}
|
||||
|
||||
return { isValid: true, stateData };
|
||||
} catch (error) {
|
||||
return { isValid: false, stateData: null, error: 'Failed to parse auth state' };
|
||||
}
|
||||
}
|
||||
|
||||
export interface AuthContext {
|
||||
authenticated: boolean;
|
||||
session: SessionData | null;
|
||||
userEmail: string;
|
||||
/**
|
||||
* CONSOLIDATED: Verify state parameter against stored state
|
||||
* Replaces duplicated verification logic in callback.ts and process.ts
|
||||
*/
|
||||
export function verifyAuthState(request: Request, receivedState: string): {
|
||||
isValid: boolean;
|
||||
stateData: AuthStateData | null;
|
||||
error?: string;
|
||||
} {
|
||||
const { isValid, stateData, error } = parseAuthState(request);
|
||||
|
||||
if (!isValid || !stateData) {
|
||||
logAuthEvent('State parsing failed', { error });
|
||||
return { isValid: false, stateData: null, error };
|
||||
}
|
||||
|
||||
if (stateData.state !== receivedState) {
|
||||
logAuthEvent('State mismatch', {
|
||||
received: receivedState,
|
||||
stored: stateData.state
|
||||
});
|
||||
return {
|
||||
isValid: false,
|
||||
stateData: null,
|
||||
error: 'State parameter mismatch'
|
||||
};
|
||||
}
|
||||
|
||||
return { isValid: true, stateData };
|
||||
}
|
||||
|
||||
/**
|
||||
* CONSOLIDATED: Create session with cookie headers
|
||||
* Replaces duplicated session creation in callback.ts and process.ts
|
||||
*/
|
||||
export async function createSessionWithCookie(userInfo: UserInfo): Promise<{
|
||||
sessionToken: string;
|
||||
sessionCookie: string;
|
||||
clearStateCookie: string;
|
||||
userId: string;
|
||||
userEmail: string;
|
||||
}> {
|
||||
const userId = userInfo.sub || userInfo.preferred_username || 'unknown';
|
||||
const userEmail = getUserEmail(userInfo);
|
||||
|
||||
const sessionToken = await createSession(userId, userEmail);
|
||||
const sessionCookie = createSessionCookie(sessionToken);
|
||||
const clearStateCookie = 'auth_state=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0';
|
||||
|
||||
return {
|
||||
sessionToken,
|
||||
sessionCookie,
|
||||
clearStateCookie,
|
||||
userId,
|
||||
userEmail
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -255,37 +347,47 @@ export async function withAuth(Astro: AstroGlobal): Promise<AuthContext | Respon
|
||||
|
||||
/**
|
||||
* CONSOLIDATED: Replace repeated auth patterns in API endpoints
|
||||
* Usage: const authResult = await withAPIAuth(request);
|
||||
* if (!authResult.authenticated) return createAuthErrorResponse();
|
||||
* Enhanced version with better return structure
|
||||
*/
|
||||
export async function withAPIAuth(request: Request): Promise<{
|
||||
authenticated: boolean;
|
||||
userId: string;
|
||||
session?: SessionData
|
||||
session?: SessionData;
|
||||
authRequired: boolean;
|
||||
}> {
|
||||
const authRequired = process.env.AUTHENTICATION_NECESSARY !== 'false';
|
||||
|
||||
if (!authRequired) {
|
||||
return {
|
||||
authenticated: true,
|
||||
userId: 'anonymous'
|
||||
userId: 'anonymous',
|
||||
authRequired: false
|
||||
};
|
||||
}
|
||||
|
||||
const sessionToken = getSessionFromRequest(request);
|
||||
if (!sessionToken) {
|
||||
return { authenticated: false, userId: '' };
|
||||
return {
|
||||
authenticated: false,
|
||||
userId: '',
|
||||
authRequired: true
|
||||
};
|
||||
}
|
||||
|
||||
const session = await verifySession(sessionToken);
|
||||
if (!session) {
|
||||
return { authenticated: false, userId: '' };
|
||||
return {
|
||||
authenticated: false,
|
||||
userId: '',
|
||||
authRequired: true
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
authenticated: true,
|
||||
userId: session.userId,
|
||||
session
|
||||
session,
|
||||
authRequired: true
|
||||
};
|
||||
}
|
||||
|
||||
@@ -297,4 +399,22 @@ export function createAuthErrorResponse(message: string = 'Authentication requir
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* CONSOLIDATED: Create consistent API responses
|
||||
*/
|
||||
export function createAPIResponse(data: any, status: number = 200): Response {
|
||||
return new Response(JSON.stringify(data), {
|
||||
status,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
export function createBadRequestResponse(message: string = 'Bad request'): Response {
|
||||
return createAPIResponse({ error: message }, 400);
|
||||
}
|
||||
|
||||
export function createSuccessResponse(data: any = { success: true }): Response {
|
||||
return createAPIResponse(data, 200);
|
||||
}
|
||||
Reference in New Issue
Block a user