Spring Boot | JWT | Spring Security

Spring Boot 3 + Spring Security 6: JWT Authentication & Authorization

Zeeshan Adil
JavaToDev
Published in
13 min readOct 29, 2023

--

you’ll learn how to implement JWT authentication and authorization in a Spring Boot 3.0 application using Spring Security 6 You’ll see how easy it is to secure your application and protect your endpoints using JSON Web Tokens Step-by-step guides.

What is JWT ?

JWT, or JSON Web Token, is a compact, self-contained means of representing claims to be transferred between two parties securely. In the context of authentication and authorization, JWT is often used to authenticate users and to grant them access to resources or services. Here’s a breakdown of how JWT works in authentication and authorization:

1. Authentication
JWT can be used for authentication by issuing a token to a user upon successful login. The token contains information about the user, typically in the form of claims. These claims may include the user’s ID, username, role, or any other relevant information. The process typically involves the following steps:

- User logs in with their credentials (e.g., username and password).
- The server validates the user’s credentials.
- Upon successful authentication, the server generates a JWT containing user claims.
- The JWT is then sent back to the user’s client (e.g., a web browser or mobile app).
- The client stores the JWT securely, often in a cookie or local storage.

2. Authorization:
JWT is also used for authorization, allowing the server to determine what a user is allowed to access. The claims within the JWT can specify the user’s roles, permissions, or other authorization information. The authorization process typically involves the following steps:

- The user’s client makes a request to a protected resource on the server.
- The server receives the request and checks for the presence of a valid JWT in the request headers.
- If a valid JWT is present, the server can read the claims within the token to determine the user’s identity and roles.
- Based on the user’s claims, the server can decide whether the user has the necessary permissions to access the requested resource.
- If the user is authorized, the server allows access to the resource; otherwise, it denies access.

JWTs are advantageous for authentication and authorization because they are self-contained and stateless. This means that the server doesn’t need to store user sessions, making it easier to scale and maintain. Additionally, JWTs can be digitally signed to ensure their integrity and authenticity, preventing tampering by malicious actors.

However, it’s crucial to handle JWTs securely, protect against token theft, and use proper encryption and validation mechanisms to ensure their reliability in authentication and authorization processes.

Let’s start coding :)

First, we need below maven dependencies in pom.xml:


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</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>
<!-- https://mvnrepository.com/artifact/org.modelmapper/modelmapper -->
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>3.1.1</version>
</dependency>

<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-jackson -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
</dependency>


Create a JwtService class as below:

@Component
public class JwtService {

public static final String SECRET = "357638792F423F4428472B4B6250655368566D597133743677397A2443264629";

public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}

public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}

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

private Claims extractAllClaims(String token) {
return Jwts
.parserBuilder()
.setSigningKey(getSignKey())
.build()
.parseClaimsJws(token)
.getBody();
}

private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}

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



public String GenerateToken(String username){
Map<String, Object> claims = new HashMap<>();
return createToken(claims, username);
}



private String createToken(Map<String, Object> claims, String username) {

return Jwts.builder()
.setClaims(claims)
.setSubject(username)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis()+1000*60*1))
.signWith(getSignKey(), SignatureAlgorithm.HS256).compact();
}

private Key getSignKey() {
byte[] keyBytes = Decoders.BASE64.decode(SECRET);
return Keys.hmacShaKeyFor(keyBytes);
}
}

In the context of using JSON Web Tokens (JWT) for authentication and authorization, the SECRET_KEY is a secret cryptographic key used to sign and verify JWTs. It's a piece of information known only to the server that generates and validates the tokens. This key is crucial for ensuring the integrity and authenticity of JWTs.

The `JwtService` class is a component responsible for various operations related to JWT (JSON Web Tokens) in a Spring Boot application. It contains methods for generating, parsing, and validating JWT tokens. Let’s go through each method and explain its purpose:

1. public String extractUsername(String token):
— This method takes a JWT token as input and extracts the subject (usually the username) from the token’s claims. It uses the `extractClaim` method to extract the subject claim.

2. public Date extractExpiration(String token):
— This method extracts the expiration date from the JWT token’s claims. It’s used to determine whether the token has expired or not.

3. public <T> T extractClaim(String token, Function<Claims, T> claimsResolver):
— This is a generic method used to extract a specific claim from the JWT token’s claims. It takes a JWT token and a `Function` that specifies how to extract the desired claim (e.g., subject or expiration) and returns the extracted claim.

4. private Claims extractAllClaims(String token):
— This method parses the JWT token and extracts all of its claims. It uses the `Jwts` builder to create a parser that is configured with the appropriate signing key and then extracts the token’s claims.

5. private Boolean isTokenExpired(String token):
— This method checks whether a JWT token has expired by comparing the token’s expiration date (obtained using `extractExpiration`) to the current date. If the token has expired, it returns `true`; otherwise, it returns `false`.

6. public Boolean validateToken(String token, UserDetails userDetails):
— This method is used to validate a JWT token. It first extracts the username from the token and then checks whether it matches the username of the provided `UserDetails` object. Additionally, it verifies whether the token has expired. If the token is valid, it returns `true`; otherwise, it returns `false`.

7. public String GenerateToken(String username):
— This method is used to generate a JWT token. It takes a username as input, creates a set of claims (e.g., subject, issued-at, expiration), and then builds a JWT token using the claims and the signing key. The resulting token is returned.

8. private String createToken(Map<String, Object> claims, String username):
— This method is responsible for creating the JWT token. It uses the `Jwts` builder to specify the claims, subject, issue date, expiration date, and the signing key. The token is then signed and compacted to produce the final JWT token, which is returned.

9. private Key getSignKey():
— This method is used to obtain the signing key for JWT token creation and validation. It decodes the `SECRET` key, which is typically a Base64-encoded key, and converts it into a cryptographic key using the `Keys.hmacShaKeyFor` method.

The `SECRET` key appears to be a hard-coded secret key used for signing and verifying JWT tokens. It’s important to ensure the security of this key and to consider more secure methods for managing it, such as using environment variables or a dedicated secret management service in a production environment.

Create User Model Class as below:

@Entity
@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "USERS")
public class UserInfo {

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "ID")
private long id;
private String username;
@JsonIgnore
private String password;
@ManyToMany(fetch = FetchType.EAGER)
private Set<UserRole> roles = new HashSet<>();


}

Create UserRepository as below:

@Repository
public interface UserRepository extends CrudRepository<UserInfo, Long> {
public UserInfo findByUsername(String username);
}

Create UserRole model as below:

@Entity
@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "ROLES")
public class UserRole {

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "ID")
private long id;
private String name;

}

Create UserDetailsServiceImpl class implement it with UserDetailsService provided by spring security:

@Component
public class UserDetailsServiceImpl implements UserDetailsService {

@Autowired
private UserRepository userRepository;

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

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

logger.debug("Entering in loadUserByUsername Method...");
UserInfo user = userRepository.findByUsername(username);
if(user == null){
logger.error("Username not found: " + username);
throw new UsernameNotFoundException("could not found user..!!");
}
logger.info("User Authenticated Successfully..!!!");
return new CustomUserDetails(user);
}
}

Above code defines a UserDetailsServiceImpl class, which implements the UserDetailsService interface in a Spring Boot application. This class is responsible for loading user details and creating a UserDetails object for authentication purposes. Let's break down the code and its functionality:

loadUserByUsername Method:

  • The loadUserByUsername method is an implementation of the loadUserByUsername method defined in the UserDetailsService interface. This method is called by Spring Security when it needs to retrieve user details for authentication.
  • When a user attempts to log in, they provide a username (or other unique identifier). The loadUserByUsername method is responsible for looking up the user in the user repository based on this provided username.
  • If the user is not found in the database, the method logs an error and throws a UsernameNotFoundException. This exception is a standard exception in Spring Security and indicates that the requested user was not found.
  • If the user is found, the method logs a successful authentication message and creates a CustomUserDetails object. CustomUserDetails is typically a custom implementation of the UserDetails interface that wraps the user information, such as username and password, as well as user roles and authorities.
  • The UserDetails object (in this case, CustomUserDetails) is returned by the method. This object is used by Spring Security for authentication and authorization checks.

Create CustomUserDetails class:

public class CustomUserDetails extends UserInfo implements UserDetails {

private String username;
private String password;
Collection<? extends GrantedAuthority> authorities;

public CustomUserDetails(UserInfo byUsername) {
this.username = byUsername.getUsername();
this.password= byUsername.getPassword();
List<GrantedAuthority> auths = new ArrayList<>();

for(UserRole role : byUsername.getRoles()){

auths.add(new SimpleGrantedAuthority(role.getName().toUpperCase()));
}
this.authorities = auths;
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}

@Override
public String getPassword() {
return password;
}

@Override
public String getUsername() {
return username;
}

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

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

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

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

This class is a custom implementation of the UserDetails interface, used for representing user details and authorities (roles) for authentication and authorization purposes.

Create JwtAuthFilter:

@Component
public class JwtAuthFilter extends OncePerRequestFilter {

@Autowired
private JwtService jwtService;

@Autowired
UserDetailsServiceImpl userDetailsServiceImpl;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

String authHeader = request.getHeader("Authorization");
String token = null;
String username = null;
if(authHeader != null && authHeader.startsWith("Bearer ")){
token = authHeader.substring(7);
username = jwtService.extractUsername(token);
}

if(username != null && SecurityContextHolder.getContext().getAuthentication() == null){
UserDetails userDetails = userDetailsServiceImpl.loadUserByUsername(username);
if(jwtService.validateToken(token, userDetails)){
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}

}

filterChain.doFilter(request, response);
}
}

In our custom `JwtAuthFilter`, we’ve overridden the `doFilterInternal` method, which gets invoked for every request to our application. This method is responsible for processing incoming requests by inspecting the “Authorization” header to identify and validate a Bearer token. Here’s how it works:

1. The filter begins by checking if there’s an “Authorization” header in the request, and if it contains a Bearer token. Requests that are not related to user login typically do not have a JWT token in their headers, so they pass through to the next filter chain without any token-related processing.

2. If the “Authorization” header is found and it starts with “Bearer,” indicating the presence of an access token, the filter proceeds to validate and authenticate this token.

3. It starts by extracting the access token from the header.

4. Then, it validates this token using the provided `JwtService`, ensuring it’s a valid and unexpired token.

5. If the token is valid, the filter authenticates the request by creating an `Authentication` object. This object represents the user’s authentication status and contains information about the user, such as their username and authorities.

6. The authenticated user is then stored in the `SecurityContext`, ensuring they have access to protected resources in the application.

In essence, the `JwtAuthFilter` intercepts requests, looks for Bearer tokens, validates them, and authenticates users if the token is valid. Requests without a valid Bearer token continue through the filter chain, while authenticated users gain access to protected resources.

Create SecurityConfig class :

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

@Autowired
JwtAuthFilter jwtAuthFilter;

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

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.csrf().disable()
.authorizeHttpRequests()
.requestMatchers("/api/v1/login").permitAll()
.and()
.authorizeHttpRequests().requestMatchers("/api/v1/**")
.authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authenticationProvider(authenticationProvider())
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class).build();

}

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

@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService());
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;

}

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

It defines the security settings, authentication, and authorization rules for the application.
securityFilterChain() Bean:

  • This method defines the security filter chain for the application. The filter chain specifies various security configurations.
  • It disables Cross-Site Request Forgery (CSRF) protection with http.csrf().disable().
  • It configures authorization rules using authorizeHttpRequests() to permit public access to specific endpoints (e.g. "/api/v1/login"). Requests to these endpoints are allowed without authentication.
  • For other endpoints under “/api/v1/**,” it requires authentication, meaning users must be authenticated to access these resources.
  • The session management is set to SessionCreationPolicy.STATELESS, indicating that the application will be stateless, and sessions are not used for user tracking. This is common in JWT-based authentication.
  • It sets an AuthenticationProvider for authentication, which likely uses the DaoAuthenticationProvider. This provider specifies the user details service and password encoder for authentication.
  • It adds the JwtAuthFilter before the UsernamePasswordAuthenticationFilter. This filter is responsible for processing JWT tokens and authenticating users.

Now create request response DTOs for login API as below:

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class AuthRequestDTO {

private String username;
private String password;
}

---------------------------

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class JwtResponseDTO {
private String accessToken;

}

Create an AuthController:

We will create a /api/v1/login rest end point in AuthController which take AuthRequestDTO (username and password) as request body and call the jwtService to generate the token.

@PostMapping("/api/v1/login")
public JwtResponseDTO AuthenticateAndGetToken(@RequestBody AuthRequestDTO authRequestDTO){
Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(authRequestDTO.getUsername(), authRequestDTO.getPassword()));
if(authentication.isAuthenticated()){
return JwtResponseDTO.builder()
.accessToken(jwtService.GenerateToken(authRequestDTO.getUsername()).build();
} else {
throw new UsernameNotFoundException("invalid user request..!!");
}
}

Enable @PreAuthorize Annotation:

To enable @PreAuthorize and also @PostAuthorize annotations in your Spring Boot application you will need to first enable the Global Method Security. To enable the Global Method Security, add the @EnableWebSecurity annotation to SecurityConfig java class.

The @PreAuthorize annotation is applied at the method level. For instance, you can place the @PreAuthorize annotation above a method annotated with @RequestMapping that handles HTTP requests. This allows only users with an ADMIN role to invoke the method.

The @PreAuthorize annotation enables the use of method security expressions. The actual business logic within a method annotated with @PreAuthorize will not be executed unless the security expression validation permits it. Let's explore a couple of security expressions to illustrate this.

HasRole()

When using the hasRole() security expression, the prefix ROLE_ is skipped. This is because Spring Framework will add the prefix automatically for us.

@PreAuthorize(“hasRole(‘ADMIN’)”)

HasAnyRole()

When you need to support multiple roles, you can use the hasAnyRole() expression.

@PreAuthorize(“hasAnyRole(‘ADMIN’,’ SUPER_USER’)”)

Let’s expose a ping API as api/v1/ping which will return a string when authentication done. This API will only work for the user which has ADMIN role otherwise will throw FORBIDDEN exception.

@PreAuthorize("hasAuthority('ADMIN')")
@GetMapping("/ping")
public String test() {
try {
return "Welcome";
} catch (Exception e){
throw new RuntimeException(e);
}
}

I’ve used MySQL db for demo purpose. Put below configuration in application.properties file.

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/jwt_auth_db
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=create
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL8Dialect
server.port=9898

We have almost done with coding. Let’s test it now.
I’ve manually added a user with the username “username123” and the password “test2day” in the system. I didn’t create an API endpoint for user registration because my primary focus was on implementing JWT (JSON Web Token) functionality. Instead, I directly inserted the user into the database.

As a result, I can now demonstrate that an access token is generated, which remains valid for a duration of 60 minutes.

Let’s hit the ping API as api/v1/ping which will return a string when authentication done.

See below when JWT token is not passed a Bearer token, we are getting 403 error code that’s mean FORBIDDEN.

Let’s pass the JWT as bearer token. See request get authenticated and returned the successful response.

✅You can find the source code here…

Next to Learn 👇

JWT Refresh Token : Spring Security

Invalidate/Revoked the JWT : Force logout the user from spring security

Cookie-based JWT Authentication with Spring Security

From Localhost to the Cloud: Deploying Spring Boot + MySQL App on Kubernetes with Docker Desktop — A Beginner Guide

We’ve just observed a concise showcase of Spring Security in action, and I hope it’s operating as anticipated, everyone!

Is Spring Security a formidable challenge? Indeed! Understanding its underlying mechanisms can be quite intricate, especially in the initial stages. It’s important to keep in mind that there are many individuals ready to support each other, so please don’t hesitate to express your thoughts or questions in the comments section.

❤️ Support & Engagement ❤️

❤ If you find this article informative and beneficial, please consider showing your appreciation by giving it a clap 👏👏👏, highlight it and replying on my story story. Feel free to share this article with your peers. Your support and knowledge sharing within the developer community are highly valued.
Please share on social media
Follow me on : Medium || LinkedIn
Check out my other articles on Medium
Subscribe to my newsletter 📧, so that you don’t miss out on my latest articles.
❤ If you enjoyed my article, please consider buying me a coffee ❤️ and stay tuned to more articles about java, technologies and AI. 🧑‍💻

--

--

Zeeshan Adil
JavaToDev

Full Stack Developer || Educator || Technical Blogger 🧑‍💻Let's Connect : https://www.linkedin.com/in/zeeshan-adil-a94b3867/