Skip to content

About

Production-shaped Go JWT auth service — bcrypt, refresh-token rotation, Postgres, Prometheus + OpenTelemetry

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

goauth-demo

CI License: MIT Go Version

A small, production-shaped JWT authentication service in Go. Built as a focused showcase: real auth fundamentals (bcrypt, refresh-token rotation, single-use redemption), Postgres-backed, instrumented with Prometheus metrics + OpenTelemetry tracing.

Scope note. This is intentionally minimal — it covers the email/password + JWT path well rather than covering every protocol poorly. OAuth/OIDC/SAML are listed as next steps below, not pretended to be implemented.

Features

  • Email/password registration with bcrypt hashing
  • JWT access tokens (HS256, short-lived, configurable TTL)
  • Refresh tokens stored as SHA-256 hashes in Postgres, rotated on every redemption (single-use; reuse fails)
  • Logout revokes all of a user's refresh tokens
  • Per-IP + per-email login rate limiting (golang.org/x/time/rate)
  • Constant-time login (dummy bcrypt on unknown email to keep timing similar)
  • /healthz, /metrics (Prometheus), OpenTelemetry HTTP instrumentation
  • Structured JSON logging via log/slog
  • Goose-style migrations (golang-migrate format)
  • Table-driven tests against an in-memory store; CI runs go vet + go test -race -cover

Architecture

cmd/server/        wiring & graceful shutdown
internal/auth/     domain logic — Service exposes Register/Login/Refresh/Logout
internal/store/    pgx-backed Store implementing auth.Store
internal/httpapi/  chi router, handlers, rate limiter, auth middleware
internal/observability/ Prometheus + OTel HTTP middleware
migrations/        golang-migrate SQL files
loadtest/          k6 scripts for /v1/login and /v1/refresh

Request lifecycle

                     ┌──────────────────────────────────────┐
client ─── POST ───▶ │  observability   (otelhttp + metrics)│
                     │       │                              │
                     │       ▼                              │
                     │   chi router                         │
                     │       │                              │
                     │       ├── /v1/login   ─┐             │
                     │       ├── /v1/refresh ─┤             │
                     │       └── /v1/me      ─┤  middleware │
                     │           (Bearer JWT) ─┘  validates │
                     │       │                              │
                     │       ▼                              │
                     │   auth.Service  (bcrypt, JWT, rotate)│
                     │       │                              │
                     │       ▼                              │
                     │   store.Store   (pgx → Postgres)     │
                     └──────────────────────────────────────┘

The split between auth.Service (domain) and store.Store (persistence) is deliberate: tests run against an in-memory auth.Store implementation, so domain behaviour is verified without spinning up Postgres.

Why refresh-token rotation?

Refresh tokens live longer than access tokens, so they're a juicier target. We mitigate by:

  1. Storing only sha256(token) in the DB — a DB leak doesn't yield usable tokens.
  2. Rotating on every refresh — the redemption is an atomic UPDATE ... SET revoked_at = now() WHERE revoked_at IS NULL RETURNING user_id. If the same token is presented twice, the second attempt finds zero rows and fails.
  3. Logout revokes all of a user's refresh tokens, not just the current one.

Running locally

make db-up    # starts Postgres in Docker, runs migrations
make run      # starts the server on :8080
make test     # runs all tests with race detector

API

# Register
curl -s localhost:8080/v1/register -d '{"email":"a@b.com","password":"hunter2hunter"}'

# Login
TOKENS=$(curl -s localhost:8080/v1/login -d '{"email":"a@b.com","password":"hunter2hunter"}')
ACCESS=$(echo $TOKENS | jq -r .access_token)
REFRESH=$(echo $TOKENS | jq -r .refresh_token)

# Authenticated call
curl -s localhost:8080/v1/me -H "Authorization: Bearer $ACCESS"

# Refresh (returns a new pair; old refresh token is now invalid)
curl -s localhost:8080/v1/refresh -d "{\"refresh_token\":\"$REFRESH\"}"

# Logout (revokes all refresh tokens for the user)
curl -s -X POST localhost:8080/v1/logout -H "Authorization: Bearer $ACCESS"

Load testing

loadtest/ contains k6 scripts for the two hot paths:

# /v1/login — bcrypt-bound, 50 VUs × 30s with p95<200ms / p99<500ms thresholds
k6 run loadtest/login.js

# /v1/refresh — verifies atomic single-use rotation holds under concurrency
k6 run loadtest/refresh.js

See loadtest/README.md for tuning.

Observability

  • /metrics exposes Prometheus counters/histograms for request rate, status code, and latency per endpoint.
  • All inbound HTTP requests are wrapped with otelhttp.NewHandler, so spans propagate automatically when an OTLP exporter is configured.
  • All logs are structured JSON via slog.

What I deliberately left out

  • OAuth / OIDC / SAML — meaningful implementations are large; faking them would be worse than omitting them. Natural next step.
  • Email verification & password reset — would need an email transport abstraction and a token table.
  • MFA / WebAuthn — separate problem space.
  • Distributed rate limiting — current limiter is in-memory; for multi-instance deploys this would move to Redis.
  • Frontend — out of scope.

Stack

Go 1.23 · chi · pgx/v5 · golang-jwt/jwt/v5 · bcrypt · prometheus/client_golang · OpenTelemetry · golang-migrate

About

Production-shaped Go JWT auth service — bcrypt, refresh-token rotation, Postgres, Prometheus + OpenTelemetry

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages