Alexander Garcia
Part 2: Architecture & Design. How TypeScript's type system enabled a security-first architecture that's both extensible and maintainable.
Alexander Garcia is an effective JavaScript Engineer who crafts stunning web experiences.
Alexander Garcia is a meticulous Web Architect who creates scalable, maintainable web solutions.
Alexander Garcia is a passionate Software Consultant who develops extendable, fault-tolerant code.
Alexander Garcia is a detail-oriented Web Developer who builds user-friendly websites.
Alexander Garcia is a passionate Lead Software Engineer who builds user-friendly experiences.
Alexander Garcia is a trailblazing UI Engineer who develops pixel-perfect code and design.
How TypeScript's type system enabled a security-first architecture that's both extensible and maintainable
In Part 1, I explained why OAuth Guardian needed to exist: OAuth misconfigurations are everywhere, and I learned this firsthand building authentication for 200M+ users at VA.gov. Now let's dive into how I architected the tool to be both powerful and developer-friendly.
Before writing any code, I established core principles based on my VA.gov experience:
Every architectural decision prioritizes security:
OAuth Guardian ships with built-in checks, but teams have unique requirements. The architecture enables:
BaseCheckI learned at VA.gov that security tools nobody uses don't improve security. OAuth Guardian prioritizes DX:
Security checks must run automatically:
OAuth Guardian uses a layered architecture that separates concerns:
┌─────────────────────────────────────────┐
│ CLI Interface │ (commander, chalk, ora)
│ src/cli.ts │
└───────────────┬─────────────────────────┘
│
┌───────────────▼─────────────────────────┐
│ Configuration System │ (js-yaml, zod)
│ src/config/loader.ts │
│ src/config/schema.ts │
│ src/config/defaults.ts │
└───────────────┬─────────────────────────┘
│
┌───────────────▼─────────────────────────┐
│ Audit Engine │
│ src/auditor/engine.ts │ ← Orchestrates everything
│ src/auditor/http-client.ts │
└───────┬───────────────────────┬─────────┘
│ │
┌───────▼───────┐ ┌────────▼──────────┐
│ Check System │ │ Report System │
│ src/checks/ │ │ src/reporters/ │
│ - oauth/ │ │ - json │
│ - nist/ │ │ - html │
│ - owasp/ │ │ - terminal │
│ - custom/ │ │ - markdown │
└───────────────┘ └───────────────────┘
Let's explore each layer.
Everything starts with TypeScript types in src/types/. This was crucial at VA.gov where type safety prevented security bugs.
Every security check returns a structured result:
export interface CheckResult { id: string; // Unique check identifier name: string; // Human-readable name category: CheckCategory; // OAUTH | NIST | OWASP | CUSTOM status: CheckStatus; // PASS | FAIL | WARNING | SKIPPED | ERROR severity?: Severity; // CRITICAL | HIGH | MEDIUM | LOW | INFO description: string; // What this check validates message?: string; // Details about the finding remediation?: string; // How to fix it references?: string[]; // RFC specs, OWASP guides metadata?: Record<string, unknown>; // Additional context timestamp: Date; // When check ran executionTime?: number; // Performance tracking }
Why this structure?
Checks need access to shared resources:
export interface CheckContext { targetUrl: string; // OAuth server being audited config?: Record<string, unknown>; // User configuration httpClient?: HttpClient; // Shared HTTP client logger?: { // Optional logging debug: (message: string, ...args: unknown[]) => void; info: (message: string, ...args: unknown[]) => void; warn: (message: string, ...args: unknown[]) => void; error: (message: string, ...args: unknown[]) => void; }; }
This dependency injection pattern enables:
OAuth Guardian is highly configurable, and Zod validates everything:
export interface AuditorConfig { target: string; // OAuth server URL oauth?: OAuthCheckConfig; // OAuth-specific settings nist?: NISTCheckConfig; // NIST compliance settings owasp?: OWASPCheckConfig; // OWASP settings checks?: CheckFilterConfig; // Which checks to run reporting?: ReportingConfig; // Output format timeout?: number; // HTTP timeout verbose?: boolean; // Detailed logging userAgent?: string; // Custom User-Agent headers?: Record<string, string>; // Additional headers pluginsDir?: string; // Custom check plugins }
The check system is OAuth Guardian's heart. I designed it based on my VA.gov experience writing dozens of security checks.
All checks extend this base:
export abstract class BaseCheck { // Check metadata (subclass defines these) abstract readonly id: string; abstract readonly name: string; abstract readonly category: CheckCategory; abstract readonly defaultSeverity: Severity; abstract readonly description: string; protected references: string[] = []; // Main execution method (subclass implements this) abstract execute(context: CheckContext): Promise<CheckResult>; // Public run method with error handling & timing async run(context: CheckContext): Promise<CheckResult> { const startTime = Date.now(); try { const result = await this.execute(context); return { ...result, executionTime: Date.now() - startTime, }; } catch (error) { return this.error( "Check execution failed", error instanceof Error ? error : undefined ); } } // Helper methods for creating results protected pass( message?: string, metadata?: Record<string, unknown> ): CheckResult { return { id: this.id, name: this.name, category: this.category, status: CheckStatus.PASS, description: this.description, message, references: this.references, metadata, timestamp: new Date(), }; } protected fail( message: string, severity?: Severity, remediation?: string, metadata?: Record<string, unknown> ): CheckResult { return { id: this.id, name: this.name, category: this.category, status: CheckStatus.FAIL, severity: severity ?? this.defaultSeverity, description: this.description, message, remediation, references: this.references, metadata, timestamp: new Date(), }; } // Similar helpers for warning(), skip(), error()... }
Template Method Pattern: run() provides structure, execute() contains logic. This ensures:
Helper Methods: pass(), fail(), warning() make checks readable:
// Compare this: return { id: this.id, name: this.name, category: this.category, status: CheckStatus.FAIL, severity: Severity.CRITICAL, description: this.description, message: "PKCE not supported", remediation: "...", references: this.references, metadata: {}, timestamp: new Date(), }; // To this: return this.fail( "PKCE not supported", Severity.CRITICAL, "Implement PKCE (RFC 7636)..." );
Extensibility: Creating a new check is straightforward:
export class PKCECheck extends BaseCheck { readonly id = "oauth-pkce"; readonly name = "PKCE Implementation Check"; readonly category = CheckCategory.OAUTH; readonly defaultSeverity = Severity.HIGH; readonly description = "Validates PKCE (RFC 7636) implementation"; protected references = ["https://datatracker.ietf.org/doc/html/rfc7636"]; async execute(context: CheckContext): Promise<CheckResult> { const httpClient = context.httpClient as HttpClient; // 1. Discover OAuth metadata const metadata = await httpClient.discoverOAuthMetadata(context.targetUrl); if (!metadata.success) { return this.warning( "Unable to discover OAuth metadata", "Implement OAuth 2.0 Authorization Server Metadata (RFC 8414)" ); } // 2. Check PKCE support const methods = metadata.data?.code_challenge_methods_supported; if (!methods || !methods.includes("S256")) { return this.fail( "PKCE not supported or S256 method missing", Severity.CRITICAL, "Enable PKCE with S256 code challenge method...\n\nExample:\n..." ); } return this.pass("PKCE properly supported with S256 method"); } }
The AuditEngine orchestrates check execution:
export class AuditEngine { private config: AuditorConfig; private httpClient: HttpClient; private checks: BaseCheck[] = []; private logger?: Logger; constructor(config: AuditorConfig) { this.config = config; this.httpClient = new HttpClient({ timeout: config.timeout, userAgent: config.userAgent, headers: config.headers, verbose: config.verbose, }); if (config.verbose) { this.setupLogger(); } } registerCheck(check: BaseCheck): void { this.checks.push(check); } registerChecks(checks: BaseCheck[]): void { this.checks.push(...checks); } async run(): Promise<Report> { const startTime = new Date(); // 1. Filter checks based on config const checksToRun = this.filterChecks(); // 2. Create shared context const context: CheckContext = { targetUrl: this.config.target, config: this.config as unknown as Record<string, unknown>, httpClient: this.httpClient, logger: this.logger, }; // 3. Run all checks const results: CheckResult[] = []; for (const check of checksToRun) { this.logger?.info(`Running check: ${check.name}`); const result = await check.run(context); results.push(result); } const endTime = new Date(); // 4. Generate comprehensive report return this.generateReport(results, startTime, endTime); } private generateReport( results: CheckResult[], startTime: Date, endTime: Date ): Report { return { metadata: { targetUrl: this.config.target, startTime, endTime, executionTime: endTime.getTime() - startTime.getTime(), version: "0.1.0", }, summary: this.generateSummary(results), results, findings: this.generateFindings(results), compliance: this.generateComplianceScorecard(results), }; } // Risk scoring, compliance calculation, etc... }
Sequential Execution: Checks run sequentially (not parallel) to:
Shared HTTP Client: One client instance for all checks:
Rich Report Generation: The engine calculates:
OAuth Guardian uses YAML configuration validated by Zod schemas.
YAML: Human-friendly, comment-supported, widely adopted Zod: Runtime type validation, great error messages, TypeScript integration
import { z } from "zod"; const OAuthCheckConfigSchema = z .object({ pkce: z.union([z.boolean(), z.enum(["error", "warning"])]).optional(), state: z.union([z.boolean(), z.enum(["error", "warning"])]).optional(), // ... }) .optional(); export const AuditorConfigSchema = z.object({ target: z.string().url(), oauth: OAuthCheckConfigSchema, nist: NISTCheckConfigSchema, owasp: OWASPCheckConfigSchema, // ... }); export type ValidatedConfig = z.infer<typeof AuditorConfigSchema>;
Benefits:
oauth.pkce: Expected boolean, received stringOAuth Guardian searches for config files automatically:
const CONFIG_FILE_NAMES = [ "oauth-guardian.config.yml", "oauth-guardian.config.yaml", ".oauth-guardian.yml", ".oauth-guardian.yaml", ]; export async function discoverAndLoadConfig(): Promise<AuditorConfig | null> { for (const fileName of CONFIG_FILE_NAMES) { try { const filePath = resolve(process.cwd(), fileName); return await loadConfigFromFile(filePath); } catch { continue; } } return null; }
This enables zero-config operation while supporting advanced customization.
OAuth Guardian supports multiple output formats, all generated from the same Report type.
Beautiful CLI output with colored formatting:
export class TerminalReporter { generate(report: Report): string { const lines: string[] = []; // Summary with colors lines.push(chalk.bold("📊 Audit Results")); lines.push(` ${chalk.green("✓")} Passed: ${report.summary.passed}`); lines.push(` ${chalk.red("✗")} Failed: ${report.summary.failed}`); // Check results with icons for (const result of report.results) { const icon = this.getStatusIcon(result.status); lines.push(`${icon} ${chalk.bold(result.name)}`); lines.push(` ${chalk.gray(result.description)}`); } return lines.join("\n"); } }
Professional reports with Handlebars templates:
export class HTMLReporter { async generate(report: Report): Promise<string> { const template = Handlebars.compile(htmlTemplate); return template({ metadata: report.metadata, summary: report.summary, results: report.results, compliance: report.compliance, }); } }
Machine-readable for CI/CD:
export class JSONReporter { generate(report: Report): string { return JSON.stringify(report, null, 2); } }
In Part 3, I'll walk through the implementation journey:
This is Part 2 of a 3-part series. ← Read Part 1: The Problem | Read Part 3: Implementation →