// The Cloudflare OAuth flow, authorization code with PKCE. The wizard is a // public client: the token comes back to this page and stays in // sessionStorage. The exchange goes straight to the token endpoint. It is a // form POST, which browsers send without a preflight, and the response // carries CORS for the origins registered on the OAuth client. const AUTH_URL = 'https://dash.cloudflare.com/oauth2/auth'; const TOKEN_URL = 'https://dash.cloudflare.com/oauth2/token'; function base64url(/** @type {ArrayBuffer|Uint8Array} */ bytes) { const arr = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes); return btoa(String.fromCharCode(...arr)) .replaceAll('+', '-') .replaceAll('/', '_') .replace(/=+$/, ''); } function randomToken() { return base64url(crypto.getRandomValues(new Uint8Array(32))); } export function redirectUri() { return `${location.origin}/callback`; } /** @param {{clientId: string, scopes: string}} config */ export async function beginOAuth(config) { const verifier = randomToken(); const state = randomToken(); const challenge = base64url( await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier)), ); sessionStorage.setItem('oauth_verifier', verifier); sessionStorage.setItem('oauth_state', state); const params = new URLSearchParams({ response_type: 'code', client_id: config.clientId, redirect_uri: redirectUri(), scope: config.scopes, state, code_challenge: challenge, code_challenge_method: 'S256', }); location.href = `${AUTH_URL}?${params}`; } /** * Exchange the code in the callback URL for a token. * @param {{clientId: string}} config * @returns {Promise} the access token */ export async function completeOAuth(config) { const params = new URLSearchParams(location.search); const code = params.get('code'); const state = params.get('state'); if (!code) { throw new Error( params.get('error_description') ?? params.get('error') ?? 'No authorization code in the callback', ); } if (state !== sessionStorage.getItem('oauth_state')) { throw new Error('OAuth state mismatch. Start over.'); } const verifier = sessionStorage.getItem('oauth_verifier'); if (!verifier) throw new Error('Missing PKCE verifier. Start over.'); const response = await fetch(TOKEN_URL, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri: redirectUri(), client_id: config.clientId, code_verifier: verifier, }), }); const body = await response.json().catch(() => ({})); if (!response.ok || !body.access_token) { throw new Error( body.error_description ?? body.error ?? `Token exchange failed (${response.status})`, ); } sessionStorage.removeItem('oauth_verifier'); sessionStorage.removeItem('oauth_state'); sessionStorage.setItem('cf_token', body.access_token); return body.access_token; } export function storedToken() { return sessionStorage.getItem('cf_token'); } export function clearToken() { sessionStorage.removeItem('cf_token'); }