REST API Security with Spring Security JWT Token Signing
Securing REST APIs is important in today’s web development, especially with microservices becoming more common. A popular way to do this is with JSON Web Tokens (JWT). Spring Security helps with JWT-based authentication and authorization in Spring applications. In this article, we’ll see how to create a Spring Security key for signing JWT tokens and use it in a Spring Boot app to secure REST APIs.
1. Set up a Spring Boot Application
Let’s begin by creating a new Spring Boot application by either using Spring Initializr (https://start.spring.io/) or your preferred IDE to create a new project with the necessary dependencies:
- Spring Web
- Spring Security
2. Add JSON Web Token Dependencies
In the pom.xml
file of the project (if using Maven), add the following dependencies for JWT token handling:
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.12.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.12.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.12.5</version> <scope>runtime</scope> </dependency>
3. Create JWT Utility Class
Next, create a utility class for handling JWT operations. This class will be responsible for generating JWT tokens and verifying them. Below is a simple implementation of the JWT utility class:
@Component public class JwtUtil { @Value("${jcg.jwt.secret}") private String jwtSecret; public String generateToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(); claims.put("Authorities", userDetails.getAuthorities()); return Jwts.builder() .setClaims(claims) .setSubject(userDetails.getUsername()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + 86400)) .signWith(getSignInKey(), SignatureAlgorithm.HS256) .compact(); } public String extractUsername(String token) { return extractClaim(token, Claims::getSubject); } 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.parser() .setSigningKey(getSignInKey()) .build() .parseClaimsJws(token) .getBody(); } public boolean validateToken(String token, UserDetails userDetails) { final String username = extractUsername(token); return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); } private boolean isTokenExpired(String token) { return extractExpiration(token).before(new Date()); } private Date extractExpiration(String token) { return extractClaim(token, Claims::getExpiration); } private SecretKey getSignInKey() { byte[] keyBytes = Decoders.BASE64.decode(jwtSecret); return Keys.hmacShaKeyFor(keyBytes); } }
The code block above represents a class named JwtUtil
which is responsible for handling JWT (JSON Web Token) operations within our application. Here is a break down its functionalities:
- The class injects the JWT secret key from the
application.properties
file using the@Value
annotation. - Token Generation: The
generateToken
method creates a JWT token based on the providedUserDetails
. It sets the subject (username), issue date, expiration date, and signs the token using the HMAC SHA-256 algorithm with the secret key. extractUsername
Method: This method takes a JWT token as input and returns the username extracted from the token’s subject claim. It delegates the claim extraction process to theextractClaim
method, passing in the token and a function reference (Claims::getSubject
) to extract the subject claim.extractClaim
Method: This generic method takes a JWT token and aFunction
that resolves a specific claim from the token’sClaims
object. It first extracts all claims from the token by invoking theextractAllClaims
method. Then, it applies the providedclaimsResolver
function to theClaims
object to extract the desired claim.extractAllClaims
Method: This method is responsible for parsing the JWT token, verifying its signature using the provided signing key, and extracting all claims from the token’s body. It uses theJwts.parser()
method to create a parser instance, sets the signing key withsetSigningKey(getSignInKey())
, and then parses the token withparseClaimsJws(token)
.- Secret Key Retrieval: The
getSignInKey
method retrieves the secret key for signing and verifying JWT tokens. It decodes the base64-encoded secret key obtained from theapplication.properties
file and converts it into aSecretKey
object.
3.1 Generate and Set JWT Secret Key
Producing a strong secret key is essential to ensure the security of JWT tokens. This entails generating an HMAC hash string consisting of 256 bits and configuring it as the JWT secret within the application.properties
file. The online tool generator located at devglan.com/online-tools can generate an HMAC hash string of 256 bits.
3.1.1 Set JWT Secret in application.properties
Once the secret key is generated, it needs to be set in the application.properties
file of the Spring Boot application:
jcg.jwt.secret=YOUR_GENERATED_SECRET_KEY
Replace YOUR_GENERATED_SECRET_KEY
with the generated HMAC hash string obtained earlier.
3.2 Implementing Authentication Token Filter
Next, let’s implement an Authentication Token Filter which is crucial for securing REST endpoints with JWT tokens. Below is an example of an authentication token filter:
@Component public class JwtRequestFilter extends OncePerRequestFilter { @Autowired private JwtUtil jwtUtil; @Autowired private UserDetailsService userDetailsService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { final String authorizationHeader = request.getHeader("Authorization"); String username = null; String jwt = null; if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { jwt = authorizationHeader.substring(7); username = jwtUtil.extractUsername(jwt); } if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); if (jwtUtil.validateToken(jwt, userDetails)) { UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); usernamePasswordAuthenticationToken .setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); } } filterChain.doFilter(request, response); } }
This filter intercepts incoming requests, extracts JWT tokens from the request header, validates them, and sets up authentication in the Spring Security context if the token is valid.
4. Configure Spring Security
Next, configure Spring Security to use JWT for authentication. Create a SecurityConfig
class and configure it as follows:
@Configuration @EnableWebSecurity public class SecurityConfig { private static final String ADMIN = "ADMIN"; private static final String USER = "USER"; @Bean public JwtRequestFilter jwtRequestFilter() { return new JwtRequestFilter(); } @Bean public DaoAuthenticationProvider authenticationProvider() throws Exception { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); authProvider.setUserDetailsService(userDetailsService()); authProvider.setPasswordEncoder(passwordEncoder()); return authProvider; } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception { http.csrf(AbstractHttpConfigurer::disable) .cors(AbstractHttpConfigurer::disable) .authorizeHttpRequests(req -> req .requestMatchers("/admin/**").hasRole(ADMIN) .requestMatchers("/user/**").hasAnyRole(USER, ADMIN) .requestMatchers("/authenticate") .permitAll() .anyRequest() .authenticated()) .sessionManagement(session -> session.sessionCreationPolicy(STATELESS)) .authenticationProvider(authenticationProvider()) .addFilterBefore(jwtRequestFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { return authConfig.getAuthenticationManager(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public UserDetailsService userDetailsService() throws Exception { InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); manager.createUser(User .withUsername("thomas") .password(encoder().encode("paine")) .roles(ADMIN).build()); manager.createUser(User .withUsername("bill") .password(encoder().encode("withers")) .roles(USER).build()); return manager; } @Bean public PasswordEncoder encoder() { return new BCryptPasswordEncoder(); } @Bean public CorsFilter corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); config.addAllowedOrigin("*"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }
This code block configures security settings for the application using Spring Security. Let’s break down its functionalities:
- The
jwtRequestFilter
method defines a bean for theJwtRequestFilter
class. This filter intercepts incoming requests, extracts JWT tokens, and authenticates them. - DaoAuthenticationProvider Bean: The
authenticationProvider
method defines a bean for theDaoAuthenticationProvider
class. This provider authenticates users based on the provided user details service and password encoder. - SecurityFilterChain Bean: The
securityFilterChain
method sets up authorization rules based on request matchers and roles.- Authorization Rules: The
.authorizeHttpRequests()
method specifies authorization rules for different types of requests:- Requests to
/admin/**
endpoints require theADMIN
role. - Requests to
/user/**
endpoints require either theUSER
orADMIN
role. - Requests to
/authenticate
endpoint are permitted without authentication (permit all). - All other requests (
anyRequest()
) require authentication.
- Requests to
- Session Management: Session management is configured to be stateless (
sessionCreationPolicy(STATELESS)
), meaning no server-side session will be created or used for storing user authentication state.
- Authorization Rules: The
- AuthenticationManager Bean: The
authenticationManager
method defines a bean for theAuthenticationManager
interface. It retrieves the authentication manager from the authentication configuration. - UserDetailsService Bean: The
userDetailsService
method defines an in-memory user store usingInMemoryUserDetailsManager
. We create two users with different roles: anADMIN
user with username “thomas” and aUSER
user with username “bill“.
4.1 Custom UserDetails Implementation
Spring Security relies on the UserDetails
interface for both authentication and authorization purposes. Below is an implementation of the User
class which implements the UserDetails
interface for authentication and authorization:
public class User implements UserDetails { private int id; private String username; private String password; private Collection<? extends GrantedAuthority> authorities; public User() { } public User(String username, String password, Collection<? extends GrantedAuthority> authorities) { this.password = password; this.username = username; this.authorities = authorities; } public User(String username, Collection<String> authorities) { this.username = username; this.authorities = authorities.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()); } // UserDetails interface methods @Override public String getUsername() { return username; } @Override public String getPassword() { return password; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @Override public boolean isEnabled() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isAccountNonExpired() { return true; } }
The User
class implements the UserDetails
interface and represents a user in the application. It includes fields for username, password, and authorities. The UserDetails
interface is a core interface in Spring Security used for user authentication and authorization. It represents a principal (user) in the system and provides methods for accessing user details and authorities.
getAuthorities()
method returns the authorities (roles) granted to the user. getPassword()
and getUsername()
methods return the password and username of the user, respectively. isAccountNonExpired()
isAccountNonLocked()
, isCredentialsNonExpired()
, isEnabled()
methods return true
if the user account is not expired, not locked, credentials are not expired, and the user is enabled, respectively.
5. Integration with Spring Boot
Finally, Let’s integrate the JWT utility and Spring Security configurations with our Spring Boot application. Here’s a basic example of a REST controller for authentication:
@RestController public class AuthController { @Autowired private AuthenticationManager authenticationManager; @Autowired private JwtUtil jwtUtil; @Autowired private UserDetailsService userDetailsService; @PostMapping("/authenticate") public ResponseEntity createAuthenticationToken(@RequestBody AuthRequest authRequest) throws Exception { try { authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(authRequest.getUsername(), authRequest.getPassword()) ); } catch (BadCredentialsException e) { throw new Exception("Incorrect username or password", e); } final UserDetails userDetails = userDetailsService .loadUserByUsername(authRequest.getUsername()); final String jwt = jwtUtil.generateToken(userDetails); return ResponseEntity.ok(new AuthResponse(jwt)); } @GetMapping("/auth/details") public UserDetails getDetails(){ var detail = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); return detail; } }
This block of code defines an AuthController
class responsible for handling authentication-related HTTP requests in the application.
- The controller class autowires the dependencies (
AuthenticationManager
,JwtUtil
, andUserDetailsService
) required for authentication and token generation. - createAuthenticationToken Method: This method handles POST requests to
/authenticate
endpoint. It attempts to authenticate the user by passing the provided credentials to theAuthenticationManager
. If authentication is successful, it generates a JWT token usingJwtUtil
and returns it in the response body. - getDetails Method: This method handles GET requests to
/auth/details
endpoint. It retrieves the authenticated user’s details (such as username, authorities) from theSecurityContextHolder
.
5.1 Securing REST Endpoints
@RestController public class SimpleController { @RolesAllowed("ADMIN") @GetMapping("/admin") public ResponseEntity<String> testAdmin() { return ResponseEntity.ok("This is the Admin role"); } @RolesAllowed("USER") @GetMapping("/user") public ResponseEntity<String> testUser() { return ResponseEntity.ok("This is the User role"); } }
This SimpleController
class defines endpoints that are accessible only to users with specific roles. It enforces role-based access control (RBAC), ensuring that certain operations can only be performed by users with the appropriate roles.
- testAdmin Method: This method handles GET requests to the
/admin
endpoint. It is annotated with@RolesAllowed("ADMIN")
, which specifies that only users with the role “ADMIN” are allowed to access this endpoint. - testUser Method: This method handles GET requests to the
/user
endpoint. Similar to thetestAdmin
method, it is annotated with@RolesAllowed("USER")
, indicating that only users with the role “USER” can access this endpoint.
To verify the functionality of the application, we can utilize POSTMAN to send a request to http://localhost:8080/authenticate and acquire a JWT token.
Next, proceed by setting the request header with the JWT key and verify if http://localhost:8080/auth/details functions correctly.
Now, let’s verify if we can access the http://localhost:8080/user endpoint:
If a user with the USER role attempts to access the /admin
endpoint, Spring Security will deny the request, and the user will receive an HTTP status code indicating access forbidden (403).
6. Conclusion
In this article, we’ve learned how to generate a key to sign JWT tokens and include it in a Spring Boot application for security. We also saw how to secure specific endpoints based on user roles. By using Spring Security to sign a JWT token, we greatly improve the security of our application.
7. Download the Source Code
This was an article on how to create a spring security key to sign JWT Token to Secure REST APIs.
You can download the full source code of this example here: Create Spring Security Key to Sign JWT Token to Secure REST APIs