/** Authentication: OIDC login, token management. */ export let authToken = localStorage.getItem('cog_token'); export let authConfig = null; let _authFailed = false; export function isAuthFailed() { return _authFailed; } export function setAuthFailed(v) { _authFailed = v; } export async function initAuth(onReady) { try { const r = await fetch('/auth/config'); authConfig = await r.json(); } catch (e) { authConfig = { enabled: false }; } if (!authConfig.enabled) { onReady(); return; } // Check for OIDC callback const params = new URLSearchParams(location.search); if (params.has('code')) { // Exchange code for token (PKCE) const code = params.get('code'); const verifier = sessionStorage.getItem('cog_pkce_verifier'); try { const tokenResp = await fetch(authConfig.issuer + '/oauth/v2/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri: location.origin + '/callback', client_id: authConfig.clientId, code_verifier: verifier || '', }), }); const tokenData = await tokenResp.json(); if (tokenData.id_token) { authToken = tokenData.id_token; localStorage.setItem('cog_token', authToken); if (tokenData.access_token) { localStorage.setItem('cog_access_token', tokenData.access_token); } } } catch (e) { console.error('token exchange failed', e); } history.replaceState(null, '', '/'); } if (authToken) { onReady(); } else { showLogin(); } } export function showLogin() { const el = document.getElementById('login-overlay'); if (el) el.style.display = 'flex'; } export async function startLogin() { if (!authConfig) return; const verifier = randomString(64); sessionStorage.setItem('cog_pkce_verifier', verifier); const encoder = new TextEncoder(); const data = encoder.encode(verifier); const hash = await crypto.subtle.digest('SHA-256', data); const challenge = btoa(String.fromCharCode(...new Uint8Array(hash))) .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); const params = new URLSearchParams({ response_type: 'code', client_id: authConfig.clientId, redirect_uri: location.origin + '/callback', scope: 'openid profile email', code_challenge: challenge, code_challenge_method: 'S256', prompt: 'login', }); location.href = authConfig.issuer + '/oauth/v2/authorize?' + params; } function randomString(len) { const arr = new Uint8Array(len); crypto.getRandomValues(arr); return Array.from(arr, b => b.toString(36).slice(-1)).join('').slice(0, len); }