DEV Community

Cover image for Building a Role-Based Access Control System with JWT in Spring Boot
Aman Gupta
Aman Gupta

Posted on

Building a Role-Based Access Control System with JWT in Spring Boot

Introduction

Welcome to my blog, where we'll embark on an exciting journey into the realm of web application security! If you're new to the world of Spring Boot or just beginning to explore the intricacies of authentication and authorization, you've come to the right place. In this inaugural article, I'm thrilled to share my take on a topic that resonates with developers of all levels: Building a Role-Based Access Control System with JWT in Spring Boot.

In this blog series, we'll dive deep into the world of Spring Security and explore how JSON Web Tokens (JWT) can amplify its capabilities. I'll guide you step by step, from laying the foundation with a solid Spring Boot setup, all the way to implementing role-based access control using JWT tokens.

In this blog, we'll cover the following topics:

  • Creating Different Roles and Assigning it to Users
  • Registering Users and Administrators
  • Generating JWT Tokens and Authentication
  • Performing Role-Based Actions

Prerequisites
Before we dive into the exciting world of building a role-based access control system with JWT in Spring Boot, let's ensure that you have the necessary tools and environment ready. Here's what you'll need to get started:

Java Development Kit (JDK): Make sure you have a compatible version of JDK installed on your machine. Spring Boot usually works well with JDK 8 or higher.

Integrated Development Environment (IDE): Choose an IDE that suits your preferences. IntelliJ IDEA and Eclipse are popular choices for Spring Boot development.

Maven or Gradle: You'll need either Maven or Gradle as a build tool to manage your project dependencies.

Setting up the Environment:
Spring Boot Project Initialization: Create a new Spring Boot project using either Spring Initializr web tool here or your IDE's project creation wizard.

Here is my setup you can follow:
Project: Maven
Spring Boot Version:2.7.3 (if not available then use 3.1.3 then change it later in pom.xml)
Java Version:17
Dependencies:
1.Spring Data JPA
2.Spring web
3.Spring Security
4.MySQL Driver
5.JSON WEB TOKEN

You will not find JSON web Token there you have to add it manually

<dependency>
     <groupId>io.jsonwebtoken</groupId>
     <artifactId>jjwt</artifactId>
     <version>0.9.1</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode

Here is full pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.alpha</groupId>
    <artifactId>alpha</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>alpha</name>
    <description>Demo project for Spring Boot Role based Authentication using JWT </description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</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-web</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
Enter fullscreen mode Exit fullscreen mode

application.properties

jwt.token.validity=18000
jwt.signing.key=YourSignInKey
jwt.authorities.key=roles
jwt.token.prefix=Bearer
jwt.header.string=Authorization

spring.datasource.url=jdbc:mysql://localhost:3306/yourdb
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
spring.user.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
server.port=8080
Enter fullscreen mode Exit fullscreen mode

This is a configuration file for a Spring Boot application.

  • The first line sets the validity duration for JSON Web Tokens (JWT) to 18000 seconds (5 hours).
  • The second line specifies the signing key to be used for generating and validating JWTs.
  • The third line defines the key for extracting authorities/roles from a JWT.
  • The fourth line sets the prefix for JWT tokens to "Bearer".
  • The fifth line specifies the header string to be used for JWT tokens in HTTP requests.
  • The next few lines configure the MySQL database connection for the application, including the URL, username, and password.
  • The line spring.jpa.show-sql=true enables the display of SQL statements executed by Hibernate.
  • The line spring.jpa.hibernate.ddl-auto=update configures Hibernate to automatically update the database schema based on the entity classes.
  • The line spring.user.datasource.driver-class-name=com.mysql.jdbc.Driver specifies the driver class for the user datasource.
  • The line spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect sets the Hibernate dialect for MySQL.
  • The last line sets the server port to 8080.

"As you can see, here is the clear folder structure."

src
└── main
    ├── java
    │   └── com
    │       └── alpha
    │           ├── config (Package)
    │           │   ├── JwtAuthenticationFilter.java
    │           │   ├── PasswordEncoder.java
    │           │   ├── TokenProvider.java
    │           │   ├── UnauthorizedEntryPoint.java
    │           │   └── WebSecurityConfig.java
    │           ├── controller (Package)
    │           │   └── UserController.java
    │           ├── dao (Package)
    │           │   ├── RoleDao.java
    │           │   └── UserDao.java
    │           ├── model (Package)
    │           │   ├── AuthToken.java
    │           │   ├── LoginUser.java
    │           │   ├── Role.java
    │           │   ├── User.java
    │           │   └── UserDto.java
    │           ├── service (Package)
    │           │   ├── impl
    │           │   │   ├── RoleServiceImpl.java
    │           │   │   └── UserServiceImpl.java
    │           │   ├── RoleService.java
    │           │   └── UserService.java
    │           └── AlphaApplication.java
    └── resources
        └── application.properties

Enter fullscreen mode Exit fullscreen mode

Let's get started!

Now we will start with understanding the config package

Spring Security is a powerful and highly customizable security framework provided by the Spring Framework for Java applications. Its primary purpose is to handle authentication, authorization, and various security aspects in web and enterprise applications. Spring Security is often used to secure web applications, RESTful APIs, and other components of a software system.

Authentication:
Authentication is the process of confirming the identity of a person or entity. It ensures that the person or entity is who they claim to be before granting access to something. It's like checking someone's ID before allowing them to enter a secure area.
Imagine a professional cricket match. Before a player can step onto the field, they must prove their identity. They do this by showing their official player card with their name, photo, and a unique ID number. The match officials, like the umpires and team captains, examine the card to ensure it matches the player's appearance and is on the list of authorized players. Once confirmed, the player is authenticated and allowed to participate in the game.

Authorization:
Authorization comes after authentication and determines what actions or resources an authenticated person or entity is allowed to access or perform. It specifies the level of access and control based on roles, permissions, or rules.

Once a cricket player is authenticated and on the field, authorization kicks in. Each player has a specific role (e.g., batsman, bowler, fielder) with associated actions they can perform. For example, a bowler is authorized to bowl, a batsman is authorized to bat, and a wicketkeeper is authorized to keep wickets. The coach or captain may have special authorization to make strategic decisions during the match, like changing the batting order or field placements.

lets move on to our config files one by one

CORSFilter :
CORSFilter class is responsible for handling CORS-related settings in a web application. It intercepts incoming HTTP requests, adds the necessary CORS headers to the response, and then allows the request to continue processing using chain.doFilter(req, res). This filter helps control and secure how resources on the server are accessed by different origins in a web application.

package com.alpha.config;

import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


public class CORSFilter implements Filter {

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) res;
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers", "X-Requested-With, Content-Type, Authorization, Origin, Accept, Access-Control-Request-Method, Access-Control-Request-Headers");
        chain.doFilter(req, res);
    }

    public void init(FilterConfig filterConfig) {}

    public void destroy() {}

}
Enter fullscreen mode Exit fullscreen mode

This class implements the Filter interface, which is part of the Java Servlet API. Filters are used to perform actions on HTTP requests and responses as they pass through the application.

  • doFilter method :- This method is required when implementing the Filter interface. It is called for each incoming HTTP request. Inside this method, the code is responsible for adding necessary CORS headers to the HTTP response. CORS headers are used to control and define the policy for cross-origin requests. They specify who can access the resources of a web page and what operations are permitted from different origins. The code in this method sets various CORS headers, such as Access-Control-Allow-Origin, Access-Control-Allow-Methods, and others. These headers dictate which domains are allowed to access the resources, which HTTP methods are permitted, and other CORS-related policies.

The init method is used for initialization tasks that the filter may need when it's first created.

The destroy method is called when the filter is being removed or shut down.

WebSecurityConfig
This class is responsible for configuring security settings, such as authentication, authorization, and request filtering, in a Spring Boot web application. It also integrates custom components, like the JwtAuthenticationFilter, to handle specific security
requirements. This configuration is a common setup for securing RESTful APIs or web applications using Spring Security.

package com.alpha.config;

import org.springframework.beans.factory.annotation.Autowired;
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.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.annotation.Resource;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource(name = "userService")
    private UserDetailsService userDetailsService;

    @Autowired
    private PasswordEncoder encoder;

    @Autowired
    private UnauthorizedEntryPoint unauthorizedEntryPoint;

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(encoder.encoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable()
                .authorizeRequests()
                .antMatchers("/users/authenticate", "/users/register").permitAll()
                .anyRequest().authenticated()
                .and()
                .exceptionHandling().authenticationEntryPoint(unauthorizedEntryPoint).and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        http.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public JwtAuthenticationFilter authenticationTokenFilterBean() throws Exception {
        return new JwtAuthenticationFilter();
    }
}
Enter fullscreen mode Exit fullscreen mode

@Configuration: Indicates that this class contains configuration settings for the application.
@EnableWebSecurity: Enables Spring Security features for the web application.
@EnableGlobalMethodSecurity(prePostEnabled = true): Enables method-level security annotations such as @PreAuthorize and @PostAuthorize.

@Resource: Injects the UserDetailsService bean, likely responsible for user-related operations.
@Autowired: Injects instances of PasswordEncoder and UnauthorizedEntryPoint beans, which are used for password hashing and handling unauthorized access, respectively.

configure(AuthenticationManagerBuilder auth) Method:

  • This method configures the authentication manager.
  • It specifies that the userDetailsService bean should be used for user authentication and sets the password encoder.

configure(HttpSecurity http) Method:

  • This method configures the HTTP security settings.
  • It includes settings for CORS (Cross-Origin Resource Sharing), CSRF (Cross-Site Request Forgery), and URL permissions.
  • It specifies which URLs are accessible without authentication ("/users/authenticate" and "/users/register") and requires authentication for all other requests.
  • It sets an authentication entry point for handling unauthorized access and defines the session management policy as STATELESS.
  • The addFilterBefore method adds a custom JwtAuthenticationFilter before the UsernamePasswordAuthenticationFilter to handle JWT (JSON Web Token) authentication.

authenticationManagerBean() Method:

  • This method declares an AuthenticationManager bean, which is used for user authentication.

JwtAuthenticationFilter Bean:

  • This method declares a bean for the JwtAuthenticationFilter, which is a custom filter used for JWT-based authentication.

TokenProvider
This class is responsible for handling JWTs in a Spring Boot application's security flow. It can generate tokens, extract user information from tokens, validate tokens, and create authentication tokens for users based on the information stored in the JWTs.

package com.alpha.config;

import io.jsonwebtoken.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.io.Serializable;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.function.Function;
import java.util.stream.Collectors;

@Component
public class TokenProvider implements Serializable {

    @Value("${jwt.token.validity}")
    public long TOKEN_VALIDITY;

    @Value("${jwt.signing.key}")
    public String SIGNING_KEY;

    @Value("${jwt.authorities.key}")
    public String AUTHORITIES_KEY;

    public String getUsernameFromToken(String token) {
        return getClaimFromToken(token, Claims::getSubject);
    }

    public Date getExpirationDateFromToken(String token) {
        return getClaimFromToken(token, Claims::getExpiration);
    }

    public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = getAllClaimsFromToken(token);
        return claimsResolver.apply(claims);
    }

    private Claims getAllClaimsFromToken(String token) {
        return Jwts.parser()
                .setSigningKey(SIGNING_KEY)
                .parseClaimsJws(token)
                .getBody();
    }

    private Boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }

    public String generateToken(Authentication authentication) {
         String authorities = authentication.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.joining(","));

        return Jwts.builder()
                .setSubject(authentication.getName())
                .claim(AUTHORITIES_KEY, authorities)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + TOKEN_VALIDITY*1000))
                .signWith(SignatureAlgorithm.HS256, SIGNING_KEY)
                .compact();
    }

    public Boolean validateToken(String token, UserDetails userDetails) {
        final String username = getUsernameFromToken(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }

    UsernamePasswordAuthenticationToken getAuthenticationToken(final String token, final Authentication existingAuth, final UserDetails userDetails) {

        final JwtParser jwtParser = Jwts.parser().setSigningKey(SIGNING_KEY);

        final Jws<Claims> claimsJws = jwtParser.parseClaimsJws(token);

        final Claims claims = claimsJws.getBody();

        final Collection<? extends GrantedAuthority> authorities =
                Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
                        .map(SimpleGrantedAuthority::new)
                        .collect(Collectors.toList());

        return new UsernamePasswordAuthenticationToken(userDetails, "", authorities);
    }

}
Enter fullscreen mode Exit fullscreen mode

@Component: Marks this class as a Spring component, allowing it to be automatically scanned and registered as a bean in the Spring application context.

  • getUsernameFromToken(String token): This method extracts the username (subject) from a JWT token.
  • getExpirationDateFromToken(String token): Retrieves the expiration date from a JWT token.
  • getClaimFromToken(String token, Function claimsResolver): A generic method to extract claims from a JWT token.
  • getAllClaimsFromToken(String token): Parses and retrieves all claims (payload) from a JWT token.
  • isTokenExpired(String token): Checks whether a JWT token has expired based on its expiration date.
  • generateToken(Authentication authentication): Generates a new JWT token based on the provided Authentication object. It includes the subject (username), authorities, issuance time, and expiration time.
  • validateToken(String token, UserDetails userDetails): Validates a JWT token against the provided UserDetails. It checks if the token's subject matches the user's username and if the token is not expired.
  • getAuthenticationToken(final String token, final Authentication existingAuth, final UserDetails userDetails): This method parses a JWT token to create an Authentication object containing the user's authorities. It's used for authenticating users based on JWT tokens.

JwtAuthenticationFilter
JwtAuthenticationFilter is responsible for intercepting incoming requests, extracting JWTs from request headers, and authenticating users based on the tokens. It ensures that authenticated users have their security context set, allowing them to access protected resources within the application.

package com.alpha.config;

import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.SignatureException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Value("${jwt.header.string}")
    public String HEADER_STRING;

    @Value("${jwt.token.prefix}")
    public String TOKEN_PREFIX;

    @Resource(name = "userService")
    private UserDetailsService userDetailsService;

    @Autowired
    private TokenProvider jwtTokenUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
        String header = req.getHeader(HEADER_STRING);
        String username = null;
        String authToken = null;

        if (header != null && header.startsWith(TOKEN_PREFIX)) {
            authToken = header.replace(TOKEN_PREFIX, "");

            try {
                username = jwtTokenUtil.getUsernameFromToken(authToken);
            } catch (IllegalArgumentException e) {
                logger.error("Error occurred while retrieving Username from Token", e);
            } catch (ExpiredJwtException e) {
                logger.warn("The token has expired", e);
            } catch (SignatureException e) {
                logger.error("Authentication Failed. Invalid username or password.");
            }
        } else {
            logger.warn("Bearer string not found, ignoring the header");
        }

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);

            if (jwtTokenUtil.validateToken(authToken, userDetails)) {
                UsernamePasswordAuthenticationToken authentication = jwtTokenUtil.getAuthenticationToken(authToken, SecurityContextHolder.getContext().getAuthentication(), userDetails);
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(req));
                logger.info("User authenticated: " + username + ", setting security context");
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }

        chain.doFilter(req, res);
    }
}
Enter fullscreen mode Exit fullscreen mode
  • This class extends OncePerRequestFilter, which ensures that this filter is applied only once per request.
  • doFilterInternal Method:This method is the core of the filter and is called for each incoming HTTP request.
  • The method first checks if a JWT is present in the request header and if it starts with the defined token prefix ("Bearer ").
  • If a valid token is found, it attempts to extract the username from the token using the TokenProvider class.
  • It catches exceptions for various token-related errors, such as token expiration (ExpiredJwtException) and invalid signatures (SignatureException), and logs them.
  • If a valid username is obtained from the token and there is no existing authentication context, it loads the user details from the UserDetailsService based on the username.
  • It then validates the token against the user details using the TokenProvider. If the token is valid, it creates an UsernamePasswordAuthenticationToken containing the user details and sets the authentication details.
  • Finally, it sets the authenticated user's security context using SecurityContextHolder.
  • After handling authentication, the filter continues the request processing by invoking chain.doFilter(req, res).

BCryptPasswordEncoder
This bean can be used throughout the application for securely hashing and verifying passwords, especially in the context of user authentication and security. It's a common practice to configure and manage password encoding components like this in Spring applications to enhance security.

package com.alpha.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
public class PasswordEncoder {

    @Bean
    public BCryptPasswordEncoder encoder(){
        return new BCryptPasswordEncoder();
    }
}
Enter fullscreen mode Exit fullscreen mode
  • BCryptPasswordEncoder is a popular password hashing library in the Spring Security framework. It's used to securely hash and verify passwords.
  • In this configuration, the BCryptPasswordEncoder bean is created and returned by the encoder() method. This bean can then be injected into other parts of the application, such as Spring Security configurations, to handle password encoding and decoding.

UnauthorizedEntryPoint
UnauthorizedEntryPoint class is responsible for handling unauthorized access to protected resources in a Spring Security-enabled application. When an unauthenticated user attempts to access a protected resource, this class sends an HTTP response with a status code of 401, indicating that the request lacks valid authentication. This response informs the client that they need to provide proper authentication credentials to access the resource.

package com.alpha.config;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Serializable;


@Component
public class UnauthorizedEntryPoint implements AuthenticationEntryPoint, Serializable {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthenticated");
    }

}
Enter fullscreen mode Exit fullscreen mode

This class implements the AuthenticationEntryPoint interface, which is part of Spring Security. The AuthenticationEntryPoint interface is responsible for handling authentication-related exceptions, particularly unauthorized access.

  • The commence method is the main method of this class, and it's called when an authentication exception occurs during an HTTP request.
  • It takes three parameters:

    1.HttpServletRequest request: Represents the incoming HTTP
    request.
    2.HttpServletResponse response: Represents the HTTP response
    that will be sent back to the client.
    3.AuthenticationException authException: Represents the
    authentication exception that occurred, typically due to
    unauthorized access.

  • In this method, it sends an HTTP response with a status code of 401 Unauthorized and a message of "Unauthenticated." This is a standard response for indicating that the request lacks valid authentication credentials or authorization.

okay lets start with our model classes

User
The User class represents a user entity with attributes like username, password, email, phone, name, and roles. It also defines a many-to-many relationship with the Role entity, allowing users to have multiple roles.

package com.alpha.model;

import com.fasterxml.jackson.annotation.JsonIgnore;

import javax.persistence.*;
import java.util.Set;

@Entity
public class User {

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

    @Column
    private String username;

    @Column
    @JsonIgnore
    private String password;

    @Column
    private String email;

    @Column
    private String phone;

    @Column
    private String name;

    @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    @JoinTable(name = "USER_ROLES",
            joinColumns = {
            @JoinColumn(name = "USER_ID")
            },
            inverseJoinColumns = {
            @JoinColumn(name = "ROLE_ID") })
    private Set<Role> roles;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Set<Role> getRoles() {
        return roles;
    }

    public void setRoles(Set<Role> roles) {
        this.roles = roles;
    }
}
Enter fullscreen mode Exit fullscreen mode
  • This defines a many-to-many relationship between the User entity and the Role entity. Users can have multiple roles, and roles can be associated with multiple users.
  • The @ManyToMany annotation indicates a many-to-many relationship.
  • The fetch = FetchType.EAGER attribute specifies that roles should be eagerly fetched when loading a user.
  • The cascade = CascadeType.ALL attribute specifies that if operations like persist, merge, remove, etc., are performed on a User entity, the same operations should be cascaded to its associated Role entities.
  • The @JoinTable annotation is used to define the name of the join table that holds the relationship between users and roles. It specifies the columns used for joining the tables.

Role
the Role class represents a user role entity with attributes like name and description. It is designed to be persisted in a database table and can be associated with users through a many-to-many relationship, as indicated by the User entity's relationship mapping that references Role.

package com.alpha.model;

import javax.persistence.*;

@Entity
public class Role {

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

    @Column
    private String name;

    @Column
    private String description;

    // Getter for id
    public long getId() {
        return id;
    }

    // Setter for id
    public void setId(long id) {
        this.id = id;
    }

    // Getter for name
    public String getName() {
        return name;
    }

    // Setter for name
    public void setName(String name) {
        this.name = name;
    }

    // Getter for description
    public String getDescription() {
        return description;
    }

    // Setter for description
    public void setDescription(String description) {
        this.description = description;
    }
}
Enter fullscreen mode Exit fullscreen mode

the Role class represents a user role entity with attributes like name and description. It is designed to be persisted in a database table and can be associated with users through a many-to-many relationship, as indicated by the User entity's relationship mapping that references Role.

AuthToken

package com.alpha.model;

/**
 * Represents an authentication token.
 */
public class AuthToken {
    private String token;

    /**
     * Constructs a new AuthToken object.
     */
    public AuthToken() {
    }

    /**
     * Constructs a new AuthToken object with the specified token.
     * 
     * @param token the authentication token
     */
    public AuthToken(String token) {
        this.token = token;
    }

    /**
     * Returns the authentication token.
     * 
     * @return the authentication token
     */
    public String getToken() {
        return token;
    }

    /**
     * Sets the authentication token.
     * 
     * @param token the authentication token to be set
     */
    public void setToken(String token) {
        this.token = token;
    }
}
Enter fullscreen mode Exit fullscreen mode

LoginUser

package com.alpha.model;

public class LoginUser {
    private String username;
    private String password;

    // Getters and Setters for username
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }

    // Getters and Setters for password
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
}
Enter fullscreen mode Exit fullscreen mode

UserDto

package com.alpha.model;

public class UserDto {

    private String username;
    private String password;
    private String email;
    private String phone;
    private String name;


    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public User getUserFromDto(){
        User user = new User();
        user.setUsername(username);
        user.setPassword(password);
        user.setEmail(email);
        user.setPhone(phone);
        user.setName(name);

        return user;
    }

}
Enter fullscreen mode Exit fullscreen mode

DAO is a Spring Data JPA repository interface typically used for performing CRUD (Create, Read, Update, Delete) operations on the entity class. Let's break down its key components:

UserDao

package com.alpha.dao;

import com.alpha.model.User;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserDao extends CrudRepository<User, Long> {
    User findByUsername(String username);
}
Enter fullscreen mode Exit fullscreen mode

RoleDao

package com.alpha.dao;

import com.alpha.model.Role;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface RoleDao extends CrudRepository<Role, Long> {
    Role findRoleByName(String name);
}
Enter fullscreen mode Exit fullscreen mode

You can use JPARepository also.

Service Layer

We will now proceed to define service interfaces for our User and Role services. These service interfaces will serve as blueprints for the actual service implementations and will encapsulate the core business logic.

As a best practice, using interfaces for service definitions promotes separation of concerns and allows for easy switching of implementations, such as when using mocking frameworks for testing.
RoleService

package com.alpha.service;

// Importing the Role model
import com.alpha.model.Role;

// Declaring the RoleService interface
public interface RoleService {
    // Method to find a Role by its name
    Role findByName(String name);
}

Enter fullscreen mode Exit fullscreen mode

UserService

package com.alpha.service;

import com.alpha.model.User;
import com.alpha.model.UserDto;

import java.util.List;

public interface UserService {

    // Saves a user
    User save(UserDto user);

    // Retrieves all users
    List<User> findAll();

    // Retrieves a user by username
    User findOne(String username);

    User createEmployee(UserDto user);

}
Enter fullscreen mode Exit fullscreen mode

Now we will implement our service logic
RoleServiceImpl

package com.alpha.service.impl;

import com.alpha.dao.RoleDao;
import com.alpha.model.Role;
import com.alpha.service.RoleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service(value = "roleService")
public class RoleServiceImpl implements RoleService {

    @Autowired
    private RoleDao roleDao;

    @Override
    public Role findByName(String name) {
        // Find role by name using the roleDao
        Role role = roleDao.findRoleByName(name);
        return role;
    }
}

Enter fullscreen mode Exit fullscreen mode

UserServiceImpl

package com.alpha.service.impl;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.alpha.dao.UserDao;
import com.alpha.model.Role;
import com.alpha.model.User;
import com.alpha.model.UserDto;
import com.alpha.service.RoleService;
import com.alpha.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

@Service(value = "userService")
public class UserServiceImpl implements UserDetailsService, UserService {

    @Autowired
    private RoleService roleService;

    @Autowired
    private UserDao userDao;

    @Autowired
    private BCryptPasswordEncoder bcryptEncoder;

    // Load user by username
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userDao.findByUsername(username);
        if(user == null){
            throw new UsernameNotFoundException("Invalid username or password.");
        }
        return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), getAuthority(user));
    }

    // Get user authorities
    private Set<SimpleGrantedAuthority> getAuthority(User user) {
        Set<SimpleGrantedAuthority> authorities = new HashSet<>();
        user.getRoles().forEach(role -> {
            authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName()));
        });
        return authorities;
    }

    // Find all users
    public List<User> findAll() {
        List<User> list = new ArrayList<>();
        userDao.findAll().iterator().forEachRemaining(list::add);
        return list;
    }

    // Find user by username
    @Override
    public User findOne(String username) {
        return userDao.findByUsername(username);
    }

    // Save user
    @Override
    public User save(UserDto user) {

        User nUser = user.getUserFromDto();
        nUser.setPassword(bcryptEncoder.encode(user.getPassword()));

        // Set default role as USER
        Role role = roleService.findByName("USER");
        Set<Role> roleSet = new HashSet<>();
        roleSet.add(role);

        // If email domain is admin.edu, add ADMIN role
        if(nUser.getEmail().split("@")[1].equals("admin.edu")){
            role = roleService.findByName("ADMIN");
            roleSet.add(role);
        }

        nUser.setRoles(roleSet);
        return userDao.save(nUser);
    }

    @Override
    public User createEmployee(UserDto user) {
        User nUser = user.getUserFromDto();
        nUser.setPassword(bcryptEncoder.encode(user.getPassword()));

        Role employeeRole = roleService.findByName("EMPLOYEE");
        Role customerRole = roleService.findByName("USER");

        Set<Role> roleSet = new HashSet<>();
        if (employeeRole != null) {
            roleSet.add(employeeRole);
        }
        if (customerRole != null) {
            roleSet.add(customerRole);
        }

        nUser.setRoles(roleSet);
        return userDao.save(nUser);
    }
}
Enter fullscreen mode Exit fullscreen mode

lets create our controller class

The initial step for a user is to complete the registration process. At a minimum, users are required to provide a username and password. By invoking the service method to save the user, this essential step is completed.

To access the application's APIs securely, users must include a server-generated JWT (JSON Web Token). All the necessary groundwork for this has been laid out in our TokenProvider. We utilize the generateToken method and include the resulting token in the response, ensuring secure access to the APIs.
UserController
UserController class handles user-related HTTP requests, including registration and authentication. It also demonstrates role-based access control for specific resources. The actual business logic for user operations is delegated to the UserService and TokenProvider components.

package com.alpha.controller;

import com.alpha.config.TokenProvider;
import com.alpha.model.AuthToken;
import com.alpha.model.LoginUser;
import com.alpha.model.User;
import com.alpha.model.UserDto;
import com.alpha.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/users")
public class UserController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private TokenProvider jwtTokenUtil;

    @Autowired
    private UserService userService;

    /**
     * Generates a token for the given user credentials.
     *
     * @param loginUser The user's login credentials.
     * @return A response entity containing the generated token.
     * @throws AuthenticationException if authentication fails.
     */
    @RequestMapping(value = "/authenticate", method = RequestMethod.POST)
    public ResponseEntity<?> generateToken(@RequestBody LoginUser loginUser) throws AuthenticationException {
        final Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                        loginUser.getUsername(),
                        loginUser.getPassword()
                )
        );
        SecurityContextHolder.getContext().setAuthentication(authentication);
        final String token = jwtTokenUtil.generateToken(authentication);
        return ResponseEntity.ok(new AuthToken(token));
    }

    /**
     * Saves a new user.
     *
     * @param user The user to be saved.
     * @return The saved user.
     */
    @RequestMapping(value="/register", method = RequestMethod.POST)
    public User saveUser(@RequestBody UserDto user){
        return userService.save(user);
    }

    /**
     * Returns a message that can only be accessed by users with the 'ADMIN' role.
     *
     * @return A message that can only be accessed by admins.
     */
    @PreAuthorize("hasRole('ADMIN')")
    @RequestMapping(value="/adminping", method = RequestMethod.GET)
    public String adminPing(){
        return "Only Admins Can Read This";
    }

    /**
     * Returns a message that can be accessed by any user.
     *
     * @return A message that can be accessed by any user.
     */
    @PreAuthorize("hasRole('USER')")
    @RequestMapping(value="/userping", method = RequestMethod.GET)
    public String userPing(){
        return "Any User Can Read This";
    }

    @PreAuthorize("hasRole('ADMIN')")
    @RequestMapping(value="/create/employee", method = RequestMethod.POST)
    public User createEmployee(@RequestBody UserDto user){
        return userService.createEmployee(user);
    }

    @PreAuthorize("hasRole('ADMIN')")
    @RequestMapping(value="/find/all", method = RequestMethod.GET)
    public List<User> getAllList(){
        return userService.findAll();
    }

    @PreAuthorize("hasRole('ADMIN')")
    @RequestMapping(value="/find/by/username", method = RequestMethod.GET)
    public User getAllList(@RequestParam String username){
        return userService.findOne(username);
    }
}
Enter fullscreen mode Exit fullscreen mode

These methods demonstrate role-based access control using Spring Security's @PreAuthorize annotation. adminPing can be accessed only by users with the 'ADMIN' role, while userPing can be accessed by users with the 'USER' role.

Before testing your apis you need to add some roles into your db

INSERT INTO role (id, description, name) VALUES (1, 'Admin role', 'ADMIN');
INSERT INTO role (id, description, name) VALUES (2, 'Employee role', 'EMPLOYEE');
INSERT INTO role (id, description, name) VALUES (3, 'User role', 'USER');
Enter fullscreen mode Exit fullscreen mode

Finally you can test your apis in postman
I have created collection for postman

Here is the whole code is in my github

If you find any doubt feel free to contact me on Instagram
Thanks for reading !
Happy Coding !

Top comments (0)