Something went wrong. Try again.
AtAuth forked from bkb.arcnode.xyz/atauth
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216/** * Input validation utilities */
import type { OAuthState } from './types';
/** * Check if the current environment is production. * Uses common environment indicators. */function isProduction(): boolean { if (typeof process !== 'undefined' && process.env) { return process.env.NODE_ENV === 'production'; } // Browser fallback: check if running on non-localhost if (typeof window !== 'undefined') { const hostname = window.location?.hostname; return hostname !== 'localhost' && hostname !== '127.0.0.1' && hostname !== '::1'; } return false;}
/** * Require HTTPS for gateway URLs in production. * * In production environments, all gateway URLs must use HTTPS to protect * tokens in transit. This prevents man-in-the-middle attacks. * * @param url - The URL to validate * @param context - Description for error message (e.g., "gatewayUrl") * @throws Error if HTTPS is required but not used */export function requireHttpsInProduction(url: string, context = 'URL'): void { if (!isProduction()) { return; // Allow HTTP in development }
try { const parsed = new URL(url); if (parsed.protocol !== 'https:') { throw new Error( `Security error: ${context} must use HTTPS in production. ` + `Got "${parsed.protocol}" in "${url}"` ); } } catch (e) { if (e instanceof Error && e.message.startsWith('Security error:')) { throw e; } throw new Error(`Invalid ${context}: ${url}`); }}
/** * Validate gateway URL for security requirements. * * @param gatewayUrl - The gateway URL to validate * @throws Error if URL is invalid or insecure */export function validateGatewayUrl(gatewayUrl: string): void { requireHttpsInProduction(gatewayUrl, 'gatewayUrl');}
/** * Validate callback URL for security requirements. * * @param callbackUrl - The callback URL to validate * @throws Error if URL is invalid or insecure */export function validateCallbackUrl(callbackUrl: string): void { requireHttpsInProduction(callbackUrl, 'callbackUrl');}
/** * Maximum allowed size for OAuth state string to prevent DoS. */const MAX_STATE_SIZE = 4096;
/** * Maximum nesting depth for state object. */const MAX_NESTING_DEPTH = 3;
/** * Validate a DID (Decentralized Identifier) format. * * @param did - The DID string to validate * @returns True if valid DID format */export function isValidDid(did: unknown): did is string { if (typeof did !== 'string') return false;
// AT Protocol DIDs start with "did:plc:" or "did:web:" return /^did:(plc|web):[a-zA-Z0-9._%-]+$/.test(did);}
/** * Validate an AT Protocol handle format. * * @param handle - The handle to validate * @returns True if valid handle format */export function isValidHandle(handle: unknown): handle is string { if (typeof handle !== 'string') return false;
// Handles are domain-like strings (e.g., "user.bsky.social") // Must be lowercase alphanumeric with dots, 3-253 chars if (handle.length < 3 || handle.length > 253) return false;
return /^[a-z0-9][a-z0-9.-]*[a-z0-9]$/.test(handle) && !handle.includes('..');}
/** * Check object nesting depth. */function getDepth(obj: unknown, current = 0): number { if (current > MAX_NESTING_DEPTH) return current; if (typeof obj !== 'object' || obj === null) return current;
let maxDepth = current; for (const value of Object.values(obj)) { const depth = getDepth(value, current + 1); if (depth > maxDepth) maxDepth = depth; } return maxDepth;}
/** * Validate and parse OAuth state from a string. * Guards against malformed JSON, oversized payloads, and deeply nested objects. * * @param stateString - The state string from URL parameter * @returns Validated OAuthState or null if invalid */export function parseOAuthState(stateString: unknown): OAuthState | null { // Must be a string if (typeof stateString !== 'string') { return null; }
// Check size limit to prevent DoS if (stateString.length > MAX_STATE_SIZE) { console.warn('OAuth state exceeds maximum size'); return null; }
// Parse JSON let parsed: unknown; try { parsed = JSON.parse(stateString); } catch { console.warn('OAuth state is not valid JSON'); return null; }
// Must be an object if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) { console.warn('OAuth state must be an object'); return null; }
// Check nesting depth if (getDepth(parsed) > MAX_NESTING_DEPTH) { console.warn('OAuth state exceeds maximum nesting depth'); return null; }
// Validate known fields const state = parsed as Record<string, unknown>;
// returnTo must be a string if present if ('returnTo' in state && typeof state.returnTo !== 'string') { console.warn('OAuth state returnTo must be a string'); return null; }
// nonce must be a string if present if ('nonce' in state && typeof state.nonce !== 'string') { console.warn('OAuth state nonce must be a string'); return null; }
// Validate returnTo is a safe URL (no javascript: etc.) if (typeof state.returnTo === 'string') { try { const url = new URL(state.returnTo, window?.location?.origin || 'https://example.com'); if (url.protocol !== 'http:' && url.protocol !== 'https:') { console.warn('OAuth state returnTo has invalid protocol'); return null; } } catch { // Relative URLs are OK, but must not contain dangerous schemes if (/^(javascript|data|vbscript):/i.test(state.returnTo)) { console.warn('OAuth state returnTo has dangerous scheme'); return null; } } }
return state as OAuthState;}
/** * Validate an app ID. * * @param appId - The app ID to validate * @returns True if valid */export function isValidAppId(appId: unknown): appId is string { if (typeof appId !== 'string') return false;
// App IDs should be alphanumeric with hyphens/underscores, 1-64 chars return /^[a-zA-Z0-9_-]{1,64}$/.test(appId);}