Enterprise Java

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
Fig 1: Generate a spring boot project with the dependencies (spring security and spring web) for jthe wt token example
Fig 1: Generate a spring boot project with the dependencies (spring security and spring web) for the jwt token example

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 provided UserDetails. 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 the extractClaim 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 a Function that resolves a specific claim from the token’s Claims object. It first extracts all claims from the token by invoking the extractAllClaims method. Then, it applies the provided claimsResolver function to the Claims 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 the Jwts.parser() method to create a parser instance, sets the signing key with setSigningKey(getSignInKey()), and then parses the token with parseClaimsJws(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 the application.properties file and converts it into a SecretKey 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 the JwtRequestFilter class. This filter intercepts incoming requests, extracts JWT tokens, and authenticates them.
  • DaoAuthenticationProvider Bean: The authenticationProvider method defines a bean for the DaoAuthenticationProvider 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 the ADMIN role.
      • Requests to /user/** endpoints require either the USER or ADMIN role.
      • Requests to /authenticate endpoint are permitted without authentication (permit all).
      • All other requests (anyRequest()) require authentication.
    • 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.
  • AuthenticationManager Bean: The authenticationManager method defines a bean for the AuthenticationManager interface. It retrieves the authentication manager from the authentication configuration.
  • UserDetailsService Bean: The userDetailsService method defines an in-memory user store using InMemoryUserDetailsManager. We create two users with different roles: an ADMIN user with username “thomas” and a USER 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, and UserDetailsService) 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 the AuthenticationManager. If authentication is successful, it generates a JWT token using JwtUtil 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 the SecurityContextHolder.

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 the testAdmin 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.

Download
You can download the full source code of this example here: Create Spring Security Key to Sign JWT Token to Secure REST APIs

Omozegie Aziegbe

Omos Aziegbe is a technical writer and web/application developer with a BSc in Computer Science and Software Engineering from the University of Bedfordshire. Specializing in Java enterprise applications with the Jakarta EE framework, Omos also works with HTML5, CSS, and JavaScript for web development. As a freelance web developer, Omos combines technical expertise with research and writing on topics such as software engineering, programming, web application development, computer science, and technology.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button