JSON Web Token (JWT) pronounced “jat” is an authentication and authorization widely used in modern apps. In this mechanism, when the user logs in with their credentials, the server generates a unique key called the JWT token that it sends to the client. The client would then include the JWT token in the header when it sends a request to a protected endpoint on the server. The server would then verify the JWT token before sending a response to the client. In this tutorial, you will see code samples for how to add JWT security to Java Spring Boot API. The code samples provided here are from a sample project on Github, the link for which will be included ion this post. Before starting with the code samples, it would be beneficial to understand JWT in a bit more.
What is JWT and how does it work?
JWT is an open standard for secure communication over the network. It facilitates information exchange between two parties as a JSON object, hence it is also lightweight data exchange. The JWT token is composed of three parts,
- Header: contains the type of token and the signing algorithm e.g. HMAC using SHA-256 that uses a secret key for both signing and verification
- Payload: Contains the claims about the user e.g. iat (issued at), ext or name
- Signature: the signature is creating by encoding both the header and payload and signing them using a secret key (or key pair)
Now, that you understand the Let’s say you have a web app with protected endpoints that are only accessible to authorized users. To access the protected endpoint, the user will do the following,
- User logs in with their credentials
- The server verifies the credentials
- The server generates a JWT token using the secret key for signing and sends it to the client
- The client app (e.g. web app) would save the token in a cookie or localStorage
- Then client app includes the JWT token in subsequent requests as bearer token in header
- The server would verify the JWT token signature to ensure it’s not tampered with
- If the JWT token is valid the server would then allow access to the protected resource
Add JWT security to Java Spring Boot API
First, start by adding the following dependencies in your pom.xml
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<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>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
</dependency>
Add the secret signing key in your application.properties
jwt.secret=e813c7608a85d2ac2012f0b8ba4528cb60235837547318a4f3274d98bada9b5f
Now add a JWTService which will use the secret signing key that you added above
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Component
@Slf4j
public class JwtService {
@Value("${jwt.secret}")
private String SECRET;
public String generateToken(String userName) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, userName);
}
private String createToken(Map<String, Object> claims, String userName) {
Date issuedDate = new Date();
log.info("Printing issued date {}", issuedDate.toString());
Date expiryDate = Date.from(LocalDate.now().plusDays(1L).atStartOfDay(ZoneId.systemDefault()).toInstant());
log.info("Expiry date is {}", expiryDate.toString());
return Jwts.builder()
.setClaims(claims)
.setSubject(userName)
.setIssuedAt(issuedDate)
.setExpiration(expiryDate)
.signWith(getSignKey(), SignatureAlgorithm.HS256).compact();
}
private Key getSignKey() {
byte[] keyBytes= Decoders.BASE64.decode(SECRET);
return Keys.hmacShaKeyFor(keyBytes);
}
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);
}
public String extractEmail(String token) {
final Claims claim = extractAllClaims(token);
return String.valueOf(claim.get("email"));
}
private Claims extractAllClaims(String token) {
return Jwts
.parserBuilder()
.setSigningKey(getSignKey())
.build()
.parseClaimsJws(token)
.getBody();
}
private Boolean isTokenExpired(String token) {
Date expirationDate = extractExpiration(token);
log.info("The date {} is", expirationDate.toString());
return expirationDate.before(new Date());
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}
As mentioned before, the client web app will include the JWT token in every request header that the server verifies. To do that in a Spring Boot API you need to use the OncePerRequestFilter which will use the JWTService to verify the token and determine whether or not the client is authorised
import com.mydaytodo.auth.methods.service.JwtService;
import com.mydaytodo.auth.methods.service.UserInfoService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
// This class helps us to validate the generated jwt token
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
@Autowired
private JwtService jwtService;
@Autowired
private UserInfoService userDetailsService;
@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 = userDetailsService.loadUserByUsername(username);
if (jwtService.validateToken(token, userDetails)) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}
Great, you have everything setup now, all that’s left is to ensure the right security mechanisms are triggered by your Spring Boot API. You can do this by defining the Spring Config as follows,
import com.mydaytodo.auth.methods.jwt.JwtAuthFilter;
import com.mydaytodo.auth.methods.service.UserInfoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
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.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Autowired
private JwtAuthFilter authFilter;
// User Creation
@Bean
public UserDetailsService userDetailsService() {
return new UserInfoService();
}
// Configuring HttpSecurity
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth ->
auth.requestMatchers("/api/auth/welcome",
"/api/auth/register",
"/api/auth/authenticate").permitAll()
.anyRequest()
.authenticated()
)
.sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authenticationProvider(authenticationProvider())
.addFilterBefore(authFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
// Password Encoding
@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();
}
}
As you can see on line 40 in the above code, you are only permitting the user to access three endpoints without authentication. Now, let’s define some API endpoints that let the users register and login,
import com.mydaytodo.auth.methods.model.AuthRequest;
import com.mydaytodo.auth.methods.service.JwtService;
import com.mydaytodo.auth.methods.service.UserInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.web.bind.annotation.*;
/**
* Used
* https://www.javainuse.com/spring/boot-jwt
* https://www.javainuse.com/spring/jwt
* https://www.geeksforgeeks.org/spring-boot-3-0-jwt-authentication-with-spring-security-using-mysql-database/
* and Meta AI to learn about implementing JWT in Spring Boot
*/
@RestController
@Slf4j
@RequestMapping("/api/auth")
public class JwtAuthenticationController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtService jwtService;
@Autowired
private UserInfoService userInfoService;
@GetMapping("/welcome")
public ResponseEntity<String> greet() throws Exception {
return new ResponseEntity<>("Welcome to JWT Auth", HttpStatus.OK);
}
/**
* Endpoint to register new users
* 1. validate the request and body
* 2. insert the user
* @param authRequest
* @return
*/
@PostMapping("/register")
public ResponseEntity<User> registerUser(@RequestBody AuthRequest authRequest) {
log.info("Go a request to register user {}", authRequest.toString());
User user = userInfoService.addUser(authRequest);
log.info("successfully added user {}", user.toString());
return new ResponseEntity<>(user, HttpStatus.OK);
}
@PostMapping("/authenticate")
public String authenticateAndGetToken(@RequestBody AuthRequest authRequest) {
log.info("Called generateToken API and about to generate authenticate via authentication manager");
Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(authRequest.getUsername(), authRequest.getPassword()));
log.info("Auth object {}", Boolean.toString(authentication.isAuthenticated()));
if (authentication.isAuthenticated()) {
log.info("Authentication successful");
return jwtService.generateToken(authRequest.getUsername());
} else {
log.info("Authentication NOT successful");
throw new UsernameNotFoundException("invalid user request");
}
}
}
Conclusion
Hope you found this post useful and feel more confident about adding JWT security to your Spring Boot API. You can find the full source code for a Spring Boot API with JWT security in this Github repo.
If you find any of my posts useful and want to support me, you can buy me a coffee 🙂
https://www.buymeacoffee.com/bhumansoni
While you are here, maybe try one of my apps for the iPhone.
Products – My Day To-Do (mydaytodo.com)
Here are some of my other bloposts on Java
How to build a full stack Spring boot API with ReactJS frontend – My Day To-Do (mydaytodo.com)
How to call REST API with WebClient – My Day To-Do (mydaytodo.com)
How to build a jokes client in Java Spring Boot with RestTemplate – My Day To-Do (mydaytodo.com)
Have a read of some of my other posts on AWS
Upload to AWS S3 bucket from Java Spring Boot app – My Day To-Do (mydaytodo.com)
Deploy NodeJS, Typescript app on AWS Elastic beanstalk – (mydaytodo.com)
How to deploy spring boot app to AWS & serve via https – My Day To-Do (mydaytodo.com)
0 Comments