Files
ClickerZ/Frontend/src/lib/oauth.js
Tetardtek b6d68374d3
Some checks failed
CI/CD — Build & Deploy / Build & Deploy (push) Failing after 21s
fix: use localStorage for PKCE verifier — survives cross-site redirects
2026-03-24 13:18:56 +01:00

84 lines
2.4 KiB
JavaScript

// OAuth 2.0 PKCE client — SuperOAuth consumer for Clickerz
const OAUTH_URL = import.meta.env.VITE_OAUTH_URL || '';
const OAUTH_CLIENT_ID = import.meta.env.VITE_OAUTH_CLIENT_ID || '';
const SESSION_KEY_VERIFIER = 'clkz_pkce_verifier';
function base64UrlEncode(buffer) {
return btoa(String.fromCharCode(...new Uint8Array(buffer)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
export function generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64UrlEncode(array.buffer);
}
export async function generateCodeChallenge(verifier) {
const data = new TextEncoder().encode(verifier);
const digest = await crypto.subtle.digest('SHA-256', data);
return base64UrlEncode(digest);
}
export async function buildAuthUrl(redirectUri, provider, scope = 'openid profile email', clientId = OAUTH_CLIENT_ID) {
const verifier = generateCodeVerifier();
const challenge = await generateCodeChallenge(verifier);
const state = base64UrlEncode(crypto.getRandomValues(new Uint8Array(16)).buffer);
const params = new URLSearchParams({
response_type: 'code',
client_id: clientId,
redirect_uri: redirectUri,
scope,
state,
provider,
code_challenge: challenge,
code_challenge_method: 'S256',
});
return {
url: `${OAUTH_URL}/oauth/authorize?${params.toString()}`,
verifier,
};
}
export async function exchangeCode(code, verifier, redirectUri, clientId = OAUTH_CLIENT_ID) {
const response = await fetch(`${OAUTH_URL}/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: clientId,
code,
code_verifier: verifier,
redirect_uri: redirectUri,
}).toString(),
});
if (!response.ok) {
const text = await response.text().catch(() => '');
throw new Error(`OAuth token exchange failed (${response.status}): ${text}`);
}
const data = await response.json();
if (!data.access_token) throw new Error('No access_token in OAuth response');
return data;
}
export function saveVerifier(verifier) {
localStorage.setItem(SESSION_KEY_VERIFIER, verifier);
}
export function loadVerifier() {
return localStorage.getItem(SESSION_KEY_VERIFIER);
}
export function clearVerifier() {
localStorage.removeItem(SESSION_KEY_VERIFIER);
}