A user authentication service in Go — focused on getting identity flows right rather than on breadth of features. Modular architecture inspired by Docker (Moby): interface-driven boundaries, one concern per package.
Google OIDC sign-in, implemented end to end:
- Authorization Code flow with PKCE — a per-request verifier (
oauth2.GenerateVerifier), challenge on the authorize call, verifier proved at exchange stateparameter validated on the callback — closes login-CSRF, where an attacker completes their own authorization and lands the victim in the attacker's account- The in-flight exchange is held in a
__Host-prefixed cookie with a 5-minute TTL —Secure,Path=/, noDomain, so a subdomain can't write it - Scopes
openid email profile, with the OIDC userinfo endpoint for the identity claims
JWT validation with the signing algorithm pinned — jwt.WithValidMethods([]string{HS256}). Without this, a token presented with alg: none or a swapped algorithm can be accepted as valid; it's the single most commonly missed line in JWT handling.
Session authentication as the alternate path — session tokens are stored hashed in MySQL, not in plaintext, so a database read doesn't hand over live sessions. Cookies are HttpOnly, Secure, SameSite=Lax.
Passwords are bcrypt, via GenerateFromPassword / CompareHashAndPassword.
POST /auth/signup
POST /auth/login
POST /auth/logout (session middleware)
GET /oauth2/login → redirect to provider
GET /oauth2/callback → state check, code exchange, session issue
This is a relying party — it consumes an identity provider, it does not issue tokens as an authorization server. Single provider (Google); multi-provider support is stubbed, not built.
It also carries almost no tests — one file, covering the error-type helpers. That's a deliberate scope choice rather than an oversight: this repo exists as a focused study of the identity flows above, and the test discipline I apply to systems I operate lives in my other work (velox runs 29 database-invariant checks in CI on every push; notif-service publishes benchmarks with their failed runs). Read this one for the auth code, not as an example of my testing practice.
All secrets come from the environment (envconfig) — nothing is committed:
OAUTH2_CLIENT_ID, OAUTH2_CLIENT_SECRET, OAUTH2_REDIRECT_URL
MYSQL_HOST, MYSQL_PORT, MYSQL_DB, MYSQL_USER, MYSQL_PASSWORD
HOST, PORT, MODE
go run ./cmd/serverGo · MySQL · ~3,800 lines