JWT Authentication using Spring Security OAuth2 in Spring Boot Example

In this tutorial, you will learn how to implement Spring Security OAuth2 for role-based JWT authentication in Spring Boot. We will create a sample application in which users can sign up, log in to retrieve a JWT token, and use that token to access secured REST APIs.

JWT is short for JSON Web Token. It is an open standard for securely transmitting information between parties as a JSON object. Because it is digitally signed, this information can be checked and trusted. JWTs can be signed with either a secret (HMAC) or a public/private key pair using RSA or ECDSA.

Follow these steps to complete this tutorial:

Create a Spring Boot Application

  1. Go to the Spring Initializr website at https://start.spring.io.
  2. Create a Spring Boot application with details as follows:
    • Project: Choose the project type (Maven or Gradle).
    • Language: Set the language to Java.
    • Spring Boot: Specify the Spring Boot version. The default selection is the latest stable version of Spring Boot, so you can leave it unchanged.
    • Project Metadata: Enter a Group and Artifact name for your project. The group name is the id of the project. Artifact is the name of your project. Add any necessary project metadata (description, package name, etc.)
    • Choose between packaging as a JAR (Java Archive) or a WAR (Web Application Archive) depends on how you plan to deploy your Spring Boot application. Choose JAR packaging if you want a standalone executable JAR file and WAR packaging if you intend to deploy your application to a Java EE application server or servlet container. When you package your Spring Boot application as a JAR using JAR packaging, it includes an embedded web server, such as Tomcat, by default. This means that you don't need to separately deploy your application to an external Tomcat server. Instead, you can run the JAR file directly, and the embedded Tomcat server will start and serve your application.
    • Select the Java version based on the compatibility requirements of your project. Consider the specific needs of your project, any compatibility requirements, and the Java version supported by your target deployment environment when making these choices.
    • Click on the Add Dependencies button.
    • Choose the following dependencies: Spring Web, Spring Security, OAuth2 Resource Server, Lombok, MySQL Driver, Spring Data JPA, and Spring Boot DevTools.

    For example:

  3. Generate the project:
    • Click on the Generate button.
    • Spring Initializr will generate a zip file containing your Spring Boot project.
  4. Download and extract the generated project:
    • Download the zip file generated by Spring Initializr.
    • Extract the contents of the zip file to a directory on your local machine.
  5. Import the project into your IDE:
    • Open your preferred IDE (IntelliJ IDEA, Eclipse, or Spring Tool Suite).
    • Import the extracted project as a Maven or Gradle project, depending on the build system you chose in Spring Initializr.

Add Dependencies for JWT Support

Add dependencies for JWT Support. We need the 'jsonwebtoken' library for Java Spring Boot. You can find the 'jsonwebtoken' at https://github.com/jwtk/jjwt#creating-a-jwe. This library is used to generate, decode, and verify JWT tokens.

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'

Generate a Java Keystore

Generate a keystore to use it for signing and verifying JWT tokens. Java comes with keytool that helps to generate a key pair. To sign JWT, RSA private key is required and to validate a JWT, RSA public key corresponding to the RSA private key is required.

Execute the following command to generate a keystore:

On Windows, open a command prompt and navigate to the location C:\Program Files\Java\jdk-17\bin\.

.\keytool -genkey -alias signjwt -keyalg RSA -keystore E:/keystore.jks -keysize 2048 -validity 365000

Here, "C:\Program Files\Java\jdk-17\bin" should be the location of your Java installation, and "E:/keystore.jks" should be the destination path and filename.

keytool -genkey -alias signjwt -keyalg RSA -keystore /path/to/keystore.jks -keysize 2048 -validity 365000
keytool -genkey -alias signjwt -keyalg RSA -keystore /path/to/keystore.jks -keysize 2048 -validity 365000

Make sure to replace "/path/to/keystore.jks" with the actual path and filename where you want to generate the keystore on your macOS/Linux/Ubuntu system.

Add Configurations

After generating the keystore.jks file, create a new folder named "keys" inside the resources directory of your project and copy the keystore.jks file into the "/resources/keys/" folder.

In the src/main/resources/application.properties file, add the following configuration properties, making sure to replace the values with those specific to your project:

#port on which the application should run
server.port= 8080

#mysql database connection
spring.datasource.url = jdbc:mysql://localhost:3306/tb_test
spring.datasource.username = root
spring.datasource.password = Testing123$
spring.datasource.timeBetweenEvictionRunsMillis = 60000
spring.datasource.maxIdle = 1
#below properties will automatically creates and updates database schema
spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=update

#public private keys
app.jwt.keystore-location=keys/keystore.jks
app.jwt.keystore-password=testing123$
app.jwt.key-alias=signjwt
app.jwt.private-key-passphrase=testing123$

Here, server.port=8080 configuration line is used to specify the port number on which the server will listen for incoming requests. In this case, it sets the server port to 8080.

spring.datasource.url: Specifies the URL for connecting to the MySQL database. In this example, it connects to a database named tb_test on localhost at port 3306.

spring.datasource.username and spring.datasource.password: Provide the credentials (username and password) for authenticating with the MySQL database.

spring.datasource.timeBetweenEvictionRunsMillis: Sets the time interval (in milliseconds) between eviction runs for idle database connections.

spring.datasource.maxIdle: Specifies the maximum number of idle connections in the connection pool.

spring.jpa.generate-ddl=true: Instructs JPA to generate the necessary SQL scripts for creating database tables based on the entity classes.

spring.jpa.hibernate.ddl-auto=update: Configures Hibernate (the JPA implementation) to automatically update the database schema when changes are detected in the entity classes.

app.jwt.keystore-location: It specifies the location of the keystore file.

app.jwt.keystore-password: It is the keystore password.

app.jwt.key-alias: It defines the alias for the specific key in the keystore that will be used for signing and verifying JWTs. In this case, the key alias is set as signjwt.

app.jwt.private-key-passphrase: It is passphrase (password) for the private key associated with the specified key alias. The private key passphrase is required to access the private key for signing JWTs.

Creating Entity classes

Create a User class that represents the user entity:

package com.example.user.entity;

import java.io.Serializable;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Builder.Default;
import lombok.Data;
import lombok.NoArgsConstructor;

@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
@EntityListeners(AuditingEntityListener.class)
@Entity
@Table(name = "user")
public class User implements Serializable {

  private static final long serialVersionUID = 1L;

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

  @Column(name = "username", nullable = false, unique = true, length = 500)
  private String username;

  @Column(name = "email", nullable = false, unique = true, length = 500)
  private String email;

  @Column(name = "password", nullable = false, length = 500)
  private String password;

  @Column(name = "first_name", nullable = true, length = 500)
  private String firstName;

  @Column(name = "last_name", nullable = true, length = 500)
  private String lastName;

  @CreatedDate
  @Column(name = "created_date", nullable = true)
  private Date createdDate;

  @LastModifiedDate
  @Column(name = "modified_date", nullable = true)
  private Date modifiedDate;

  @Default
  @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
  @JoinColumn(name = "user_id", referencedColumnName = "id")
  private Set<Role> roles = new HashSet<>();
}

Create a Role class that represents the role entity:

package com.example.user.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder(toBuilder = true)
@Entity
@Table(name = "role")
public class Role {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @Column(name = "name", nullable = true, unique = false, length = 100)
  private String name;

  @Column(name = "user_id", nullable = false, unique = false, length = 50)
  private Long userId;
}

Create a Post class that represents the post entity:

package com.example.post.entity;

import java.util.Date;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Builder.Default;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder(toBuilder = true)
@EntityListeners(AuditingEntityListener.class)
@Entity
@Table(name = "post")
public class Post {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @Column(name = "title", nullable = false, length = 500)
  private String title;

  @Column(name = "body", nullable = true, length = 5000)
  private String body;

  @Column(name = "path", nullable = true, length = 500)
  private String path;

  @Default
  @Column(name = "deleted")
  private boolean deleted = false;

  @CreatedDate
  @Column(name = "created_date", nullable = true)
  private Date createdDate;

  @LastModifiedDate
  @Column(name = "modified_date", nullable = true)
  private Date modifiedDate;

  @CreatedBy
  @Column(name = "createdBy", nullable = true)
  private Long createdBy;

  @LastModifiedBy
  @Column(name = "modifiedBy", nullable = true)
  private Long modifiedBy;
}

Create DTO (Data Transfer Object) classes

Create a DTO class named SignUpDto:

package com.example.user.dto;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(Include.NON_NULL)
public class SignUpDto {
  
  private String email;
  private String username;
  private String password;
  private String status;
}

Create a DTO class named LoginDto:

package com.example.user.dto;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(Include.NON_NULL)
public class LoginDto {
  private String email;
  private String username;
  private String password;
  private String status;
  private String token;
}

Create a DTO class named PostDto:

package com.example.post.dto;

import java.util.Date;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(Include.NON_NULL)
public class PostDto {
  private Long postId;
  private String title;
  private String body;
  private String path;
  private Boolean deleted;
  private Long createdBy;
  private Long modifiedBy;
  private Date createdDate;
  private Date modifiedDate;
}

Create Repository interfaces

Create a UserRepository interface that represents a repository responsible for handling data access operations related to users:

package com.example.user.repository;

import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import com.example.user.entity.User;

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

  User findByEmail(String email);

  User findByUsername(String username);
}

Create a RoleRepository class that represents a repository responsible for handling data access operations related to user roles:

package com.example.user.repository;

import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import com.example.user.entity.Role;

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

}

Create a PostRepository class that represents a repository responsible for handling data access operations related to user posts:

package com.example.post.repository;

import java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import com.example.post.entity.Post;

@Repository
public interface PostRepository extends CrudRepository<Post, Long> {
  Page<Post> findByCreatedByAndDeletedIsFalse(Long userId, Pageable pageable);

  Optional<Post> findByIdAndDeletedIsFalse(Long postId);
}

Create Custom Exceptions

Create classes to handle custom exceptions. Custom exceptions allow you to create specific exception types for your application that can be thrown when certain exceptional situations occur. Let's start by creating a Java class named Error with three private fields: message, status, and timestamp. This class represents data container that holds information related to an error:

package com.example.exception.model;

import lombok.Data;

@Data
public class Error {
	private String message;
	private int status;
	private Long timestamp;
}

Create a custom exception class named UnauthorizedException, which extends the RuntimeException class:

package com.example.exception;

public class UnauthorizedException extends RuntimeException {
  private static final long serialVersionUID = 1L;

  public UnauthorizedException(String message) {
    super(message);
  }

}

Create a custom exception class named ValidationException, which extends the RuntimeException class:

package com.example.exception;

public class ValidationException extends RuntimeException {
  private static final long serialVersionUID = 1L;

  public ValidationException(String message) {
    super(message);
  }

}

Create a Global Exception Handler class named GlobalExceptionHandlerController. The purpose of this class is to handle specific exceptions globally, providing consistent and customized error responses to clients when certain exceptions occur during the application's execution:

package com.example.exception.controller;

import java.util.Date;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import com.example.exception.UnauthorizedException;
import com.example.exception.ValidationException;
import com.example.exception.model.Error;
import jakarta.servlet.http.HttpServletRequest;

@ControllerAdvice
public class GlobalExceptionHandlerController {

  @ExceptionHandler(UnauthorizedException.class)
  public ResponseEntity<Object> unauthorized(UnauthorizedException ex, HttpServletRequest request) {
    Error error = new Error();
    error.setMessage(ex.getMessage());
    error.setTimestamp(new Date().getTime());
    error.setStatus(HttpStatus.UNAUTHORIZED.value());
    return new ResponseEntity<>(error, null, HttpStatus.UNAUTHORIZED);
  }

  @ExceptionHandler(ValidationException.class)
  public ResponseEntity<Object> validation(ValidationException ex, HttpServletRequest request) {
    Error error = new Error();
    error.setMessage(ex.getMessage());
    error.setTimestamp(new Date().getTime());
    error.setStatus(HttpStatus.BAD_REQUEST.value());
    return new ResponseEntity<>(error, null, HttpStatus.BAD_REQUEST);
  }

}

Create Security Configuration Classes

Create a class named CustomUserDetails, which implements the Spring Security UserDetails interface. The purpose of this class is to provide a custom representation of user details that can be used in authentication and authorization processes within a Spring Security-based application.

package com.example.auth.service;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.example.user.entity.User;


public class CustomUserDetails implements UserDetails {
  private static final long serialVersionUID = 1L;

  private static final Logger LOGGER = LoggerFactory.getLogger(CustomUserDetails.class);

  private User user;

  public CustomUserDetails(User user) {
    this.user = user;
  }

  @Override
  public Collection<? extends GrantedAuthority> getAuthorities() {
    List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();

    List<String> authorityList =
        user.getRoles().stream().map(a -> a.getName()).collect(Collectors.toList());
    for (String authority : authorityList) {
      authorities.add(new SimpleGrantedAuthority(authority));
    }
    LOGGER.info("User Roles = {}", authorities);
    return authorities;
  }

  @Override
  public String getPassword() {

    return user.getPassword();
  }

  @Override
  public String getUsername() {
    return user.getUsername();
  }

  @Override
  public boolean isAccountNonExpired() {
    return true;
  }

  @Override
  public boolean isAccountNonLocked() {
    return true;
  }

  @Override
  public boolean isCredentialsNonExpired() {
    return true;
  }

  @Override
  public boolean isEnabled() {
    return true;
  }

  public long getId() {
    return user.getId();
  }

  public String getEmail() {
    return user.getEmail();
  }

}

Create a service class named CustomUserDetailsService that implements the UserDetailsService interface from the Spring Security framework. It is responsible for providing user details like username, password, roles, to Spring Security during the authentication process.

package com.example.auth.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import com.example.user.entity.User;
import com.example.user.repository.UserRepository;


@Service
public class CustomUserDetailsService implements UserDetailsService {
  @Autowired
  private UserRepository userRepository;

  @Override
  public CustomUserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    User user = userRepository.findByUsername(username);
    if (user == null) {
      throw new UsernameNotFoundException("User not found.");
    }
    return new CustomUserDetails(user);
  }

}

This class overrides the loadUserByUsername method from the UserDetailsService interface. It is called by Spring Security during the authentication process when it needs to retrieve user details based on the provided username. Here, the service uses the UserRepository to find a user by the username. The UserRepository class that we created earlier has a method findByUsername, which queries the database for a user with the specified username.

In summary, this CustomUserDetailsService class acts as a bridge between Spring Security's authentication process and the database. It fetches user details from the database using the UserRepository and returns them in the form of a custom CustomUserDetails object that implements Spring Security's UserDetails interface. The CustomUserDetails object is used by Spring Security for user authentication and authorization.

Create a configuration class named KeystoreConfig responsible for loading and providing access to cryptographic keys from a keystore. The keystore is typically a file that contains keys and certificates used for various security-related operations.

package com.example.auth.config;

import java.io.IOException;
import java.io.InputStream;
import java.security.Key;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;

@Configuration
public class KeystoreConfig {
  private static final Logger log = LoggerFactory.getLogger(KeystoreConfig.class);

  @Value("${app.jwt.keystore-location}")
  private String keyStorePath;

  @Value("${app.jwt.keystore-password}")
  private String keyStorePassword;

  @Value("${app.jwt.key-alias}")
  private String keyAlias;

  @Value("${app.jwt.private-key-passphrase}")
  private String privateKeyPassphrase;

  @Bean
  public KeyStore getKeyStore() {
    try {
      KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
      InputStream resourceAsStream =
          Thread.currentThread().getContextClassLoader().getResourceAsStream(keyStorePath);
      keyStore.load(resourceAsStream, keyStorePassword.toCharArray());
      return keyStore;
    } catch (IOException | CertificateException | NoSuchAlgorithmException | KeyStoreException e) {
      log.error("Unable to load keystore: {}", keyStorePath, e);
    }

    throw new IllegalArgumentException("Unable to load keystore");
  }

  @Bean
  public RSAPrivateKey getJwtSigningPrivateKey() {
    try {
      Key key = getKeyStore().getKey(keyAlias, privateKeyPassphrase.toCharArray());
      if (key instanceof RSAPrivateKey) {
        return (RSAPrivateKey) key;
      }
    } catch (UnrecoverableKeyException | NoSuchAlgorithmException | KeyStoreException e) {
      log.error("Unable to load private key from keystore: {}", keyStorePath, e);
    }

    throw new IllegalArgumentException("Unable to load private key");
  }

  @Bean
  public RSAPublicKey getJwtValidationPublicKey(KeyStore keyStore) {
    try {
      Certificate certificate = keyStore.getCertificate(keyAlias);
      PublicKey publicKey = certificate.getPublicKey();

      if (publicKey instanceof RSAPublicKey) {
        return (RSAPublicKey) publicKey;
      }
    } catch (KeyStoreException e) {
      log.error("Unable to load private key from keystore: {}", keyStorePath, e);
    }

    throw new IllegalArgumentException("Unable to load RSA public key");
  }

  @Bean
  public JwtDecoder jwtDecoder(RSAPublicKey rsaPublicKey) {
    return NimbusJwtDecoder.withPublicKey(rsaPublicKey).build();
  }
}

Create a configuration class named JwtManager, which is responsible for handling JWT (JSON Web Tokens). JWTs are commonly used for authentication and authorization purposes in web applications.

package com.example.auth.config;

import java.util.Date;
import java.util.Map;
import java.util.function.Function;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;


@Component
public class JwtManager {
  private static final Logger LOGGER = LoggerFactory.getLogger(JwtManager.class);

  // 25 hours expiry time
  public static final long JWT_TOKEN_VALIDITY_IN_HOUR = 24 * 60 * 60;

  @Autowired
  private KeystoreConfig keystoreConfig;

  public String generateToken(Map<String, Object> claims, String subject) {
    LOGGER.info("generating token....");

    return Jwts.builder().setClaims(claims).setSubject(subject)
        .setIssuedAt(new Date(System.currentTimeMillis()))
        .setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY_IN_HOUR * 1000))
        .signWith(keystoreConfig.getJwtSigningPrivateKey(), SignatureAlgorithm.RS256).compact();
  }

  // get username from token
  public String getUsernameFromToken(String token) {
    return getClaimFromToken(token, Claims::getSubject);
  }

  // get user id from token
  public String getIdFromToken(String token) {
    return getClaimFromToken(token, Claims::getId);
  }

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

  // get information from token
  private Claims getAllClaimsFromToken(String token) {
    return Jwts.parserBuilder().setSigningKey(keystoreConfig.getJwtSigningPrivateKey()).build()
        .parseClaimsJws(token).getBody();
  }

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

  // get expiration time from jwt token
  public Date getExpirationDateFromToken(String token) {
    return getClaimFromToken(token, Claims::getExpiration);
  }

  // check if token is expired
  private Boolean isTokenExpired(String token) {
    final Date expiration = getExpirationDateFromToken(token);
    return expiration.before(new Date());
  }
}

Here, the method generateToken is used to generate a new JSON Web Token based on the provided claims and subject. The claims is a map object containing additional data (claims) to be included in the token. These claims can be used to store information about the user or any other relevant data. The map should have String keys and Object values. The subject typically represents the user for whom the token is being generated. It is commonly the username or user ID. The method builds the JWT using the Jwts.builder() from the io.jsonwebtoken.Jwts class. It sets various properties of the token, such as the claims (setClaims), the subject (setSubject), the issuance time (setIssuedAt), and the expiration time (setExpiration). The expiration time is set based on the current time plus a predefined validity period (25 hours in this case). The method then signs the token using the RSA private key obtained from the keystoreConfig component, using the RS256 (RSA with SHA-256) signature algorithm. The method returns a string representing the generated JWT.

The class contains other methods to extract the username, id, claim, and token expiration time. It also has a method to check the token if it is expired.

Create a Java class named JwtSecurityFilter that represents a Spring Boot filter, which is responsible for handling JSON Web Tokens (JWT) for authentication:

package com.example.auth.config;

import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import com.example.auth.service.CustomUserDetails;
import com.example.auth.service.CustomUserDetailsService;
import io.jsonwebtoken.ExpiredJwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@Component
public class JwtSecurityFilter extends OncePerRequestFilter {
  @Autowired
  private JwtManager jwtManager;
  @Autowired
  private CustomUserDetailsService customUserDetailsService;

  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
      FilterChain filterChain) throws ServletException, IOException {
    final String requestTokenHeader = request.getHeader("Authorization");

    String username = null;
    String jwtToken = null;
    // get only token if the token is in the form of "Bearer token"
    if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
      jwtToken = requestTokenHeader.substring(7);
      try {
        username = jwtManager.getUsernameFromToken(jwtToken);
      } catch (IllegalArgumentException e) {
        logger.info("Unable to get JWT Token");
      } catch (ExpiredJwtException e) {
        logger.info("JWT Token has expired");
      }
    } else {
      logger.warn("JWT Token does not begin with Bearer String");
    }

    // validate the token
    if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {

      CustomUserDetails userDetails = customUserDetailsService.loadUserByUsername(username);

      // if token is valid configure Spring Security to manually set authentication
      if (jwtManager.validateToken(jwtToken, userDetails)) {

        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
            new UsernamePasswordAuthenticationToken(userDetails, null,
                userDetails.getAuthorities());
        usernamePasswordAuthenticationToken
            .setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

        SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
      }
    }
    filterChain.doFilter(request, response);
  }

}

This class is a custom Spring Security filter that intercepts incoming requests, extracts the JWT token from the Authorization header, validates the token, and if valid, sets the user's authentication in the Spring Security context. This enables token-based authentication for the application, allowing authenticated users to access protected resources and perform authorized actions.

Create a configuration class named SecurityConfiguration. This class represents a configuration for the security setup of a Spring Boot application, specifically focusing on authentication and authorization using Spring Security:

package com.example.auth.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
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.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.example.auth.service.CustomUserDetailsService;
import jakarta.servlet.http.HttpServletResponse;

@Configuration
@EnableWebSecurity(debug = false)
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration {

  @Autowired
  private JwtSecurityFilter jwtSecurityFilter;

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.cors(cors -> cors.disable()).csrf(csrf -> csrf.disable())
        .authorizeHttpRequests(
            (authz) -> authz.requestMatchers("/users/signup", "/users/login").permitAll()
                .anyRequest().authenticated())
        .sessionManagement((sessionManagement) -> {
          sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }).exceptionHandling(exceptionHandling -> exceptionHandling
            .authenticationEntryPoint((request, response, ex) -> {
              response.sendError(HttpServletResponse.SC_UNAUTHORIZED, ex.getMessage());
            }))
        .addFilterBefore(jwtSecurityFilter, UsernamePasswordAuthenticationFilter.class);

    return http.build();
  }


  @Bean
  public UserDetailsService userDetailsService() {
    return new CustomUserDetailsService();
  }

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

}

This configuration sets up Spring Security to handle user authentication and authorization, using JWT tokens for stateless authentication. The JwtSecurityFilter plays a crucial role in validating and processing JWT tokens, while the CustomUserDetailsService is used to load user-specific details during authentication. The BCryptPasswordEncoder ensures that user passwords are securely hashed and verified.

The @EnableWebSecurity annotation enables Spring Security's web security features for the application. The @EnableMethodSecurity(prePostEnabled = true) enables method-level security using Spring Security's annotations like @PreAuthorize and @PostAuthorize.

Create Utility class

Create a utility class named UserUtil:

package com.example.user.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import com.example.auth.service.CustomUserDetails;


public class UserUtil {
  private static final Logger LOGGER = LoggerFactory.getLogger(UserUtil.class);

  public static CustomUserDetails getCurrentLoggedInUser() {
    LOGGER.info("getCurrentLoggedInUser... invoked");
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    CustomUserDetails userDetails = (CustomUserDetails) auth.getPrincipal();
    return userDetails;
  }
}

This class provides a static utility method, getCurrentLoggedInUser(), which allows other parts of the application to easily access the details of the currently authenticated user. The method retrieves the authentication object from the security context, which contains the user's principal information. It then casts the principal object to a CustomUserDetails instance to return more detailed information about the user. This utility method is handy when you need to access user-specific information throughout the application without the need to repeat the authentication retrieval process.

Create Services

Create a service interface named UserService:

package com.example.user.service;

import com.example.user.dto.LoginDto;
import com.example.user.dto.SignUpDto;

public interface UserService {
  SignUpDto signUp(SignUpDto signUpDto);

  LoginDto login(LoginDto loginRequest);
}

Create a service interface named PostService:

package com.example.post.service;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import com.example.post.dto.PostDto;

public interface PostService {
  PostDto create(PostDto postDto);

  Page<PostDto> getPostsByUserId(Long userId, Pageable pageable);

  PostDto getPostById(Long postId);

  PostDto update(Long postId, PostDto postDto);

  PostDto delete(Long postId);
}

Create Service implementation classes

Create a service implementation class named UserServiceImpl for interface UserService:

package com.example.user.service.impl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import com.example.auth.config.JwtManager;
import com.example.auth.service.CustomUserDetails;
import com.example.auth.service.CustomUserDetailsService;
import com.example.exception.UnauthorizedException;
import com.example.exception.ValidationException;
import com.example.user.dto.LoginDto;
import com.example.user.dto.SignUpDto;
import com.example.user.entity.Role;
import com.example.user.entity.User;
import com.example.user.repository.RoleRepository;
import com.example.user.repository.UserRepository;
import com.example.user.service.UserService;

@Service
public class UserServiceImpl implements UserService {
  private static final Logger LOGGER = LoggerFactory.getLogger(UserServiceImpl.class);

  @Autowired
  private UserRepository userRepository;
  @Autowired
  private RoleRepository roleRepository;
  @Autowired
  private CustomUserDetailsService customUserDetailsService;
  @Autowired
  private PasswordEncoder passwordEncoder;
  @Autowired
  private JwtManager jwtManager;

  @Override
  public SignUpDto signUp(SignUpDto signUpDto) {
    LOGGER.info("calling signUp....");

    if (signUpDto.getEmail() == null || signUpDto.getEmail().isEmpty()) {
      throw new ValidationException("email is required.");
    }
    if (signUpDto.getUsername() == null || signUpDto.getUsername().isEmpty()) {
      throw new ValidationException("username is required.");
    }
    if (signUpDto.getPassword() == null || signUpDto.getPassword().isEmpty()) {
      throw new ValidationException("password is required.");
    }

    BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    String encodedPassword = passwordEncoder.encode(signUpDto.getPassword());

    User user = User.builder().email(signUpDto.getEmail()).username(signUpDto.getUsername())
        .password(encodedPassword).build();
    user = userRepository.save(user);

    List<Role> authorities = new ArrayList<>();
    authorities.add(Role.builder().userId(user.getId()).name("USER").build());
    // authorities.add(Role.builder().user(user).name("ADMIN").build());
    roleRepository.saveAll(authorities);

    return SignUpDto.builder().status("success").build();
  }

  @Override
  public LoginDto login(LoginDto loginDto) {
    LOGGER.info("calling login....");

    if (loginDto.getUsername() == null || loginDto.getUsername().isEmpty()) {
      throw new ValidationException("username is required.");
    }
    if (loginDto.getPassword() == null || loginDto.getPassword().isEmpty()) {
      throw new ValidationException("password is required.");
    }

    CustomUserDetails userDetails;
    try {
      userDetails = customUserDetailsService.loadUserByUsername(loginDto.getUsername());

    } catch (UsernameNotFoundException e) {
      LOGGER.error("Error: ", e);
      throw new UnauthorizedException("User Not Found.");
    }

    if (passwordEncoder.matches(loginDto.getPassword(), userDetails.getPassword())) {
      LOGGER.info("password matched");

      Map<String, Object> claims = new HashMap<>();
      claims.put("username", userDetails.getUsername());

      String authorities = userDetails.getAuthorities().stream().map(GrantedAuthority::getAuthority)
          .collect(Collectors.joining(","));
      claims.put("roles", authorities);
      claims.put("userId", userDetails.getId());
      String subject = userDetails.getUsername();
      String jwt = jwtManager.generateToken(claims, subject);
      return LoginDto.builder().token(jwt).build();
    }

    throw new UnauthorizedException("Username or Password does not match.");
  }
}

Create a service implementation class named PostServiceImpl for interface PostService:

package com.example.post.service.impl;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import com.example.exception.ValidationException;
import com.example.post.dto.PostDto;
import com.example.post.entity.Post;
import com.example.post.repository.PostRepository;
import com.example.post.service.PostService;
import jakarta.transaction.Transactional;

@Service
public class PostServiceImpl implements PostService {
  @Autowired
  private PostRepository postRepository;

  @Transactional
  @Override
  public PostDto create(PostDto postDto) {
    Post post = Post.builder().title(postDto.getTitle()).body(postDto.getBody())
        .path(postDto.getPath()).createdBy(postDto.getCreatedBy()).build();
    post = postRepository.save(post);
    return PostDto.builder().postId(post.getId()).title(post.getTitle()).body(post.getBody())
        .path(post.getPath()).createdBy(post.getCreatedBy()).build();
  }

  @Override
  public Page<PostDto> getPostsByUserId(Long userId, Pageable pageable) {
    Page<Post> posts = postRepository.findByCreatedByAndDeletedIsFalse(userId, pageable);
    List<PostDto> postDtos = new ArrayList<>();
    posts.forEach(p -> {
      postDtos.add(PostDto.builder().title(p.getTitle()).postId(p.getId()).body(p.getBody())
          .path(p.getPath()).deleted(p.isDeleted()).createdBy(p.getCreatedBy())
          .modifiedBy(p.getModifiedBy()).createdDate(p.getCreatedDate())
          .modifiedDate(p.getModifiedDate()).build());
    });
    return new PageImpl<PostDto>(postDtos, pageable, posts.getTotalElements());
  }

  @Override
  public PostDto getPostById(Long postId) {
    Optional<Post> optPost = postRepository.findByIdAndDeletedIsFalse(postId);
    if (optPost.isPresent()) {
      Post post = optPost.get();
      return PostDto.builder().title(post.getTitle()).postId(post.getId()).body(post.getBody())
          .path(post.getPath()).deleted(post.isDeleted()).createdBy(post.getCreatedBy())
          .modifiedBy(post.getModifiedBy()).createdDate(post.getCreatedDate())
          .modifiedDate(post.getModifiedDate()).build();
    }
    throw new ValidationException("Post not found");
  }

  @Override
  public PostDto update(Long postId, PostDto postDto) {
    if (postId == null) {
      throw new ValidationException("postId is required.");
    }
    Optional<Post> optPost = postRepository.findByIdAndDeletedIsFalse(postId);
    if (optPost.isEmpty()) {
      throw new ValidationException("Post not found");
    }
    Post post = optPost.get();
    post = post.toBuilder().title(postDto.getTitle()).body(postDto.getBody())
        .path(postDto.getPath()).build();
    post = postRepository.save(post);
    return PostDto.builder().title(post.getTitle()).postId(post.getId()).body(post.getBody())
        .path(post.getPath()).deleted(post.isDeleted()).createdBy(post.getCreatedBy())
        .modifiedBy(post.getModifiedBy()).createdDate(post.getCreatedDate())
        .modifiedDate(post.getModifiedDate()).build();
  }

  @Override
  public PostDto delete(Long postId) {
    Optional<Post> optPost = postRepository.findByIdAndDeletedIsFalse(postId);
    if (optPost.isEmpty()) {
      throw new ValidationException("Post not found.");
    }
    Post post = optPost.get();
    post.setDeleted(true);
    post = postRepository.save(post);
    return PostDto.builder().title(post.getTitle()).postId(post.getId()).body(post.getBody())
        .path(post.getPath()).deleted(post.isDeleted()).createdBy(post.getCreatedBy())
        .modifiedBy(post.getModifiedBy()).createdDate(post.getCreatedDate())
        .modifiedDate(post.getModifiedDate()).build();
  }
}

Create Web Controllers

Create a controller class named UserController that will handle HTTP requests and interact with the UserService:

package com.example.user.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
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;
import com.example.user.dto.LoginDto;
import com.example.user.dto.SignUpDto;
import com.example.user.service.UserService;

@RestController
@RequestMapping(path = "/users")
public class UserController {
  @Autowired
  private UserService userService;

  @PostMapping(path = "/signup", consumes = {MediaType.APPLICATION_JSON_VALUE},
      produces = {MediaType.APPLICATION_JSON_VALUE})
  public ResponseEntity<SignUpDto> signUp(@RequestBody SignUpDto signUpRequest) {
    return ResponseEntity.ok(userService.signUp(signUpRequest));
  }

  @PostMapping(path = "/login", consumes = {MediaType.APPLICATION_JSON_VALUE})
  public ResponseEntity<LoginDto> login(@RequestBody LoginDto loginDto) {
    return ResponseEntity.ok(userService.login(loginDto));
  }
}

Create a controller class named PostController that will handle HTTP requests and interact with the PostService:

package com.example.post.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.data.web.SortDefault;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.example.auth.service.CustomUserDetails;
import com.example.post.dto.PostDto;
import com.example.post.service.PostService;
import com.example.user.util.UserUtil;

@RequestMapping(path = "/api/posts")
@RestController
public class PostController {
  @Autowired
  private PostService postService;

  @PreAuthorize("hasAuthority('USER')")
  @PostMapping(path = "/create", consumes = {MediaType.APPLICATION_JSON_VALUE})
  public ResponseEntity<PostDto> create(@RequestBody PostDto postDto) {
    CustomUserDetails loggedInUser = UserUtil.getCurrentLoggedInUser();
    postDto.setCreatedBy(loggedInUser.getId());
    return ResponseEntity.ok(postService.create(postDto));
  }

  @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')")
  @GetMapping
  public ResponseEntity<Page<PostDto>> getPosts(
      @PageableDefault(page = 0, size = 30) @SortDefault.SortDefaults({
          @SortDefault(sort = "created", direction = Sort.Direction.DESC)}) Pageable pageable) {
    CustomUserDetails loggedInUser = UserUtil.getCurrentLoggedInUser();
    return ResponseEntity.ok(postService.getPostsByUserId(loggedInUser.getId(), pageable));
  }

  @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')")
  @GetMapping(path = "/{postId}")
  public ResponseEntity<PostDto> getPostById(@PathVariable(name = "postId") Long postId) {
    return ResponseEntity.ok(postService.getPostById(postId));
  }

  @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')")
  @GetMapping(path = "/users/{userId}")
  public ResponseEntity<Page<PostDto>> getPostsByUserId(@PathVariable(name = "userId") Long userId,
      @PageableDefault(page = 0, size = 30) @SortDefault.SortDefaults({
          @SortDefault(sort = "created", direction = Sort.Direction.DESC)}) Pageable pageable) {
    return ResponseEntity.ok(postService.getPostsByUserId(userId, pageable));
  }

  @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')")
  @PutMapping(path = "/update/{postId}", consumes = {MediaType.APPLICATION_JSON_VALUE})
  public ResponseEntity<PostDto> update(@PathVariable(name = "postId", required = true) Long postId,
      @RequestBody PostDto postDto) {
    return ResponseEntity.ok(postService.update(postId, postDto));
  }

  @PreAuthorize("hasAuthority('ADMIN')")
  @DeleteMapping(path = "/delete/{postId}")
  public ResponseEntity<PostDto> delete(@PathVariable(name = "postId") Long postId) {
    return ResponseEntity.ok(postService.delete(postId));
  }
}

Configure JPA Auditing

Annotate the main class with @EnableJpaAuditing annotation. It is used to enable auditing in JPA entities. Auditing allows automatic population of specific fields such as createdDate and modifiedDate in JPA entities based on the current date and time. It is commonly used to keep track of when an entity was created or last modified:

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing
@SpringBootApplication
public class ExampleJwtAuthenticationApplication {

	public static void main(String[] args) {
		SpringApplication.run(ExampleJwtAuthenticationApplication.class, args);
	}

}

Build and Run your Application

Test your Spring Boot application's endpoints by using API testing tools such as Postman:

  1. Create a new User:
  2. Login to get the JWT access token:
  3. Create a new post using the JWT access token. Include an "Authorization" header in your request. Navigate to the Headers tab and enter "Authorization" as the key. In the value field, input "Bearer YOUR_TOKEN" (replace YOUR_TOKEN with the actual bearer token you receive after a successful login). For example: