Building a Production-Grade Authentication Service (From Scratch)

Most authentication tutorials stop at “generate a JWT and you’re done.”
That’s not how real systems work.
I wanted to understand authentication deeply, not just use libraries, so I built a production-grade authentication service from scratch with security, scalability, and real-world failure scenarios in mind.
Goals
Understand how authentication works internally
Build secure token-based authentication
Handle real-world issues like token theft, replay attacks, and brute force
Design a system that scales horizontally
Tech Stack
Node.js with Express for the API layer
PostgreSQL for persistent storage
Redis for caching and coordination
JWT for access tokens
bcrypt for hashing passwords and refresh tokens
Core Design Philosophy
I designed the system around a simple principle: separate performance from control.
This led to a two-token architecture.
Access Tokens (Stateless)
Short-lived, around 15 minutes
Stored on the client
Not stored in the database
Verified using a signature
Refresh Tokens (Stateful)
Long-lived
Stored in hashed form in the database
Can be revoked
Used to generate new access tokens
This separation provides performance through stateless verification and security through centralized control.
Password Storage
Passwords are never stored directly.
bcrypt.hash(password, 10)
bcrypt provides built-in salting, resistance to brute force attacks, and an adjustable cost factor.
Even if the database is compromised, raw passwords are not exposed.
Token System Design
Access Token
Contains userId and role
Signed using a secret key
Not encrypted, only signed
Verified on every request
JWT payloads are readable. Security comes from the signature, not secrecy.
Refresh Token Design
Instead of storing raw tokens, I generate a combination of tokenId and tokenSecret.
Only the hashed tokenSecret is stored in the database, and the combined token is returned to the client.
If the database is compromised, attackers cannot use the stored tokens because they are hashed.
Refresh Token Rotation
On every refresh request, the old token is invalidated and a new one is issued.
This prevents replay attacks and reuse of stolen tokens.
Token Revocation
Each refresh token includes a revoked flag and an expiration timestamp.
This allows invalidating tokens instantly and logging out specific sessions.
Security Hardening
Rate limiting is used to prevent brute force attacks and returns HTTP 429 when limits are exceeded.
Account lockout is implemented after multiple failed login attempts to prevent password guessing.
Refresh tokens are stored in hashed form to protect against database leaks.
JWT signatures ensure that any tampering makes the token invalid.
Architecture Refactor
Initially, all logic was inside route handlers.
I refactored the system into:
Routes for handling HTTP requests
Services for business logic
Middleware for reusable functionality
This improved code structure, testability, and scalability.
Error Handling
Instead of using try-catch blocks everywhere, I implemented centralized error handling middleware.
This ensures consistent responses, cleaner code, and easier debugging.
Background Jobs
Expired tokens are periodically cleaned up from the database.
This prevents unnecessary data growth and maintains performance.
JWT Key Rotation
Instead of relying on a single secret key, the system supports multiple keys.
Old keys can be phased out gradually, allowing safe rotation without downtime.
Key Learnings
JWT is not secure by default.
Stateless and stateful approaches involve tradeoffs.
Security is achieved through multiple layers.
Refresh tokens provide the main control mechanism.
Separation of concerns improves system design.
What I’d Improve Next
Add OAuth support such as Google or GitHub login
Implement two-factor authentication
Add device and session tracking
Move secrets to a dedicated secret manager
Conclusion
This project changed how I think about backend systems.
Authentication is not just about logging users in.
It is about trust, control, and handling failure scenarios correctly.
Understanding how the system behaves under attack is just as important as making it work.
