File size: 5,105 Bytes
1fff71f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
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 = '/';
	}
}