Christian Kniep
new webapp
1fff71f
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<ArrayBuffer> {
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<void | UserProfile> {
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<UserProfile | null> {
// 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<UserProfile> {
// 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 = '/';
}
}