Spaces:
Sleeping
Sleeping
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 = '/';
}
}
|