← All projects

Case · Go backend

Keyring: an authentication service

An authentication and authorization service in Go: EdDSA JWTs with key rotation and JWKS, refresh tokens with rotation and theft detection, TOTP 2FA (RFC 6238) with backup codes, RBAC, lockout on brute force, password reset by a one-time token, an audit log, a device list, openapi.yaml. Not independently audited, not a production IdP.

  • Go
  • stdlib crypto
  • SQLite
  • modernc.org/sqlite (no CGO)
  • JWT EdDSA/Ed25519
  • JWKS
  • PBKDF2
  • TOTP
  • RBAC
  • REST
  • OpenAPI 3
  • Vanilla JS
  • embed
  • Docker
  • GitHub Actions

01 · How it looks

Screenshots

Screenshot of the project "Keyring: an authentication service" at 1280 pixels wide
Desktop, 1280 px
Screenshot of the project "Keyring: an authentication service" on a phone screen, 390 pixels wide
Phone, 390 px

02 · About

What it is and why

Keyring is an authentication and authorization service in Go: sign-up and sign-in, short-lived JWTs with key rotation and JWKS, refresh tokens with rotation and theft detection, TOTP 2FA with backup codes, RBAC, brute-force protection, password reset, an audit log and device management.

The standard library plus modernc.org/sqlite (no CGO); the interface is vanilla JS embedded through embed, and the API is described in openapi.yaml. This is a learning project: it has not been independently security audited and is not a production IdP; the limitations are listed below.

Limitations

Not independently audited and not a production IdP. There is no OAuth 2.0 or OIDC, WebAuthn, e-mail confirmation or social sign-in. Passwords use PBKDF2 rather than argon2id because argon2 is not in the stdlib.

TOTP secrets and private signing keys sit in the database in plain text, so a database dump lets someone forge tokens. The password reset e-mail is a stub (a log and a file), and there is no QR code for 2FA.

Instant revocation works only inside the service itself: a third-party JWT consumer learns of a revocation no earlier than exp (10 minutes). The rate limiter lives in the memory of one process, and there is no scaling out.

The demo passwords are public, shown in the interface and marked with a banner; they must not be used outside a demonstration. The -race tests run only in CI, and the Docker image has not been built.

What is inside

  • A JWT access token with alg=EdDSA (Ed25519): signing and parsing are written on the stdlib, none and HS256 are rejected before the signature check, and exp, nbf, iss, aud and typ are verified
  • Key rotation through the API and a public JWKS (RFC 7517/8037): another service verifies tokens itself, and the old key lives on for access_ttl + 1 minute
  • Refresh tokens are stored as a SHA-256 hash; presenting an already used token revokes the whole token family and writes token.reuse_detected to the audit log
  • TOTP per RFC 6238 on the stdlib, checked against the RFC test vectors, a +-1 step window, replay protection and 8 one-time backup codes
  • Passwords: PBKDF2-HMAC-SHA256, 600,000 iterations, constant-time comparison, and a dummy hash for an unknown login so it does not reveal whether the user exists
  • Lockout on brute force with exponential delay (429 and Retry-After), plus a per-IP request limit
  • RBAC: permissions are read from the database on every request; you cannot leave the system without an administrator or disable yourself
  • Password reset by a one-time token that ends all sessions; an append-only audit log (database triggers forbid UPDATE and DELETE)
  • A device list with session revocation, a CSP without unsafe-inline, HttpOnly + SameSite=Strict cookies, a CSRF token and Origin checks

03 · How to run

Run it locally

git clone <repository on request> auth-service
cd auth-service
SEED_DEMO=1 go run ./cmd/server   # http://localhost:8101, the public demo accounts are shown on the sign-in page

04 · Code

The repository is private

The source lives in a private repository. I will grant access on request: write to me and I will send an invitation.

Request the code