An internship case study in building distributed authentication systems reveals how cross-domain cookie handling, PKCE flows for CLI tools, and strict layered architecture create real-world constraints that textbooks rarely cover.

Authentication sounds straightforward until you implement it across multiple applications sharing a single PostgreSQL database. The gap between "use OAuth" and "make it actually work in production" is where most of the interesting engineering happens.
The Problem Space
The system required three separate applications to authenticate users through GitHub OAuth: a backend API handling token management, a CLI tool for terminal-based access, and a server-side rendered web portal. All three needed to share authentication state while operating in different execution environments.
The backend managed JWT access tokens with 3-minute expiry and opaque refresh tokens with single-use rotation. The CLI needed to complete OAuth in a browser but hand results back to a terminal. The web portal called the backend on every request. Simple in theory, problematic in practice.
The Cross-Domain Cookie Problem
The first major failure came from a domain mismatch. The backend set cookies after OAuth completion, but the web portal lived on a different domain. Browsers blocked those cookies. The login flow worked, but sessions never persisted.
The solution required redirecting with tokens as query parameters (?at=...&rt=...) instead of setting cookies directly. The frontend's /auth/callback route received them and set cookies on its own domain. Two lines of redirect logic that fundamentally changed the architecture.
This reveals a common distributed systems pattern: when components run in different security boundaries, you cannot assume shared state. The backend cannot reach into the frontend's cookie jar, no matter how convenient that would be.
PKCE for Terminal-Based Auth
The CLI authentication presented a different challenge. Browser-based auth and terminal-based auth have fundamentally different security models. PKCE (Proof Key for Code Exchange) prevents auth code interception by binding the authorization code to the client that requested it.
The implementation spun up a local HTTP server on port 9876 to catch the OAuth callback. The user's browser completed GitHub login, GitHub redirected to localhost:9876, the CLI captured the code, exchanged it for tokens, stored them at ~/.insighta/credentials.json, and shut down.
This approach works but introduces its own constraints: port conflicts if another process uses 9876, firewall issues on restrictive networks, and the assumption that the user's browser can reach localhost. Each assumption is a potential failure mode.
Database-Level Issues
Smaller bugs accumulated. The upsertUser function inserted users as inactive, preventing returning users from logging in after their first session. The fix was straightforward: ON CONFLICT DO UPDATE SET is_active = true. But it required understanding the exact semantics of the upsert operation.
The UUID import used v4 instead of v7. Version 7 UUIDs are time-ordered, which matters for database index performance at scale. A v4 UUID on every insert causes page splits in B-tree indexes; a v7 UUID inserts in order.
Team Architecture Constraints
The team project (a Go-based booking system with Gin and PostgreSQL) enforced strict layered architecture: handlers, services, repositories. No business logic in handlers, no database calls in services. Every operation touching multiple tables required atomic transactions.
The cancellation feature had a subtle bug in refund window calculation. The policy was full refund if cancelled more than 12 hours before the session. The initial implementation calculated from the time of cancellation request, not from the scheduled session start time. A client cancelling 13 hours before a session scheduled 1 hour from now would incorrectly get a full refund.
Fixing it meant recalculating relative to scheduled_start_time instead of NOW(). One line change, but it required precise business rule specification first. "12 hours before" is ambiguous; "12 hours before the session start time" is not.
Trade-offs and Patterns
Several patterns emerge from this work:
Token placement determines architecture. When the backend sets cookies directly, it assumes control over the client's execution environment. When tokens pass as query parameters, the frontend controls cookie lifecycle. This separation creates more complexity but enables independent deployment.
CLI authentication is inherently limited. The local server approach works for development tools but fails in headless environments (CI/CD, containers). For those cases, device flow OAuth or personal access tokens are more appropriate.
Type systems catch different things. Go's static typing caught errors that JavaScript's runtime would miss. The trade-off is more boilerplate and slower iteration during prototyping.
Ambiguous specifications create bugs. "12 hours before" vs "12 hours before session start time" sounds like semantics, but it produces different refund calculations. Business logic requires precise temporal references.
Atomic transactions are non-negotiable for consistency. When a booking operation touches availability slots, subscription credits, and booking records, partial failures create orphaned data. All-or-nothing is the only safe default.
Implementation Notes
The backend used JWT with 3-minute expiry and refresh token rotation. Each refresh token was single-use; using one invalidated it and issued a new one. This prevents token replay attacks but requires the client to handle token refresh reliably.
The web portal was EJS (server-side rendered). Every request called the backend, meaning no client-side state management. This simplified the architecture but increased latency for each page load.
The database schema handled ON CONFLICT clauses explicitly. PostgreSQL's upsert semantics (INSERT ... ON CONFLICT ... DO UPDATE) prevent race conditions that separate SELECT-then-INSERT patterns would introduce.
What This Reveals
Building authentication across multiple applications is a coordination problem as much as a technical one. Each component makes assumptions about the others' execution environment. When those assumptions mismatch, you get cross-domain cookie failures, token handling mismatches, and business logic bugs that look trivial in hindsight.
The fix is not better documentation or more careful coding. It is designing for the constraints first: which domains control which cookies, which environments can complete which flows, which operations require atomicity. Start with the constraints, then implement the solution. The reverse order creates the bugs described here.

Comments
Please log in or register to join the discussion