Secure User Authentication with JWT, Access Tokens & Refresh Tokens
Introduction
When a user registers, the application securely stores their credentials in the database. However, storing user information alone is not enough. The application also needs a secure mechanism to recognize authenticated users every time they interact with protected resources.
A login system should answer one simple question:
"How does the server know that this request is coming from an authenticated user?"
The traditional solution was server-side sessions, where the server stored session data in memory or a database. Modern applications, however, often rely on JSON Web Tokens (JWT) because they are lightweight, stateless, and highly scalable.
In this article, we'll build the complete login workflow using JWT, understand why Access Tokens and Refresh Tokens are required, and see how protected APIs verify user identity.
Login Flow
The login request follows the same layered architecture used throughout the project.
Client
│
▼
Routes
│
▼
Controller
│
▼
Service
│
▼
Repository
│
▼
PostgreSQL
The workflow is:
User submits email and password.
Controller receives the request.
Service validates the credentials.
Repository retrieves the user from PostgreSQL.
bcrypt verifies the password.
JWT generates an Access Token.
JWT generates a Refresh Token.
Refresh Token is stored in PostgreSQL.
Both tokens are returned to the client.
Controller
The controller is responsible only for handling the incoming HTTP request.
Its responsibilities are:
Extract email and password.
Call the authentication service.
Return the generated tokens to the client.
Handle errors gracefully.
Service
The authentication service contains the complete login workflow.
It performs the following steps:
Find the user using the email address.
Verify the password using bcrypt.
Generate the JWT Access Token.
Generate the Refresh Token.
Save the Refresh Token in PostgreSQL.
Return user information along with both tokens.
Repository
The repository interacts directly with PostgreSQL.
For login, it performs operations such as:
Finding the user by email.
Updating the Refresh Token after successful authentication.
Keeping database logic isolated from business logic makes the application easier to maintain and test.
Verifying the Password
Although the password entered during login is plain text, the password stored in the database is already hashed.
Instead of converting the stored hash back into the original password (which is impossible), bcrypt hashes the entered password again using the salt embedded inside the stored hash.
const isMatch = await bcrypt.compare(
enteredPassword,
storedHash
);
Internally, bcrypt performs:
Entered Password
↓
Extract Salt from Stored Hash
↓
Hash Entered Password
↓
Compare Hashes
↓
true / false
If both hashes match, authentication succeeds.
Why JWT?
Once the user's identity has been verified, the server needs a way to recognize that user during future requests.
Instead of asking the user to submit their email and password for every API call, the server issues a JSON Web Token (JWT).
The client stores this token and includes it in every protected request.
Authorization: Bearer
This allows the server to authenticate the user without maintaining session data.
Anatomy of a JWT
A JWT consists of three sections.
`Header
.
Payload
.
Signature`
Example:
xxxxx.yyyyy.zzzzz
Header
Contains metadata.
{
"alg": "HS256",
"typ": "JWT"
}
Payload
Contains user claims.
{
"id": 1,
"email": "john@example.com"
}
Signature
Protects the token from tampering.
If someone modifies the payload, the signature immediately becomes invalid.
Why Access Token?
The Access Token is used to authenticate protected API requests.
Examples:
GET /profile
GET /dashboard
POST /logout
Instead of sending credentials repeatedly, the client simply sends:
Authorization: Bearer
Access Tokens are intentionally short-lived (typically 15 minutes to 1 hour).
Why Refresh Token?
If only Access Tokens existed, users would need to log in every time the token expired.
To improve user experience, a Refresh Token is also generated.
When the Access Token expires:
Access Token Expired
↓
Client sends Refresh Token
↓
Server verifies Refresh Token
↓
Generate New Access Token
↓
Continue Using Application
Refresh Tokens usually remain valid for several days.
Authentication Flow
User Login
↓
Verify Email & Password
↓
Generate Access Token
↓
Generate Refresh Token
↓
Store Refresh Token
↓
Return Tokens
↓
Client Stores Tokens
↓
Access Protected APIs
↓
Access Token Expires
↓
Use Refresh Token
↓
Generate New Access Token
JWT Middleware
Protected routes require authentication.
Instead of validating the JWT inside every controller, the application uses middleware.
The middleware:
Reads the Authorization header.
Extracts the JWT.
Verifies its signature.
Decodes the payload.
Attaches the authenticated user to the request.
Passes control to the controller.
If verification fails, the request is rejected.
Client
↓
JWT Middleware
↓
Verify Token
↓
Valid?
↓
Yes → Controller
No → 401 Unauthorized
Testing Login
Request:
POST /api/auth/login
{
"email":"sriya@gmail.com",
"password":"Password123"
}
Response:
{
"success": true,
"user": {
"id": 1,
"username": "Sriya",
"email": "sriya@gmail.com"
},
"accessToken": "...",
"refreshToken": "..."
}
Summary
In this article we:
Implemented secure user login.
Verified passwords using bcrypt.
Generated JWT Access Tokens.
Generated Refresh Tokens.
Stored Refresh Tokens in PostgreSQL.
Protected APIs using JWT middleware.
Built a stateless authentication system.
What's Next?
Now that users can authenticate successfully, the next step is handling one of the most common real-world scenarios:
Forgot Password
In the next article, we'll build a complete password recovery workflow using Resend, secure reset tokens, email delivery, and password reset validation.
Continue Reading Part 4 - https://dev.to/t_sriya_2af6abc7e8d4e87da/part-4-building-an-authentication-system-from-scratch-backend-setup-k2c
Live App: https://auth-flow-five-iota.vercel.app/auth/
Backend API: https://auth-flow-backend-1v2h.onrender.com/
github url: https://github.com/sriyaT/Auth-Flow
Connect : LinkedIn : https://www.linkedin.com/in/t-sriya-b4234510a/, github : https://github.com/sriyaT
Author: Sriya T.




Top comments (0)