JWT Authentication with Spring Boot

JWT Authentication with Role-based access in a Spring Boot

Introduction

This tutorial will guide you through implementing JWT-based authentication in a Spring Boot application with role-based access. We’ll also integrate a React frontend that allows users to log in and redirects them based on their roles (Admin or User).


JWT Authentication Flow

The flow of JWT-based authentication involves several steps for secure communication between the user, frontend, and backend. Below is a visual representation of the JWT authentication flow:

The flowchart demonstrates how a user logs in, receives a JWT token, stores it in local storage, and sends it with each authenticated request. The backend verifies the token and either grants access or responds with a 401 Unauthorized status if the token is invalid.

Backend: Spring Boot Setup

Start by creating a Spring Boot application with the following dependencies:

  • Spring Web for creating REST APIs
  • Spring Security for handling security and authentication
  • Spring Data JPA for interacting with the database
  • Lombok (optional) for reducing boilerplate code


1. Set Up `pom.xml` (Maven Dependencies)

Include the following dependencies in your `pom.xml` file:


<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.11.5</version>
    </dependency>
</dependencies>
                

The above configuration includes essential dependencies for JWT authentication, Spring Security, and data handling with Spring Data JPA.


2. Configure `application.properties`

The application’s configuration file will contain JWT secret key and expiration time settings:

# Database Configureation
# JWT Secret key and token expiration time
jwt.secret=your-secret-key
jwt.expiration=1800000  # 30 minutes in milliseconds
                

Note: Ensure the jwt.secret is securely stored and not exposed in public repositories. This secret is used for signing JWT tokens, and it should be kept safe.

# JWT Authentication Flow Structure

The JWT authentication process is structured as follows:

+-----------+               +----------+
|   User    |               |  Backend |
+-----------+               +----------+
     |                           |
     |---(Send Login Request)---->|
     |                           |
     |<--(Send JWT Token)--------|
     |  (Validate Credentials)   |
     |                           |
     v                           v
+----------+                   +--------------+
| Frontend |                   | Backend      |
+----------+                   +--------------+
     |                               |
     |--(Token Stored in localStorage)-->| 
     |<--(JWT in Authorization Header)---|
     |    (Send Authenticated Request)   |
     |                                   |
     |<--(Verify Token)------------------|
     |    [Valid Token]                  |
     |<--(Authorized Response)-----------|
     |                                   |
     v                                   v
+--------------+                  +-------------+
| Backend      |                  | Backend     |
+--------------+                  +-------------+
     |                                   |
     |<--(Verify Token)------------------|
     |    [Invalid Token]                |
     |<--(401 Unauthorized)--------------|
            

JWT Token process in Hindi...........

Yeh diagram ek basic login aur authentication process ko dikhata hai jo backend aur frontend ke beech hota hai, aur kaise JWT (JSON Web Token) use hota hai:

  1. User -> Backend (Login Request):

    • User frontend (jaise website ya app) pe login details submit karta hai.
    • Frontend yeh details backend ko bhejta hai.
  2. Backend -> User (JWT Token):

    • Backend, user ke credentials validate karta hai.
    • Agar credentials sahi hain, toh backend ek JWT token generate karta hai aur frontend ko bhejta hai.
  3. Frontend -> Backend (Token Stored in localStorage):

    • Frontend apne side par JWT token ko localStorage mein store karta hai, taaki future requests ke liye use ho sake.
  4. Frontend -> Backend (Authenticated Request with JWT):

    • Jab user koi request bhejta hai (jaise data ya service request), frontend is token ko request ke authorization header mein bhejta hai.
  5. Backend -> Backend (Verify Token):

    • Backend yeh token verify karta hai ki yeh valid hai ya nahi. Agar token valid hai, toh backend response bhejta hai.
  6. Backend -> User (Authorized Response):

    • Agar token valid hota hai, toh backend user ko authorized response bhejta hai, jaise ki requested data.
  7. Backend -> Backend (Invalid Token):

    • Agar token invalid hota hai (for example, expired ya tampered), toh backend request reject kar deta hai.
  8. Backend -> User (401 Unauthorized):

    • Jab token invalid hota hai, backend ek 401 Unauthorized error bhejta hai, jiska matlab hai user ko access dena mana hai.

3. User and Role Entities

Now, let’s define the entities for User and Role.

// User Entity

If Lombok is not installed in your Eclipse project 
and you're manually writing getter and setter methods 
instead of using Lombok annotations (@Getter and @Setter), 
you can manually implement


package com.example.security.entity;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@Entity
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Table(name = "User")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;
    private String email;
    private String password;

    @ManyToOne
    @JoinColumn(name = "role_id")
    private Role role; // A user has a specific role (Admin/User)
}
                

// Role Entity
package com.example.security.entity;

import jakarta.persistence.*;

@Entity
public class Role {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name; // Role name (Admin or User)

    public Role() {}

    public Role(String name) {
        this.name = name;
    }

    // Getters and Setters
}
                

The User entity stores information about the user (username, email, password) and links

to the Role entity. The Role entity contains a role name (e.g., Admin or User).

4. UserDetailsService Configuration

We need to configure the UserDetailsService to load user-specific data from the database and handle password encoding:


// UserDetailsService Configuration
package com.example.security.config;

import com.example.security.entity.User;
import com.example.security.exception.UserNotFoundException;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class MyConfig {

    private final UserRepository userRepository;

    public MyConfig(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Bean
    public UserDetailsService userDetailsService() {
        return username -> userRepository.findByEmail(username).map(user -> {
            return User.builder()
                    .username(user.getEmail())
                    .password(user.getPassword())
                    .roles(user.getRole().getName())
                    .disabled(!user.isActive())
                    .build(); // Mapping user data to Spring Security's User object
        }).orElseThrow(() -> new UserNotFoundException("User not found with username: " + username));
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(); // Encrypting passwords using BCrypt
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }
}
                

Note: The passwordEncoder bean uses BCrypt to securely encode passwords. Additionally, the UserDetailsService is responsible for loading user information from the database.


5. Security Configuration

Next, we configure Spring Security to authenticate requests and define a JWT filter for validating tokens:


// Security Configuration
package com.example.security.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import com.example.security.filter.JwtAuthenticationFilter;

@Configuration
public class SecurityConfig {

    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/auth/login", "/api/public").permitAll() // Allow unauthenticated access to specific endpoints
                .anyRequest().authenticated() // Protect all other endpoints
                .and()
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); // Add the JWT filter
        return http.build();
    }
}
                

In the above configuration, we disable CSRF protection (for stateless authentication) and allow unauthenticated access to certain endpoints like login. The JWT filter is added before the default authentication filter to check the presence and validity of the JWT token.

Note: This setup covers the JWT authentication flow. You can extend it with role-based access and additional features as needed.

Key Points Explained

pom.xml Dependencies: The dependencies for Spring Boot, Spring Security, JPA, and JWT are necessary for authentication, data handling, and token management.

application.properties Configuration: You configure the secret key for JWT and token expiration time. These values should be kept secret and secured.

Entities (User and Role): User stores user data, and Role defines the user’s role, such as Admin or User. Each user is associated with one role.

UserDetailsService Configuration: This service loads user data for Spring Security, maps it into Spring Security’s User object, and handles password encoding (with BCrypt for security).

Security Configuration: The SecurityConfig class defines how to authenticate requests, handle JWTs, and secure APIs. It ensures that JWT tokens are validated before granting access to protected routes.

JWT Token Generation and Validation: The JWT token is generated in the JwtHelper class, and it contains the user’s credentials and role data, signed securely to ensure data integrity and authenticity.

Comments

Popular posts from this blog

RAZORPAY CODE BACKEND

RAZORPAY FRONTEND CODE