Hello! Welcome back to our course on cybersecurity.
In our last lesson, you did a great job dissecting the structure of a JSON Web Token, understanding its three core components: the header, payload, and signature. We established that the signature guarantees the token's integrity.
Now, we'll put that knowledge into action. Your goal for this lesson is to explain how the JWT-based authentication flow works in web applications. We'll trace the journey of a JWT from the moment a user logs in to how it's used to grant access to protected resources. Understanding this flow is fundamental to implementing secure authentication in the Next.js applications you build.
1. The Core JWT Authentication Flow
At a high level, the JWT authentication process involves the client proving its identity once, receiving a token (like a temporary ID card), and then showing that token for all future requests.
To get a clear overview of this process, let's start with a short, animated explanation.
Session Vs JWT: The Differences You May Not Know!
This segment from ByteByteGo provides a concise, step-by-step walkthrough of the entire JWT authentication flow, from credential verification to using the token for subsequent requests.
Watch the video from 01:43 to 02:40. Focus on the sequence of events between the user, the server, and the client application.
As the video showed, we can break this process down into four main steps:
-
Login and Token Issuance: A user submits their credentials (e.g., email and password) to a login endpoint. The server validates these credentials. If they are correct, the server generates a JWT, signs it with a secret key, and sends it back to the client.
-
Client-Side Storage: The client application receives the JWT and must store it. Common storage options include browser
localStorage,sessionStorage, or anHttpOnlycookie. We'll discuss the security pros and cons of these options in the next lesson, but for now, the key point is that the client holds onto the token. -
Authenticated Requests: For any subsequent request to a protected API endpoint or page, the client includes the JWT in the
Authorizationheader, typically using theBearerscheme. The header looks like this:Authorization: Bearer <your_jwt_here>. -
Server-Side Verification: The server receives the request, extracts the token from the header, and verifies its signature using the same secret key it used to sign it. It also checks claims like the expiration date (
exp). If the signature is valid and the token hasn't expired, the server trusts the claims in the payload (like the user ID) and processes the request.
This flow is visually summarized in the diagram below.

A major advantage of this approach is that it's stateless. The server doesn't need to keep a record of active sessions in a database or memory. All the information required for authentication is contained within the token itself, which is held by the client. This makes it much easier to scale applications, especially in modern microservice architectures where requests from a single user might be handled by many different servers.
2. A Practical Look at the Flow in Next.js
Given your experience with Next.js, let's see how these steps translate into code. The following article provides a guide to implementing JWT authentication in a Next.js application. We'll focus on two key parts: the login API that issues the token, and the middleware that protects routes.
Complete Guide to JWT Authentication in Next.js 15
The article 'Complete Guide to JWT Authentication in Next.js 15' provides excellent code examples that map directly to the flow we've just discussed. We'll examine the server-side logic for token issuance and verification.
Please review two sections of this article: Building the Login API: Found under the heading of the same name. Notice how it verifies the user, creates a token with createToken, and sets it in an HttpOnly cookie in the response. Implementing Middleware Protection: Found under its own heading. Observe how the middleware.ts file intercepts requests to protected routes (like /dashboard), retrieves the token from the cookie, and uses verifyToken to validate it. Don't worry about understanding every line of code; focus on how the code implements the steps of the authentication flow.
These code snippets demonstrate the JWT flow in a tangible way:
- The
/api/auth/loginroute handles Step 1 (Token Issuance) and Step 2 (Token Transmission) by creating the JWT and sending it to the client inside a cookie. - The
middleware.tsfile handles Step 4 (Server-Side Verification) for every request to a protected route, ensuring that only users with a valid token can proceed.
3. Handling Expiration: Access Tokens and Refresh Tokens
There's a critical challenge with the flow we've described so far. For security, JWTs (often called access tokens) should have a short expiration time—perhaps 15 minutes. This limits the damage if a token is stolen. But if the token expires every 15 minutes, does the user have to log in again? That would be a terrible user experience.
This is solved by introducing a second type of token: the refresh token.
The flow works like this:
- When the user first logs in, the server issues both a short-lived access token and a long-lived refresh token.
- The client uses the access token for all API requests, just as before. The refresh token is stored securely and is only sent to a special token refresh endpoint.
- When the access token expires, the client's next API request will fail. The client detects this, and instead of asking the user to log in, it sends its refresh token to the
/api/refresh-tokenendpoint. - The server verifies the refresh token (checking that it's valid and hasn't been revoked). If it's good, the server issues a brand new access token.
- The client can now retry the failed API request with the new access token.
This entire process happens automatically, without the user even noticing. The following video explains this dual-token strategy very clearly.
Session Vs JWT: The Differences You May Not Know!
This segment from ByteByteGo explains the refresh token pattern, clarifying the roles of the short-lived access token and the long-lived refresh token in balancing security with user experience.
Watch the video from 03:52 to 05:06. Pay close attention to the different lifespans and purposes of the access token versus the refresh token.
Test your understanding!
A user is actively using your web application. Their JWT access token, which has a 15-minute lifespan, expires.
Which token should the client application send to the server to get a new access token, and why shouldn't it send the other token with every API request?
Show answer
The client should send the refresh token to a dedicated endpoint (e.g., /api/refresh) to obtain a new access token.
It shouldn't send the refresh token with every regular API request because the refresh token is more powerful and long-lived. Exposing it with every request would increase the risk of it being stolen. The access token is designed to be sent frequently, and its short lifespan minimizes the security risk if it's compromised. The refresh token is only used on the rare occasion that the access token expires.
Conclusion
You now have a comprehensive understanding of the entire JWT-based authentication flow, from the initial login to maintaining a session with refresh tokens.
Key Takeaways:
- The JWT authentication flow begins with the user logging in to receive a signed JWT from the server.
- The client sends this JWT in the
Authorizationheader of subsequent requests to prove its identity. - This process is stateless, which is highly beneficial for scalable, modern applications.
- To balance security and user experience, a pattern of short-lived access tokens and long-lived refresh tokens is used to handle token expiration seamlessly.
In our next lesson, we'll focus on the security implications of this flow. We'll explore common JWT security vulnerabilities, such as what happens if a token is stolen, and discuss the best practices to mitigate these risks.