import { authStore } from '$lib/stores/auth'; import { apiClient } from '$lib/services/api'; import { OAUTH_CLIENT_ID, OAUTH_REDIRECT_URI, OAUTH_PROVIDER_URL, ENABLE_MOCK_AUTH, MOCK_USER_ID, MOCK_USERNAME, MOCK_EMAIL, API_TOKEN } from '$lib/utils/constants'; import type { UserProfile } from '$lib/types/api'; /** * Authentication service for OAuth2 PKCE and mock auth */ // PKCE helper functions function generateRandomString(length: number): string { const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'; const values = crypto.getRandomValues(new Uint8Array(length)); return Array.from(values) .map((x) => possible[x % possible.length]) .join(''); } async function sha256(plain: string): Promise { const encoder = new TextEncoder(); const data = encoder.encode(plain); return crypto.subtle.digest('SHA-256', data); } function base64urlencode(buffer: ArrayBuffer): string { const bytes = new Uint8Array(buffer); let str = ''; for (const byte of bytes) { str += String.fromCharCode(byte); } return btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); } /** * Generate PKCE code verifier and challenge */ export async function generatePKCE(): Promise<{ verifier: string; challenge: string }> { const verifier = generateRandomString(128); const hashed = await sha256(verifier); const challenge = base64urlencode(hashed); return { verifier, challenge }; } /** * Initiate OAuth2 login flow */ export async function initiateOAuthLogin(): Promise { if (ENABLE_MOCK_AUTH) { // Use mock auth in development return mockLogin(); } // Generate PKCE parameters const { verifier, challenge } = await generatePKCE(); // Store verifier in sessionStorage for callback sessionStorage.setItem('pkce_verifier', verifier); // Build authorization URL const params = new URLSearchParams({ client_id: OAUTH_CLIENT_ID, redirect_uri: OAUTH_REDIRECT_URI, response_type: 'code', scope: 'openid profile email', code_challenge: challenge, code_challenge_method: 'S256', state: generateRandomString(32) // CSRF protection }); // Store state for validation sessionStorage.setItem('oauth_state', params.get('state')!); // Redirect to OAuth provider window.location.href = `${OAUTH_PROVIDER_URL}?${params.toString()}`; } /** * Handle OAuth callback after authorization */ export async function handleOAuthCallback( code: string, state: string ): Promise { // Validate state const storedState = sessionStorage.getItem('oauth_state'); if (!storedState || storedState !== state) { throw new Error('Invalid OAuth state - possible CSRF attack'); } // Get verifier const verifier = sessionStorage.getItem('pkce_verifier'); if (!verifier) { throw new Error('PKCE verifier not found'); } // Clean up stored values sessionStorage.removeItem('oauth_state'); sessionStorage.removeItem('pkce_verifier'); // Exchange authorization code for token const tokenResponse = await fetch(`${OAUTH_PROVIDER_URL}/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ client_id: OAUTH_CLIENT_ID, code, code_verifier: verifier, grant_type: 'authorization_code', redirect_uri: OAUTH_REDIRECT_URI }) }); if (!tokenResponse.ok) { throw new Error('Failed to exchange authorization code for token'); } const tokenData = await tokenResponse.json(); const accessToken = tokenData.access_token; const expiresIn = tokenData.expires_in || 3600; // Default 1 hour // Set auth headers for API client apiClient.setAuth(accessToken, ''); // user_id will be set after profile fetch // Fetch user profile from backend const userProfile = await apiClient.getUserProfile(); // Calculate token expiry const tokenExpiry = Date.now() + expiresIn * 1000; // Update API client with user_id apiClient.setAuth(accessToken, userProfile.user_id); // Update auth store authStore.login( { user_id: userProfile.user_id, username: userProfile.username, email: userProfile.email, avatar_url: userProfile.avatar_url, tokenExpiry }, accessToken, tokenExpiry ); return userProfile; } /** * Mock authentication for development */ export async function mockLogin(): Promise { // Use configured API token for mock auth const mockToken = API_TOKEN; const tokenExpiry = Date.now() + 24 * 60 * 60 * 1000; // 24 hours // Set auth headers apiClient.setAuth(mockToken, MOCK_USER_ID); // Create mock user profile const mockProfile: UserProfile = { user_id: MOCK_USER_ID, username: MOCK_USERNAME, email: MOCK_EMAIL, avatar_url: undefined }; // Update auth store authStore.login( { ...mockProfile, tokenExpiry }, mockToken, tokenExpiry ); return mockProfile; } /** * Logout user */ export function logout(): void { // Clear API client auth apiClient.clearAuth(); // Clear auth store authStore.logout(); // Redirect to home if (typeof window !== 'undefined') { window.location.href = '/'; } }