User Authentication with JWT

Isuru Jayalath
Enlear Academy
Published in
7 min readFeb 5, 2024

--

Photo by FlyD on Unsplash

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:

  1. 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

  1. 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, and jsonwebtoken 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 using jwt.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.

--

--

Undergraduate of University of Moratuwa, Faculty of Information Technology.