Hello everyone,

Greetings today!

Today we will learn how to secure REST API with JWT Authentication in Spring Boot, so let's get started

  • What is JWT 
    • JWT stands for JSON Web Token which is used to securely share information/claims between client and server for authorization purposes.
  • Structure of JWT
    • JWT consists of three parts separated by dots.
      • Header
      • Payload
      • Signature
    • JWT Header - Consists of information about the type of token & signing algorithm.
    • JWT Payload- It consists of a set of claims such as issuer, expiration, subject, and role. The payload is Base64 encoded and forms the second part of the token
    • JWT Signature - To create a signature, we need to take the encoded header + encoded payload + secret and sign it using the algorithm specified in the header. Signatures are used to verify all changes in the token.
  • Example of JWT Token 
    • Below is an example JWT token that can be generated at the end of this example. You can access the JWT IO to decode the token below and understand the structure above.
    • eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZXN0QHlvcG1haWwuY29tIiwiaXNzIjoiZXhhbXBsZS5pbyIsIlJPTEUiOiJTVFVERU5UIiwiU1VCSkVDVF9JRCI6MywiaWF0IjoxNjU3Mzg0MTgxLCJleHAiOjE2NTc5ODg5ODF9.wMCQyZfa4cJCG6Qf5Fn7-yEJtQiI-NiC1DqrQ1NomSUovnOCGY4bNhYwDpV04a1pdSg21E5fICVA6dzQBsHxGw
Now you should know what a JWT is and how a JWT token is constructed. Create a Spring Boot project to learn Spring REST API authentication with JWT.

Go to the Spring Initializr, create a project with the following dependencies, and open the project in your favorite IDE.



Project Description
  • We will be using PostgreSQL as a database.
  • We will create 2 Rest Controller
    • UserController has below endpoints that are unsecured i.e. anyone can access these endpoints to register/log in. 
      • Register User 
      • Login  - This will provide JWT Token in response which will be used to access secured endpoints.
    • ProductController
      • Add Product - This API endpoint is a secure endpoint, so users with an EMPLOYEE role can access this API endpoint.
  • We will be creating the below tables for storing user details 
    • auth_user - Storing user details & role id of a user.
    • role - Storing Roles available in our system
Now let's start developing.

First, let's add database-related configuration to the application.properties file.

spring.datasource.url=jdbc:postgresql://localhost:5432/postgres
spring.datasource.username=add_your_db_username
spring.datasource.password=add_your_db_password
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto = update

Next, add a model package and create User.java & Role.java classes to map your entities to the auth_user & role table.

User.java
package com.demo.jwt.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Entity
@Table(name = "auth_user")
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {

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

    private String name;

    @Column(unique = true)
    private String emailAddress;

    private String password;

    private String address;

    @ManyToOne(cascade = CascadeType.PERSIST)
    private Role role;
}
Role.java 
package com.demo.jwt.model;

import lombok.Data;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "role")
@Data
public class Role {

    @Id
    @Column
    private Long id;

    private String name;
}

Next, add a repository package and create a RoleRepository and UserRepository to access the database. Since we use Spring JPA, we extend JpaRepository. We will also add some methods that we will use later.

RoleRepository.java
package com.demo.jwt.repository;

import com.demo.jwt.model.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface RoleRepository extends 
		JpaRepository<Role,Long> {

    Optional<Role> findByName(String name);
}

UserRepository.java
package com.demo.jwt.repository;

@Repository
public interface UserRepository extends JpaRepository<User,Long>{

    Optional<User> findByEmailAddress (String emailAddress);
}

Next, let's create a dto package and add the following DTO classes for requests and responses from our REST API.
  • UserDTO
  • LoginDTO
  • ResponseDTO 
UserDTO.java
package com.demo.jwt.dto;

import lombok.Data;

@Data
public class UserDTO {

    private String name;

    private String emailAddress;

    private String password;

    private String address;

    private String roleName;

}
LoginDTO.java
package com.demo.jwt.dto;

import lombok.Data;

@Data
public class LoginDTO {

    private String emailAddress;

    private String password;
}
ResponseDTO.java
package com.demo.jwt.dto;

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class ResponseDTO<T> {

    private String responseMsg;

    private T json;
}

Then create an exception package and add BadRequestException. This extends the RunTimeException that throws in case of bad requests.

BadReqeustException.java
package com.demo.jwt.exception;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class BadRequestException extends RuntimeException{

    private String msg;

}

Next, we will add JSON web token dependency in pom.xml

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>

Next, let's create a util package and add a JwtUtil.java class with methods for JWT validation, JWT token generation, and JWT token parsing.

Note - Currently we are only adding a method that generates a JWT token. Add the rest of the methods as needed.

Generating a token requires specifying an expiration date and time, an associated role, a signature algorithm, and a secret. You can also add a set of claims that are used to exchange information between the client and server.

JwtUtil.java
package com.demo.jwt.util;
import com.demo.jwt.dto.JwtTokenDTO;
import com.demo.jwt.model.User;
import io.jsonwebtoken.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class JwtUtil {

    private final Logger logger= LoggerFactory
    	.getLogger(JwtUtil.class);

    private final String jwtSecret = "testsecret";
    private final String jwtIssuer = "test_issuer";

    public String generateAccessToken(User user) {

        return Jwts.builder()
                .setSubject(user.getEmailAddress())
                .setIssuer(jwtIssuer)
                .claim("ROLE", user.getRole().getName())
                .claim("SUBJECT_ID",user.getId())
                .setIssuedAt(new Date())
                .setExpiration(
                	new Date(System.currentTimeMillis()+ 
                    7 * 24 * 60 * 60 * 1000)) // 1 week
                .signWith(SignatureAlgorithm.HS512, jwtSecret)
                .compact();
    }
}
Next, create a configuration package with the BeanConfiguration.java class where you can define all the beans your application needs. For now, let's add a PasswordEncoder bean that will be used to encrypt passwords that we store in the database.

BeanConfiguration.java
package com.demo.jwt.config;

@Configuration
public class BeanConfiguration {

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

Next, let's create a UserController under the Controller package. UserController defines two endpoints for registering users and logins that can be accessed by anyone without authentication
package com.demo.jwt.controller;

import com.demo.jwt.dto.LoginDTO;
import com.demo.jwt.dto.ResponseDTO;
import com.demo.jwt.dto.UserDTO;
import com.demo.jwt.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @PostMapping("/register")
    public ResponseEntity<ResponseDTO> 
    	addUser(@RequestBody UserDTO userDTO){
        
        userService.registerUser(userDTO);
        return new ResponseEntity<>
        	(ResponseDTO.builder()
            	.responseMsg("User register successfully.")
				.build(), HttpStatus.CREATED);
    }

    @PostMapping("/login")
    public ResponseEntity<ResponseDTO> 
    	login(@RequestBody LoginDTO loginDTO){
        
        String token = userService.login(loginDTO);
        return new ResponseEntity<>
        	(ResponseDTO.builder()
            	.json(token)
                .responseMsg("Login successful.")
				.build(), HttpStatus.OK);
    }
}

In the above code, UserController is calling the UserService method for registration & login purposes so let's create a service package with the UserService.java interface.

UserService.java
package com.demo.jwt.service;

import com.demo.jwt.dto.LoginDTO;
import com.demo.jwt.dto.UserDTO;

public interface UserService {
    
  void registerUser(UserDTO userDTO);

  String login(LoginDTO loginDTO);
}

Then create an impl package under the service package and define the implementation of the above service methods in UserServiceImpl.java.
UserServiceImpl.java

package com.demo.jwt.service.impl;

import com.demo.jwt.dto.LoginDTO;
import com.demo.jwt.dto.UserDTO;
import com.demo.jwt.exception.BadRequestException;
import com.demo.jwt.model.Role;
import com.demo.jwt.model.User;
import com.demo.jwt.repository.RoleRepository;
import com.demo.jwt.repository.UserRepository;
import com.demo.jwt.service.UserService;
import com.demo.jwt.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password
		.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

@Service
@Transactional
public class UserServiceImpl implements UserService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private RoleRepository roleRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private JwtUtil jwtUtil;

    @Override
    public void registerUser(UserDTO userDTO) {
        //TODO Add Validation To Restrict Roles If Needed.
        //TODO Add Validations For Unique Email
        Optional<Role> optionalRole = 
        	roleRepository.findByName(userDTO.getRoleName());
        
      if(optionalRole.isEmpty()){
         throw new BadRequestException("Select a valid role.");
      }
       
      User user=User.builder()
      				.name(userDTO.getName())
        			.address(userDTO.getAddress())
                	.emailAddress(userDTO.getEmailAddress())
                    .role(optionalRole.get())
                    .password(passwordEncoder.encode
                    		(userDTO.getPassword()))
                	.build();
        userRepository.save(user);
    }

    @Override
    public String login(LoginDTO loginDTO) {
        Optional<User> userOptional=
        	userRepository.findByEmailAddress
            	(loginDTO.getEmailAddress());
        
        if(userOptional.isEmpty()){
            throw new BadRequestException("User Not Found.");
        }
        if(passwordEncoder.matches
        	(loginDTO.getPassword(),
            userOptional.get().getPassword())
          ){
            return jwtUtil.generateAccessToken
            		(userOptional.get());
        }else{
           throw new 
           BadRequestException("Invalid UserName Or Password");
        }
    }
}

Note - You should use the PasswordEncode matching method to ensure that the encrypted password you store in the database matches the plain password that the user enters at login. If the authentication succeeds, it calls the JwtUtil 's generateAccessToken() method that has already been defined.

Next, create an exception handler package and an underlying GlobalExceptionHandler.java class that handles all exceptions thrown by your code.
package com.demo.jwt.exceptionhandler;

import com.demo.jwt.dto.ResponseDTO;
import com.demo.jwt.exception.BadRequestException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class GlobalExceptionHandler {

    private final Logger logger= 
    	LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler({BadRequestException.class})
    public ResponseEntity<ResponseDTO> 
    		handleBadRequestException(BadRequestException e)
    {
      logger.info("Bad Request Found {} ",e.getMsg(),e);
      return new ResponseEntity<>(
      	ResponseDTO.builder().
        	responseMsg(e.getMsg()).build(),
			HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler({Exception.class})
    public ResponseEntity<ResponseDTO> 
    			handleException(Exception e)
    {
      logger.info("Unknown error occur {} ",
      	e.getMessage(),e);
      
      return new ResponseEntity<>(
      		ResponseDTO.builder()
            	.responseMsg(e.getMessage()).build(),
			HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

Next, add a JwtTokenDTO that will be used to store details after token parsing, and a Principal.java class that contains basic getters and setters.

Principal.java
package com.demo.jwt.dto;

import lombok.Data;

@Data
public class Principal {

    private String emailAddress;

    private String token;

    private String role;

}
JwtTokenDTO.java
package com.demo.jwt.dto;

import lombok.Builder;
import lombok.Data;

import java.util.Date;

@Builder
@Data
public class JwtTokenDTO {

    private String subject;

    private Date expirationDate;

    private String role;
}
Next, under the JwtUtil.java class, add methods to validate and parse the JWT token using the secret key used to sign the token.
  //TODO Throw And Handle Below Exceptions Under 
  		 Global Exception Handler
	
  public boolean validate(String token) {
    try {
     Jwts.parser()
     	 .setSigningKey(jwtSecret)
         .parseClaimsJws(token);
     return true;
    } catch (MalformedJwtException ex) {
       logger.error("Invalid JWT token - {}", ex.getMessage());
    } catch (ExpiredJwtException ex) {
       logger.error("Expired JWT token - {}", ex.getMessage());
    } catch (UnsupportedJwtException ex) {
       logger.error("Unsupported JWT token - {}",
       	ex.getMessage());
    } catch (IllegalArgumentException ex) {
       logger.error("JWT claims string is empty - {}",
       	ex.getMessage());
    }
        return false;
    }

    public JwtTokenDTO getJwtTokenDTO(String token){
        Claims claims = 
        		Jwts.parser()
        		.setSigningKey(jwtSecret)
                .parseClaimsJws(token).getBody();
        
        return JwtTokenDTO.builder().subject(claims.getSubject())
                .expirationDate(claims.getExpiration())
                .role(claims.get("ROLE", String.class))
                .build();
    }

Next, let's create a SpringSecurityConfiguration.java class under the configuration package. Here we define a SecurityFilterChain bean, define a lot of configurations such as the session creation policy as stateless, and also disable cors and csrf. Also, add configuration to expose the UserController API endpoint as public. And secured other API endpoints.

package com.demo.jwt.config;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityConfiguration {

    @Autowired
    private JwtTokenFilter jwtTokenFilter;

    @Bean
    public SecurityFilterChain 
    		filterChain(HttpSecurity httpSecurity) 
            	throws Exception 
     {

        httpSecurity = httpSecurity.cors()
        				.and().csrf().disable();
        httpSecurity=httpSecurity.sessionManagement()
        			.sessionCreationPolicy
                    (SessionCreationPolicy.STATELESS)
                    .and();

        httpSecurity.authorizeRequests()
        		.antMatchers(HttpMethod.POST,"/user/**")
                .permitAll()
                .anyRequest().authenticated();

        httpSecurity.addFilterBefore
        	(jwtTokenFilter, 
            UsernamePasswordAuthenticationFilter.class);
        return httpSecurity.build();
    }
}

Now let's create JwtTokenFilter.java under the filter package. This filter uses the JwtUtil method to validate the token. Also create a UserNamePasswordAuthenticationToken and add a SecurityContextHolder.

package com.demo.jwt.filter;

@Component
public class JwtTokenFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtil jwtUtil;

    @Override
    protected void 
    	doFilterInternal(
        	HttpServletRequest request,
        	HttpServletResponse response,
			FilterChain filterChain
           ) throws ServletException, IOException 
     {
        
        final String header = 
        	request.getHeader(HttpHeaders.AUTHORIZATION);
        
        if (isEmpty(header) || !header.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }

        final String token = header.split(" ")[1].trim();
        if (!jwtUtil.validate(token)) {
            filterChain.doFilter(request, response);
            return;
        }

        JwtTokenDTO jwtTokenDTO=jwtUtil.getJwtTokenDTO(token);
        List<GrantedAuthority> grantedAuthorityList=
        	new ArrayList<>();

        grantedAuthorityList.add(
          new SimpleGrantedAuthority(jwtTokenDTO.getRole()));

        Principal principal=new Principal();
        principal.setToken(token);
        principal.setEmailAddress(jwtTokenDTO.getSubject());
        principal.setRole(jwtTokenDTO.getRole());

        UsernamePasswordAuthenticationToken authenticationToken=
			new UsernamePasswordAuthenticationToken
            (principal,null,grantedAuthorityList);
        
        SecurityContextHolder.getContext()
        	.setAuthentication(authenticationToken);
        filterChain.doFilter(request,response);
    }
}

All necessary configuration for JWT authentication is complete. Now it's time to test. Now let's create a ProductController under the controller package with a simple endpoint (secure endpoint) for adding products.

ProductController.java

package com.demo.jwt.controller;

@RestController
@RequestMapping("/product")
public class ProductController {

    private final Logger logger= 
    	LoggerFactory.getLogger(ProductController.class);

    @PostMapping
    @PreAuthorize("hasAuthority('EMPLOYEE')")
    public ResponseEntity<ResponseDTO> addProduct(){
     
    //NOTE This method is just to check add product 
    	//is called or not.
        
     logger.info("Add product called successfully.");
     
     return new ResponseEntity<>(
        ResponseDTO.builder().responseMsg("Product Added.")
		.build(), HttpStatus.CREATED);
    }
}

We specified that all users with the EMPLOYEE role should be granted access to the Add Product endpoint using Spring Security's @PreAuthorize annotation.

Next, we need to add a record in the role table using the below query.

insert into role values(1,'EMPLOYEE');

You can register a single user with the Registration API endpoint as shown below.


Generate a JWT token using the Login API endpoint as shown below.

Then try to access the add product endpoint using the above token generated by the login API

Note - We need to pass the token under Authorization in postman with Type as Bearer Token as shown below.

Also, trying to access the product endpoint without adding a token will return a 403 Forbidden as shown below.


Attaching the project structure for reference.


Let me know in the comments if you run into any issues implementing the example above.
Thanks!