Overview
SecureAuth is a standalone authentication microservice you can drop into any project. It handles everything from initial registration to token refresh, session management, and role-based permissions — so you don't have to rewrite auth from scratch every time.
The problem it solves
I kept writing the same authentication boilerplate across projects. Basic JWT, no refresh tokens, no session management, no way to revoke tokens. It worked until it didn't.
SecureAuth is what I wish I had from the start.
Architecture
The API follows a clean separation: routes handle HTTP concerns, services contain the business logic, and repositories handle all database interactions. Nothing touches the database directly except the repository layer.
src/
├── routes/
│ ├── auth.routes.ts # /register, /login, /refresh, /logout
│ └── users.routes.ts # /me, /sessions, /sessions/:id
├── services/
│ ├── auth.service.ts # Token generation, validation, rotation
│ └── user.service.ts # User CRUD, password hashing
├── repositories/
│ ├── user.repo.ts
│ └── session.repo.ts
└── middleware/
├── authenticate.ts # Verify access token
└── authorize.ts # Check roles/permissions
Key features
- Refresh token rotation — every refresh issues a new refresh token and invalidates the old one
- Token family detection — reused refresh tokens trigger a full session wipe (theft detection)
- Device sessions — users can see all active sessions with device info and revoke any of them
- Role-based access — attach roles to users, protect routes with
authorize('admin')middleware - Rate limiting — login attempts are rate-limited per IP with exponential backoff
Database schema
The two core tables beyond users are refresh_tokens and audit_logs:
CREATE TABLE refresh_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
token TEXT UNIQUE NOT NULL,
family UUID NOT NULL, -- for theft detection
device_info JSONB,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
What I learned
The hardest part was handling concurrent token refresh requests — if two API calls fire at the same moment with an expired access token, both shouldn't try to refresh simultaneously. I solved this with a client-side queue that holds pending requests while a refresh is in flight.
The second hard thing was testing. Auth has a lot of edge cases: expired tokens, revoked tokens, concurrent requests, clock skew. I ended up with a comprehensive test suite using Jest and Supertest before I trusted it in production.