# Quickslice OAuth PKCE Flow Guide (Server-Side) This guide covers implementing OAuth 2.0 with PKCE for server-side applications like Astro, Express, or other Node.js backends. ## Overview PKCE (Proof Key for Code Exchange) protects the authorization code exchange from interception attacks. Quickslice requires the `S256` method (SHA-256 hash). --- ## Astro Backend Integration ### Project Structure ``` src/ ├── pages/ │ └── api/ │ └── auth/ │ ├── login.ts # Initiates OAuth flow │ ├── callback.ts # Handles redirect │ └── refresh.ts # Token refresh └── lib/ └── pkce.ts # PKCE utilities ``` ### PKCE Utilities (`src/lib/pkce.ts`) ```typescript import { createHash, randomBytes } from 'crypto'; export function generateCodeVerifier(): string { return randomBytes(32) .toString('base64') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, ''); } export function generateCodeChallenge(verifier: string): string { return createHash('sha256') .update(verifier) .digest('base64') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, ''); } export function generateState(): string { return randomBytes(16).toString('base64url'); } ``` ### Login Endpoint (`src/pages/api/auth/login.ts`) ```typescript import type { APIRoute } from 'astro'; import { generateCodeVerifier, generateCodeChallenge, generateState } from '../../../lib/pkce'; const QUICKSLICE_SERVER = import.meta.env.QUICKSLICE_SERVER; const CLIENT_ID = import.meta.env.QUICKSLICE_CLIENT_ID; const REDIRECT_URI = import.meta.env.QUICKSLICE_REDIRECT_URI; export const GET: APIRoute = async ({ cookies, redirect }) => { const codeVerifier = generateCodeVerifier(); const codeChallenge = generateCodeChallenge(codeVerifier); const state = generateState(); // Store in secure httpOnly cookies cookies.set('pkce_verifier', codeVerifier, { httpOnly: true, secure: true, sameSite: 'lax', path: '/', maxAge: 600, // 10 minutes }); cookies.set('oauth_state', state, { httpOnly: true, secure: true, sameSite: 'lax', path: '/', maxAge: 600, }); const authUrl = new URL(`${QUICKSLICE_SERVER}/oauth/authorize`); authUrl.searchParams.set('client_id', CLIENT_ID); authUrl.searchParams.set('redirect_uri', REDIRECT_URI); authUrl.searchParams.set('response_type', 'code'); authUrl.searchParams.set('code_challenge', codeChallenge); authUrl.searchParams.set('code_challenge_method', 'S256'); authUrl.searchParams.set('state', state); authUrl.searchParams.set('scope', 'atproto'); return redirect(authUrl.toString(), 302); }; ``` ### Callback Endpoint (`src/pages/api/auth/callback.ts`) ```typescript import type { APIRoute } from 'astro'; const QUICKSLICE_SERVER = import.meta.env.QUICKSLICE_SERVER; const CLIENT_ID = import.meta.env.QUICKSLICE_CLIENT_ID; const REDIRECT_URI = import.meta.env.QUICKSLICE_REDIRECT_URI; export const GET: APIRoute = async ({ url, cookies, redirect }) => { const code = url.searchParams.get('code'); const state = url.searchParams.get('state'); const error = url.searchParams.get('error'); if (error) { const errorDescription = url.searchParams.get('error_description') || 'Unknown error'; return new Response(`Authentication failed: ${errorDescription}`, { status: 400 }); } if (!code || !state) { return new Response('Missing code or state', { status: 400 }); } // Validate state to prevent CSRF const savedState = cookies.get('oauth_state')?.value; if (state !== savedState) { return new Response('State mismatch', { status: 400 }); } // Get stored verifier const codeVerifier = cookies.get('pkce_verifier')?.value; if (!codeVerifier) { return new Response('Missing PKCE verifier', { status: 400 }); } // Exchange code for tokens const tokenResponse = await fetch(`${QUICKSLICE_SERVER}/oauth/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri: REDIRECT_URI, client_id: CLIENT_ID, code_verifier: codeVerifier, }), }); if (!tokenResponse.ok) { const error = await tokenResponse.text(); return new Response(`Token exchange failed: ${error}`, { status: 400 }); } const tokens = await tokenResponse.json(); // Clear PKCE cookies cookies.delete('pkce_verifier', { path: '/' }); cookies.delete('oauth_state', { path: '/' }); // Store tokens in secure httpOnly cookies cookies.set('access_token', tokens.access_token, { httpOnly: true, secure: true, sameSite: 'strict', path: '/', maxAge: tokens.expires_in, }); cookies.set('refresh_token', tokens.refresh_token, { httpOnly: true, secure: true, sameSite: 'strict', path: '/', maxAge: 60 * 60 * 24 * 30, // 30 days }); cookies.set('user_did', tokens.sub, { httpOnly: true, secure: true, sameSite: 'strict', path: '/', maxAge: 60 * 60 * 24 * 30, }); return redirect('/', 302); }; ``` ### Refresh Endpoint (`src/pages/api/auth/refresh.ts`) ```typescript import type { APIRoute } from 'astro'; const QUICKSLICE_SERVER = import.meta.env.QUICKSLICE_SERVER; const CLIENT_ID = import.meta.env.QUICKSLICE_CLIENT_ID; export const POST: APIRoute = async ({ cookies }) => { const refreshToken = cookies.get('refresh_token')?.value; if (!refreshToken) { return new Response(JSON.stringify({ error: 'No refresh token' }), { status: 401, headers: { 'Content-Type': 'application/json' }, }); } const tokenResponse = await fetch(`${QUICKSLICE_SERVER}/oauth/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken, client_id: CLIENT_ID, }), }); if (!tokenResponse.ok) { cookies.delete('access_token', { path: '/' }); cookies.delete('refresh_token', { path: '/' }); return new Response(JSON.stringify({ error: 'Refresh failed' }), { status: 401, headers: { 'Content-Type': 'application/json' }, }); } const tokens = await tokenResponse.json(); cookies.set('access_token', tokens.access_token, { httpOnly: true, secure: true, sameSite: 'strict', path: '/', maxAge: tokens.expires_in, }); cookies.set('refresh_token', tokens.refresh_token, { httpOnly: true, secure: true, sameSite: 'strict', path: '/', maxAge: 60 * 60 * 24 * 30, }); return new Response(JSON.stringify({ success: true }), { status: 200, headers: { 'Content-Type': 'application/json' }, }); }; ``` ### Environment Variables ```bash # .env QUICKSLICE_SERVER=https://your-quickslice-server.com QUICKSLICE_CLIENT_ID=your-client-id QUICKSLICE_REDIRECT_URI=https://your-app.com/api/auth/callback ``` --- ## Security Notes - **HTTPS required** in production (HTTP only allowed for localhost) - **httpOnly cookies** prevent XSS token theft - **State parameter** prevents CSRF attacks - **Short PKCE cookie lifetime** (10 min) limits exposure window - **Secure + SameSite cookies** provide additional protection ## Related Resources - [RFC 7636 - PKCE](https://datatracker.ietf.org/doc/html/rfc7636) - [OAuth 2.0 Authorization Code Flow](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1)