Theme
Auth API icon
Sandbox Only — Not for Production

This API is strictly a learning project and demo sandbox. It is actively monitored — all requests are logged with IP, user agent, and timestamps. Abuse, unauthorized production usage, or excessive automated requests will result in immediate IP bans. By accessing the API, you agree to these terms.

Request Access

Acknowledge the acceptable use policy to reveal the API endpoints and Swagger UI.

Role Solo Developer
Runtime .NET 10 / ASP.NET Core 10
Database PostgreSQL (Supabase)
Hosting Fly.io (Docker)
Status Sandbox — Open for Testing
Note Password reset endpoints are scaffolded — email service (SendGrid / Resend) not yet wired up

The Problem

Most auth tutorials stop at "hash the password and return a JWT." Real-world auth needs rate limiting, token refresh, audit trails, role-based access, multiple auth provider support, and graceful error handling — none of which fits in a blog post. I wanted to build the full system end-to-end and make it available for anyone to test or plug into their own local projects.

The Approach

Engineered a full-featured authentication API using Clean Architecture with four distinct layers — Domain, Application, Infrastructure, and Api. The system supports two interchangeable auth providers (Supabase GoTrue and local BCrypt+JWT) behind a shared IAuthProvider interface, meaning the entire auth backend can be swapped without touching a single controller. Deployed on Fly.io as a public sandbox with Swagger docs for easy exploration.

System Architecture

Clean Architecture with strict dependency direction — outer layers depend on inner layers, never the reverse. The Domain layer has zero dependencies. Dual auth providers implement the same interface so switching between Supabase and local auth is a single config change.

API Layer — AuthApi.Api
Controllers · Middleware · Auth Handlers
AuthController UsersController AdminController HealthController ExceptionMiddleware Rate Limiter
↓ depends on
Application — AuthApi.Application
Interfaces · DTOs · Services
IAuthProvider IProfileService IAuditLogger ITokenService IUserRepository
All contracts, zero implementations
↓ implements
Infrastructure — AuthApi.Infrastructure
EF Core · Auth Providers · Security
SupabaseAuthProvider LocalAuthProvider Repositories BCrypt Hasher JWT Service
Domain — AuthApi.Domain
Entities · Enums
AppUser Profile AuditLog RefreshToken UserRole
Zero dependencies

Auth Flow

Client authenticates via email/password, receives JWT access + refresh tokens. Protected endpoints validate the Bearer token. Admin endpoints use a separate API key scheme. Token refresh is handled server-side without re-authentication.

Client
POST /auth/login
AuthApi
Validate credentials
Supabase / Local
Client
access_token + refresh_token
AuthApi
Client
GET /users/me (Bearer token)
AuthApi

API Structure

RESTful API versioned at /api/v1. Auth endpoints are public but rate-limited. User endpoints require JWT Bearer. Admin endpoints require an API key header. All errors return a consistent JSON shape with correlation IDs for debugging.

Method Endpoint Auth Purpose
POST /api/v1/auth/register Public Create account, returns JWT tokens
POST /api/v1/auth/login Public Authenticate with email/password
POST /api/v1/auth/refresh Public Exchange refresh token for new pair
POST /api/v1/auth/logout Bearer Revoke current session
POST /api/v1/auth/forgot-password Public Initiate password reset · email service not yet integrated
POST /api/v1/auth/reset-password Public Reset password with token · pending email service
GET /api/v1/users/me Bearer Get current user profile
PATCH /api/v1/users/me Bearer Update profile (fullName, avatarUrl)
GET /api/v1/admin/audit-logs API Key Paginated audit trail
GET /api/v1/admin/users API Key All user profiles (admin only)
GET /health Public Liveness probe
GET /health/dependencies Public Deep health check (DB + Auth)

Dual Auth Providers

Both implement IAuthProvider — same interface, different backends. Switch with a single config value.

Supabase Provider
Currently active on the sandbox
Proxies Supabase GoTrue REST API for auth operations. Battle-tested JWT infrastructure — no secrets rotation to manage.
Local Provider
Offline / self-hosted
BCrypt (work factor 12) + local JWTs. Full auth flow without any external service — for dev, testing, or air-gapped deployments.

Security & Rate Limiting

JWT Bearer Auth
User endpoints (/users/*)
Access tokens expire in 15 minutes. Refresh tokens last 7 days, stored hashed in PostgreSQL.
API Key Auth
Admin endpoints (/admin/*)
Separate scheme via X-Api-Key header. Keeps admin access decoupled from user auth flow.
Rate Limiting
Per-IP throttling
Auth-sensitive: 5 req / 60s per IP. General: 30 req / 60s. Built-in ASP.NET Core rate limiter — no external dependencies.
Audit Logging
Every auth action tracked
User ID, IP, user agent, timestamp, success/failure — all logged. Queryable via admin endpoint with pagination.

Database Design

PostgreSQL with Entity Framework Core 10. Profiles mirror Supabase auth users for local data ownership. Audit logs track every auth action with IP, user agent, and success/failure for security forensics. Refresh tokens have expiry tracking for automatic cleanup.

profiles
  • id UUID PK
  • email VARCHAR UNIQUE
  • full_name VARCHAR
  • avatar_url VARCHAR NULL
  • role ENUM (User/Admin)
app_users
  • id UUID PK
  • email VARCHAR UNIQUE
  • password_hash VARCHAR
  • email_confirmed BOOLEAN
audit_logs
  • id SERIAL PK
  • action VARCHAR
  • user_id UUID NULL
  • ip_address VARCHAR
  • success BOOLEAN
refresh_tokens
  • id UUID PK
  • user_id FK → app_users
  • token_hash VARCHAR
  • expires_at TIMESTAMP
  • revoked BOOLEAN

Technical Decisions & Tradeoffs

Clean Architecture
Chose: 4-layer strict dependency direction
Domain has zero dependencies. Application defines interfaces only. Infrastructure implements everything. Api wires it all via DI. This means swapping PostgreSQL for another DB or Supabase for Firebase touches Infrastructure only — no controller or service code changes.
Alternative: Vertical slices — faster for small projects, but harder to swap providers.
Dual Auth Providers
Chose: IAuthProvider interface with Supabase + Local implementations
Production uses Supabase GoTrue (battle-tested, managed). Development and self-hosted deployments use local BCrypt+JWT. Switching is a single config change: Auth:Provider = "Local". No branching logic in controllers.
Alternative: Supabase only — simpler, but locks you into one vendor.
BCrypt Work Factor 12
Chose: Balanced hashing cost
Work factor 12 takes ~250ms per hash on modern hardware — slow enough to make brute force impractical, fast enough that login doesn't feel laggy. Lower factors are vulnerable; higher factors punish legitimate users.
Alternative: Argon2id — more resistant to GPU attacks, but less ecosystem support in .NET.
Docker on Fly.io
Chose: Multi-stage Docker build + Fly.io auto-scaling
Multi-stage build keeps the image lean (no SDK, runtime only). Fly.io auto-stops when idle and auto-starts on requests — minimal cost for a sandbox. Forced HTTPS at the edge with no app-level TLS config.
Alternative: Azure App Service — better .NET integration, but higher idle cost.

Error Handling

Every error — validation, auth failure, rate limiting, server error — returns a consistent JSON shape with a correlationId that maps to server-side logs. Global exception middleware catches unhandled errors so no stack traces ever leak to clients.

Error Response Shape
{
  "status": 401,
  "message": "Invalid credentials.",
  "correlationId": "0HNJ..."
}
Rate Limited (429)
{
  "status": 429,
  "message": "Too many requests. Please try again later."
}

What It Does

Register & Login
Token Refresh
Role-Based Access
Rate Limiting
Audit Logging
Health Checks
Password Reset soon
Swagger Docs

What I Learned

Building a full auth system — not a tutorial, but a deployed, documented API — forced me to think about every edge case: What happens when Supabase is down? How do you prevent user enumeration on forgot-password? Why does the rate limiter need separate policies for auth vs general endpoints? Clean Architecture paid off the moment I needed to add the local auth provider — zero changes to controllers, just a new class implementing the same interface. The dual-provider pattern is something I'll carry into every future project.

Next Project