Alexander Garcia
Part 3 - Implementation Journey. From first commit to Phase 1 MVP: 3 weeks, 4 security checks, 118 tests, and lessons learned building an open-source security tool.
Read time is about 17 minutes
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.
From first commit to Phase 1 MVP: 3 weeks, 4 security checks, 118 tests, and lessons learned building an open-source security tool
In Part 1, I explained why OAuth security auditing is critical based on my VA.gov experience. In Part 2, I detailed the TypeScript-powered architecture that makes OAuth Guardian extensible and maintainable. Now let's walk through the actual implementation journey building Phase 1 MVP in 3 weeks.
I started with the basics:
mkdir oauth-guardian && cd oauth-guardian npm init -y git init
TypeScript Configuration: At VA.gov, we used strict TypeScript settings to catch security bugs. I brought that discipline to OAuth Guardian:
{ "compilerOptions": { "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true, "noUncheckedIndexedAccess": true, "noImplicitOverride": true } }
These settings catch entire classes of bugs:
noUnusedLocals: Dead code that might hide bugsnoImplicitReturns: Functions that don't handle all casesnoUncheckedIndexedAccess: Array access without bounds checkingI created the type system first (Type-Driven Development):
src/types/check.ts: The check result type
export enum Severity { CRITICAL = "critical", HIGH = "high", MEDIUM = "medium", LOW = "low", INFO = "info", } export enum CheckStatus { PASS = "pass", FAIL = "fail", WARNING = "warning", SKIPPED = "skipped", ERROR = "error", } export interface CheckResult { id: string; name: string; category: CheckCategory; status: CheckStatus; severity?: Severity; description: string; message?: string; remediation?: string; references?: string[]; metadata?: Record<string, unknown>; timestamp: Date; executionTime?: number; }
This type guided every subsequent decision. When implementing checks, I always knew the exact structure to return.
The HTTP client wraps axios with OAuth-specific features:
export class HttpClient { private client: AxiosInstance; constructor(options: HttpClientOptions) { this.client = axios.create({ timeout: options.timeout ?? 10000, headers: { "User-Agent": options.userAgent ?? "OAuth-Guardian/0.1.0", ...options.headers, }, validateStatus: () => true, // Don't throw on 4xx/5xx }); } async discoverOAuthMetadata(baseUrl: string): Promise<MetadataResult> { // Try RFC 8414 endpoint const rfc8414 = await this.get( `${baseUrl}/.well-known/oauth-authorization-server` ); if (rfc8414.status === 200) { return { success: true, data: this.parseJson(rfc8414.data), source: "RFC 8414", }; } // Fallback to OIDC Discovery const oidc = await this.get(`${baseUrl}/.well-known/openid-configuration`); if (oidc.status === 200) { return { success: true, data: this.parseJson(oidc.data), source: "OIDC Discovery", }; } return { success: false, attempted: [ { url: rfc8414.config.url, status: rfc8414.status }, { url: oidc.config.url, status: oidc.status }, ], }; } }
Lesson from VA.gov: Always have fallback strategies. OAuth servers implement RFC 8414 or OIDC Discovery (or neither). Supporting both maximizes compatibility.
PKCE (Proof Key for Code Exchange) was the perfect first check because I implemented it from scratch at VA.gov. I knew every detail:
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) support"; protected references = ["https://datatracker.ietf.org/doc/html/rfc7636"]; async execute(context: CheckContext): Promise<CheckResult> { const httpClient = context.httpClient as HttpClient; // Step 1: Discover OAuth metadata this.log(context, "Discovering OAuth metadata..."); const metadata = await httpClient.discoverOAuthMetadata(context.targetUrl); if (!metadata.success) { return this.warning( `Unable to discover OAuth metadata. Attempted endpoints:\n${metadata.attempted ?.map((a) => ` - ${a.url} (${a.status})`) .join("\n")}`, "Implement OAuth 2.0 Authorization Server Metadata (RFC 8414)..." ); } // Step 2: Check PKCE support const methods = metadata.data?.code_challenge_methods_supported; if (!methods || methods.length === 0) { return this.fail( "PKCE not supported - no code_challenge_methods_supported", Severity.CRITICAL, this.getRemediationGuidance() ); } if (!methods.includes("S256")) { return this.fail( `PKCE supported but S256 method missing. Found: ${methods.join(", ")}`, Severity.HIGH, "Add S256 code challenge method support..." ); } return this.pass( `PKCE properly supported with S256 method (via ${metadata.source})` ); } private getRemediationGuidance(): string { return ` **Why This Matters:** PKCE protects against authorization code interception attacks. Without it, mobile apps and SPAs are vulnerable to code injection. **How to Fix:** 1. Add PKCE support to your OAuth server 2. Require code_challenge parameter in authorization requests 3. Support S256 code challenge method 4. Validate code_verifier on token exchange **Example (Node.js):** \`\`\`javascript const crypto = require('crypto'); // Client generates code verifier const codeVerifier = crypto.randomBytes(32).toString('base64url'); // Client creates code challenge const challenge = crypto .createHash('sha256') .update(codeVerifier) .digest('base64url'); // Authorization request includes challenge const authUrl = \`\${authEndpoint}? response_type=code& client_id=\${clientId}& redirect_uri=\${redirectUri}& code_challenge=\${challenge}& code_challenge_method=S256\`; // Token request includes verifier const tokenRequest = { grant_type: 'authorization_code', code: authCode, redirect_uri: redirectUri, code_verifier: codeVerifier, }; \`\`\` **References:** - RFC 7636: https://datatracker.ietf.org/doc/html/rfc7636 - OAuth 2.0 Security BCP: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics `; } }
I immediately tested against real OAuth servers:
npm run build # Google OAuth (has PKCE) node dist/cli.js https://accounts.google.com # ✅ PASS: PKCE properly supported with S256 method # GitHub (no metadata endpoint) node dist/cli.js https://github.com # ⚠️ WARNING: Unable to discover OAuth metadata
Key Insight: Real-world testing revealed that many OAuth servers don't implement metadata discovery. The warning status (vs. failure) acknowledges we can't verify PKCE without metadata it's not necessarily missing.
I added three critical checks based on VA.gov incidents:
1. State Parameter Check (CSRF Protection)
export class StateParameterCheck extends BaseCheck { readonly id = "oauth-state-parameter"; readonly name = "State Parameter Validation"; readonly category = CheckCategory.OAUTH; readonly defaultSeverity = Severity.HIGH; async execute(context: CheckContext): Promise<CheckResult> { // Check if authorization endpoint exists const metadata = await this.discoverMetadata(context); if (!metadata.authorization_endpoint) { return this.fail( "No authorization endpoint found - cannot verify state parameter usage", Severity.HIGH, "Configure authorization_endpoint in OAuth metadata..." ); } return this.pass( "Authorization endpoint configured. Ensure state parameter " + "is required in all authorization requests." ); } }
2. Redirect URI Validation Check
export class RedirectURICheck extends BaseCheck { readonly id = "oauth-redirect-uri"; readonly name = "Redirect URI Validation"; readonly category = CheckCategory.OAUTH; readonly defaultSeverity = Severity.CRITICAL; async execute(context: CheckContext): Promise<CheckResult> { // Validate redirect URI registration exists const metadata = await this.discoverMetadata(context); if (!metadata.authorization_endpoint) { return this.skip("No authorization endpoint to validate"); } return this.pass( "Authorization endpoint exists. Ensure redirect URIs use exact matching " + "(no wildcards, no substring matching)." ); } }
3. Token Storage Check
export class TokenStorageCheck extends BaseCheck { readonly id = "oauth-token-storage"; readonly name = "Token Storage Security"; readonly category = CheckCategory.OAUTH; readonly defaultSeverity = Severity.HIGH; async execute(context: CheckContext): Promise<CheckResult> { const metadata = await this.discoverMetadata(context); if (!metadata.token_endpoint) { return this.fail( "No token endpoint configured", Severity.HIGH, "Configure token_endpoint in OAuth metadata" ); } // Verify HTTPS const url = new URL(metadata.token_endpoint); if (url.protocol !== "https:") { return this.fail( `Token endpoint uses insecure protocol: ${url.protocol}`, Severity.CRITICAL, "ALWAYS use HTTPS for token endpoints" ); } return this.pass( `Token endpoint properly secured with HTTPS. ` + `Ensure tokens are stored in HttpOnly cookies (not localStorage).` ); } }
JSON Reporter (for CI/CD):
export class JSONReporter { private options: JSONReporterOptions; constructor(options: JSONReporterOptions = {}) { this.options = { pretty: options.pretty ?? false, }; } generate(report: Report): string { return JSON.stringify(report, null, this.options.pretty ? 2 : 0); } }
Terminal Reporter (beautiful CLI output):
export class TerminalReporter { generate(report: Report): string { const lines: string[] = []; lines.push(""); lines.push(chalk.bold("📊 Audit Results")); lines.push(""); // Summary lines.push(chalk.bold("Summary:")); lines.push(` Total Checks: ${report.summary.totalChecks}`); lines.push(` ${chalk.green("✓")} Passed: ${report.summary.passed}`); lines.push(` ${chalk.red("✗")} Failed: ${report.summary.failed}`); lines.push(` ${chalk.yellow("⚠")} Warnings: ${report.summary.warnings}`); lines.push(""); lines.push( ` Compliance: ${this.colorizeCompliance( report.summary.compliancePercentage )}%` ); lines.push( ` Risk Score: ${this.colorizeRiskScore(report.summary.riskScore)}/100` ); // Check results lines.push(""); lines.push(chalk.bold("Check Results:")); 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)}`); if (result.message) { lines.push(` ${this.getStatusColor(result.status)(result.message)}`); } lines.push(""); } return lines.join("\n"); } }
YAML Configuration with Zod Validation:
// src/config/schema.ts import { z } from "zod"; export const AuditorConfigSchema = z.object({ target: z.string().url(), oauth: z .object({ pkce: z.union([z.boolean(), z.enum(["error", "warning"])]).optional(), state: z.union([z.boolean(), z.enum(["error", "warning"])]).optional(), redirectUri: z .union([z.boolean(), z.enum(["error", "warning"])]) .optional(), tokenStorage: z .union([z.boolean(), z.enum(["error", "warning"])]) .optional(), }) .optional(), // ... more config options }); export function validateConfig(config: unknown): ValidatedConfig { return AuditorConfigSchema.parse(config); }
Configuration Loader with Auto-Discovery:
// src/config/loader.ts const CONFIG_FILE_NAMES = [ "oauth-guardian.config.yml", "oauth-guardian.config.yaml", ".oauth-guardian.yml", ".oauth-guardian.yaml", ]; export async function loadConfig( target: string, configPath?: string ): Promise<AuditorConfig> { // 1. Explicit config path if (configPath) { const result = await safeLoadConfigFromFile(configPath); if (!result.success) { throw new Error(`Config errors:\n${result.errors?.join("\n")}`); } return result.config!; } // 2. Auto-discovery const discovered = await discoverAndLoadConfig(); if (discovered) { discovered.target = target; return discovered; } // 3. Defaults return getDefaultConfig({ target }); }
Example Configuration File:
# oauth-guardian.config.yml target: https://auth.example.com oauth: pkce: true state: true redirectUri: true tokenStorage: true nist: assuranceLevel: AAL2 sessionManagement: true owasp: severityThreshold: high reporting: format: html output: ./audit-report.html failOn: critical includeRemediation: true timeout: 10000 verbose: false
I built a professional HTML report with Handlebars:
Template (templates/html-report.hbs):
<html> <head> <title>OAuth Guardian Security Audit</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f7fa; } .container { max-width: 1200px; margin: 0 auto; background: white; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 40px; } .summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 20px; } .stat-card { padding: 20px; border-radius: 8px; text-align: center; } .stat-card.passed { border-left: 4px solid #28a745; } .stat-card.failed { border-left: 4px solid #dc3545; } /* ... more styles ... */ </style> </head> <body> <div class="container"> <header> <h1>🛡️ OAuth Guardian</h1> <p>Security Audit Report</p> </header> <section class="summary"> <div class="stat-card passed"> <div class="stat-number"></div> <div class="stat-label">Passed</div> </div> <div class="stat-card failed"> <div class="stat-number"></div> <div class="stat-label">Failed</div> </div> <!-- ... more stats ... --> </section> <section class="results"> <div class="check-result "> <h3></h3> <p></p> <div class="message"></div> <div class="remediation"> <h4>💡 Remediation</h4> </div> </div> </section> </div> </body> </html
Reporter Implementation:
export class HTMLReporter { async generate(report: Report): Promise<string> { // Load template const templateContent = await readFile(this.options.templatePath, "utf-8"); const template = Handlebars.compile(templateContent); // Register helpers Handlebars.registerHelper("formatDate", (date: Date) => { return new Date(date).toLocaleString(); }); Handlebars.registerHelper("formatRemediation", (text: string) => { // Convert markdown code blocks to HTML let formatted = text.replace( /```(\w+)?\n([\s\S]*?)```/g, "<pre>$2</pre>" ); formatted = formatted.replace(/\n/g, "<br>"); return new Handlebars.SafeString(formatted); }); // Generate HTML return template({ metadata: report.metadata, summary: report.summary, results: report.results, compliance: report.compliance, }); } }
# Build npm run build # Generate HTML report node dist/cli.js https://accounts.google.com --format html --output report.html # Open report open report.html
Result: A beautiful, professional HTML report with:
I wrote 118 unit tests covering:
describe("PKCECheck", () => { it("should PASS when S256 is supported", async () => { const mockClient = { discoverOAuthMetadata: jest.fn().mockResolvedValue({ success: true, data: { code_challenge_methods_supported: ["S256"], }, source: "RFC 8414", }), }; const check = new PKCECheck(); const result = await check.run({ targetUrl: "https://auth.example.com", httpClient: mockClient, }); expect(result.status).toBe(CheckStatus.PASS); expect(result.severity).toBeUndefined(); }); it("should FAIL when PKCE is not supported", async () => { const mockClient = { discoverOAuthMetadata: jest.fn().mockResolvedValue({ success: true, data: { code_challenge_methods_supported: [], }, }), }; const check = new PKCECheck(); const result = await check.run({ targetUrl: "https://auth.example.com", httpClient: mockClient, }); expect(result.status).toBe(CheckStatus.FAIL); expect(result.severity).toBe(Severity.CRITICAL); expect(result.remediation).toContain("PKCE"); }); it("should WARNING when metadata cannot be discovered", async () => { const mockClient = { discoverOAuthMetadata: jest.fn().mockResolvedValue({ success: false, attempted: [ { url: "https://auth.example.com/.well-known/...", status: 404 }, ], }), }; const check = new PKCECheck(); const result = await check.run({ targetUrl: "https://auth.example.com", httpClient: mockClient, }); expect(result.status).toBe(CheckStatus.WARNING); expect(result.message).toContain("Unable to discover"); }); });
Testing the full audit flow:
describe("AuditEngine", () => { it("should run all registered checks and generate report", async () => { const engine = new AuditEngine({ target: "https://auth.example.com", timeout: 10000, }); engine.registerChecks([ new PKCECheck(), new StateParameterCheck(), new RedirectURICheck(), new TokenStorageCheck(), ]); const report = await engine.run(); expect(report.results).toHaveLength(4); expect(report.summary.totalChecks).toBe(4); expect(report.summary.compliancePercentage).toBeGreaterThanOrEqual(0); expect(report.summary.riskScore).toBeGreaterThanOrEqual(0); }); });
describe("TerminalReporter", () => { it("should generate colored terminal output", () => { const reporter = new TerminalReporter({ colors: true }); const report = createMockReport(); const output = reporter.generate(report); expect(output).toContain("✓"); expect(output).toContain("✗"); expect(output).toContain("Audit Results"); expect(output).toContain("Summary:"); }); });
Coverage: 75% statements, 86% branches, 93% functions
Defining CheckResult, CheckContext, and AuditorConfig upfront guided every decision. When I needed to add a feature, I asked: "How does this fit the type system?" This prevented architectural mistakes.
Testing against Google OAuth and GitHub revealed:
Lesson from VA.gov: Always test with production systems.
I spent significant time on:
Why? Security tools that are hard to use don't get used.
Type safety caught bugs before runtime:
Impact: Zero runtime type errors in production.
The BaseCheck abstract class makes adding checks trivial. In Phase 2 (NIST compliance), I'll add:
All without changing core architecture.
Remediation guidance is part of check results, not separate docs. This ensures:
After 3 weeks:
Next: Phase 2 will add NIST 800-63B compliance checks and Phase 3 will add custom rules engine.
# Install npm install -g oauth-guardian # Test your OAuth server oauth-guardian https://your-oauth-server.com # Generate HTML report oauth-guardian https://accounts.google.com --format html --output report.html
GitHub: github.com/asg5704/oauth-guardian
Have you found OAuth misconfigurations in production? What security checks would you add to OAuth Guardian? Let me know:
Alexander Garcia built authentication infrastructure at VA.gov for 5+ years, processing 200M+ authentications. He hand-wrote OAuth 2.0 implementations, custom cryptographic libraries, and architected systems serving millions of Veterans and their families.
Experience includes:
Connect: alexandergarcia.dev | GitHub | LinkedIn
This is Part 3 of a 3-part series. ← Read Part 1: The Problem | ← Read Part 2: Architecture