User Authentication with JWT
JSON Web Token (JWT) authentication has become a popular choice for securing web applications due to its simplicity, scalability, and versatility. In this comprehensive guide, let’s dive into the details of JWT authentication, covering its structure, working principles, advantages, and implementation considerations.
1. What is JWT?
Imagine a digital passport for your online identity. That’s essentially what a JWT is. It’s a self-contained, cryptographically signed piece of information passed between parties (usually a client and a server) to verify authorization and grant access to protected resources. This JWT comprises three parts:
1. Header:
The header typically consists of two parts: the type of the token (JWT) and the signing algorithm being used, such as HMAC SHA256 or RSA.
{
"alg": "HS256",
"typ": "JWT"
}
2. Payload:
The second part of the token is the payload, which contains claims. Claims are statements about an entity (typically like user ID, roles, expiration time, etc.)
{
"sub": "1234567890",
"name": "kamal",
"iat": 1516239022
}
3. Signature:
To create the signature part, the encoded header, encoded payload, and a secret key are used. The signature is used to verify that the sender of the JWT is who it says it is and to ensure that the message wasn’t changed along the way.
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
2. How JWT Works:
- Token Creation:
The server creates a JWT by encoding the header and payload, and then signing it with a secret key.
2. Token Transmission:
The JWT can be transmitted between parties (usually in the Authorization
header of an HTTP request) without the need for the parties to share a password.
3. Token Verification:
The recipient (server) of the JWT can verify its authenticity by decoding it and comparing the computed signature against the one included in the JWT. If they match, it indicates that the token is valid.
3. How JWT differ from Traditional Authentication
- Statelessness:
Traditional authentication often involves maintaining a session state on the server. After a user logs in, the server stores information about the user’s session, requiring server-side storage and management. JWT is stateless. The token itself contains all the necessary information about the user, eliminating the need for the server to store session data. This statelessness contributes to scalability and reduces server-side storage requirements
2. Data Format:
Traditional authentication involves the exchange of credentials (username and password) for a session ID or token, usually stored in cookies. JWTs use a compact, URL-safe format to represent claims. They consist of a header, payload, and signature, and they are often transmitted in the Authorization
header of an HTTP request.
3. Scalability:
Session-based authentication can lead to challenges in scalability, especially in systems with a large number of concurrent users. Maintaining session state requires server resources. JWTs more scalable because of the Stateless nature and decentralized verification. Each request contains the necessary information for authentication, reducing the server’s dependency on session management.
4. Token Expiration and Refresh Tokens:
Session-based systems may rely on short-lived session IDs, but refreshing the session often requires additional server requests. JWTs can include an expiration time, and refresh tokens can be used to obtain a new access token without requiring the user to re-enter credentials
5. Decentralized Authentication
Traditional authentication often relies on a centralized server to manage session state and validate user identity. JWTs support decentralized authentication. Different services or microservices can independently verify JWTs without a centralized authentication server.
4. Best Practices for JWT Authentication
When JWT authentication implemented correctly, provides a secure and efficient way to handle user authentication in web applications. However, to ensure the integrity and confidentiality of the authentication process, it’s essential to follow best practices.
1. Use HTTPS
Always use HTTPS to encrypt data in transit. This prevents man-in-the-middle attacks and ensures that the token is transmitted securely between the client and the server.
2. Token Expiration
Set a reasonable expiration time for JWTs to limit their validity. This mitigates the risk of stolen or intercepted tokens being misused for an extended period.
3. Secret Key Security
Keep the secret key used for signing JWTs confidential. Use strong, unique keys, and avoid hardcoding them in your codebase. Consider using environment variables to store sensitive information.
4. Avoid Storing Sensitive Information
Do not include sensitive information in the JWT payload. JWTs are not meant for storing confidential data.
5. Token Refreshing
Consider using refresh tokens for a more secure token rotation mechanism. Refresh tokens can be used to obtain a new access token without requiring the user to re-enter their credentials.
5. Implementing JWT with Nodejs
Lets see how we can implement user authentication with Nodejs
First create a Nodejs backend in your local machine. Here is the command. This will install the basic necessary dependencies for this project.
npm init
npm install express bodyparser cors cookieParser jwt
Then create a index.js file and add following codes.
1. Express Setup
const express = require("express");
const bodyparser = require("body-parser");
const cors = require("cors");
const cookieParser = require("cookie-parser");
const jwt = require("jsonwebtoken");
const app = express();
- This section imports the necessary packages/modules:
express
for creating the server,body-parser
for parsing request bodies,cors
for handling Cross-Origin Resource Sharing,cookie-parser
for parsing cookies, andjsonwebtoken
for working with JSON Web Tokens (JWT). - It initializes an Express application using
express()
.
2. Middleware Setup
app.use(express.json());
app.use(cookieParser());
const PORT = 3000;
app.use(cors());
app.use(bodyparser.json());
3.Login Endpoint
app.post("/login", async (req, res) => {
try {
const { email, password } = req.body;
if (email == "test@gmail.com" && password == "123") {
const accessToken = jwt.sign(email, "key");
res.cookie("token", accessToken);
res.json({ Status: "Success" });
} else {
return res.status(401).send("Invalid credentials");
}
} catch (error) {
res.json({ Status: "Falied to login" });
}
});
- This route handles the POST request to “/login”. It expects an email and password in the request body.
- If the provided credentials match a hardcoded set (
"test@gmail.com"
and"123"
), it generates a JWT usingjwt.sign
and sets it as a cookie named "token". - If the credentials are incorrect, it returns a 401 status with the message “Invalid credentials”.
4. Authentication Middleware
function authenticate(req, res, next) {
try {
const token = req.cookies.token;
const decoded = jwt.verify(token, "key");
next();
} catch (e) {
res.status(401).send({ error: "please authenticate" });
}
}
- This middleware function (
authenticate
) checks if there is a valid JWT in the "token" cookie. If not, it sends a 401 status response with the message "please authenticate". - If the token is valid, it calls the
next()
function, allowing the request to proceed to the next middleware or route.
5. Dashboard Endpoint
app.get("/dashboard", authenticate, async (req, res) => {
res.json({ Status: "Success"});
});
- This route (“/dashboard”) is protected by the
authenticate
middleware. Only requests with a valid JWT in the "token" cookie can access this route. - If the request passes authentication, it responds with a JSON object containing “Success” and some data (
req.name
is expected, but it seems to be missing in the code).
6. Error Handling Middleware
app.use((err, req, res, next) => {
console.log(err);
res.status(err.status || 500).send("Something went wrong!");
});
This is a generic error-handling middleware. If any error occurs in the previous middleware or routes, it logs the error and sends a generic error message with an appropriate status code (defaults to 500 Internal Server Error).
7.Server Listening
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
This starts the server on the specified port (3000
in this case) and logs a message to the console when the server is successfully running.
6. Testing
Here I am using Thunder client extension in Vs code. But you can use postman as well.
First lets send a post request to check the login functionality.
After sending a post request for login, a token will be sent back to the browser. You can view it on the Cookies tab.
Then you can access the route (“/dashboard”) which is protected by the authenticate
middleware. Because now you have a valid token in your client side.
If you delete the token from cookies you can not access the (“/dashboard”) route.
That is how you can authenticate users and protect routes of a web application. Note that in real-world scenario you have to follow the best practices like hashing the passwords when users a registering to your system, pass an object as the payload instead of just the email when creating a token, use environment variables or a secure configuration management system to store sensitive information like jwt secret key.