~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
101 lines · 3.1 KB Raw History
// 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<string>} 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');
}