chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// 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');
}