Skip to content

Auth / Anubis

Anubis is the identity module for L.Edge.R — an edge-native authentication service built on Cloudflare Workers, Hono, D1, and WebCrypto. Ships as a module inside the L.Edge.R monorepo for MVP, designed for extraction into a standalone company-wide IdP.

Stack

Concern Technology
Runtime Cloudflare Workers (V8)
Framework Hono
Database D1 (SQLite, shared with backend/)
JWT jose (HS256 → RS256 on extraction)
Password hashing WebCrypto crypto.subtle PBKDF2-SHA256
Language TypeScript (strict)

Architecture

Follows the same Clean/Hexagonal pattern as backend/:

anubis/src/
├── domain/            Pure entities and domain errors. No external deps.
├── application/
│   ├── ports/         Interfaces (contracts) for infrastructure.
│   └── use-cases/     Orchestrate domain logic via ports.
└── infrastructure/
    ├── db/            D1 repository implementations.
    ├── jwt/           jose-based token service.
    ├── password/      WebCrypto PBKDF2 password service.
    ├── http/          Hono routes and auth middleware.
    └── container/     Dependency injection factory.

Zero coupling to accounting domain. Auth domain imports nothing from src/domain/ or src/application/ outside src/auth/.

API

Method Path Auth Description
POST /auth/signup No Create account + get JWT
POST /auth/login No Verify credentials + JWT
GET /auth/me Bearer token Return current user

Response: { token: string, user: { id, email, name } }

Setup

1. Install

npm install

2. Set JWT Secret

wrangler secret put JWT_SECRET
# Generate with: openssl rand -base64 32

3. Apply Migration

# Local:
wrangler d1 migrations apply auth-db --local --env local

# Production:
wrangler d1 migrations apply auth-db --env prod

4. Run Locally

npm run dev

Domain

AuthUser Entity

id: string       (UUID v4)
email: string    (lowercase, trimmed)
name: string
passwordHash: string
createdAt: string
updatedAt: string

AuthErrors

  • InvalidCredentialsError — wrong email or password (same message, prevents email enumeration)
  • UserAlreadyExistsError — signup with existing email
  • UnauthorizedError — missing or invalid token

Use Cases

SignupUseCase

  1. Normalize email
  2. Check for existing user → UserAlreadyExistsError if found
  3. Hash password (PBKDF2-SHA256, 310,000 iterations)
  4. Create AuthUser with UUID
  5. Save to D1
  6. Sign JWT
  7. Return { token, user: { id, email, name } }

LoginUseCase

  1. Normalize email
  2. Find by email → InvalidCredentialsError if not found
  3. Verify password → InvalidCredentialsError if false (same error as step 2 — no enumeration)
  4. Sign JWT
  5. Return { token, user }

ValidateTokenUseCase

  1. Verify JWT
  2. Find user by sub claim
  3. Return AuthUser

Security Details

Password Hashing (PBKDF2)

  • Algorithm: PBKDF2-SHA256, 310,000 iterations (NIST SP 800-132)
  • Stored format: pbkdf2:sha256:<iterations>:<base64salt>:<base64hash>
  • Format allows iteration count upgrades without breaking existing hashes

JWT (HS256 → RS256)

  • MVP: HS256 symmetric. JWT_SECRET is the signing key.
  • On extraction: RS256 asymmetric. Auth Worker holds private key; consumer Workers verify via GET /auth/jwks.json.
  • Expiry: 7 days for MVP (no refresh token yet)
  • jti (JWT ID) included for future token revocation

JWT claims:

{
  "sub": "<user-id>",
  "email": "[email protected]",
  "name": "User Name",
  "iat": 1700000000,
  "exp": 1700604800,
  "jti": "<uuid>"
}

Rate Limiting (MVP)

Cloudflare WAF rules (free tier): - POST /auth/login — max 10 requests/IP/min → 429 - POST /auth/signup — max 5 requests/IP/min → 429

Key Design Decisions

HS256 now, RS256 on extraction

MVP uses symmetric HMAC-SHA256. backend/ validates tokens using the same JWT_SECRET. When this module extracts to a standalone Worker, switch to RS256: auth holds the private key, all consumer services verify via GET /auth/jwks.json without needing the secret.

Shared D1 database

Auth and backend/ bind to the same D1 instance, sharing the users table. Auth owns the password_hash column. On extraction, create a dedicated auth D1 and migrate user records.

Roadmap

Phase 2 — Hardening

  • Refresh tokens
  • Password reset
  • Google OAuth
  • Role-based authorization middleware
  • Token revocation list (KV store, check jti)
  • Re-hash passwords on login if iteration count outdated

Phase 3 — Extraction to Standalone Worker

  • Move to own repo/Worker project
  • Generate RSA key pair, switch to RS256
  • Add GET /auth/jwks.json
  • Add admin console UI
  • Update backend/ authMiddleware to fetch JWKS remotely
  • Create dedicated D1, migrate users