Securing REST APIs with Spring Security and JWT

Methuselah Nwodobeh
Methuselah Nwodobeh
January 20, 20245 min read

Securing REST APIs with Spring Security and JWT

This blog post demonstrates how to secure REST APIs using Spring Security and JSON Web Tokens (JWT). We'll cover the following:

  • Setting up a Spring Boot project with Spring Security
  • Implementing JWT authentication and authorization
  • Creating REST endpoints and securing them with JWT

Prerequisites

  • Java 17 or higher
  • Maven or Gradle
  • Basic understanding of Spring Boot and Spring Security

Project Setup

  1. Create a new Spring Boot project using Spring Initializr (https://start.spring.io/). Include the following dependencies:

    • Spring Web
    • Spring Security
    • Spring Data JPA (optional, if you need database access)
    • H2 Database (optional, for in-memory database)
    • JSON Web Token (JWT)
  2. Add the following JWT dependency to your pom.xml (if using Maven):

```xml io.jsonwebtoken jjwt-api 0.11.5 io.jsonwebtoken jjwt-impl 0.11.5 runtime io.jsonwebtoken jjwt-jackson 0.11.5 runtime ```

Or to your build.gradle (if using Gradle):

```gradle dependencies { implementation 'io.jsonwebtoken:jjwt-api:0.11.5' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' } ```

Implementing JWT Authentication

  1. Create a User entity (optional, if using database):

```java import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id;

@Entity public class User {

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

private String username;
private String password;
private String role; // e.g., "USER", "ADMIN"

// Getters and setters

} ```

  1. Create a UserDetailsService:

```java import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service;

import java.util.ArrayList;

@Service public class MyUserDetailsService implements UserDetailsService {

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    // Replace with your actual user retrieval logic from database
    if ("user".equals(username)) {
        return new User("user", "password", new ArrayList<>()); // In-memory user for demonstration
    } else {
        throw new UsernameNotFoundException("User not found: " + username);
    }
}

} ```

  1. Create a JWTUtil class:

```java import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Service;

import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.function.Function;

@Service public class JwtUtil {

private String SECRET_KEY = "secret"; // Replace with a strong, randomly generated secret key

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.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
}

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

public String generateToken(UserDetails userDetails) {
    Map<String, Object> claims = new HashMap<>();
    return createToken(claims, userDetails.getUsername());
}

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

    return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
            .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // 10 hours
            .signWith(SignatureAlgorithm.HS256, SECRET_KEY).compact();
}

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

} ```

  1. Create a AuthenticationRequest and AuthenticationResponse class:

```java public class AuthenticationRequest {

private String username;
private String password;

// Getters and setters

} ```

```java public class AuthenticationResponse {

private final String jwt;

public AuthenticationResponse(String jwt) {
    this.jwt = jwt;
}

public String getJwt() {
    return jwt;
}

} ```

  1. Create a JWTRequestFilter:

```java 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 javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException;

@Component public class JwtRequestFilter extends OncePerRequestFilter {

@Autowired
private MyUserDetailsService userDetailsService;

@Autowired
private JwtUtil jwtUtil;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
        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);
        }
    }
    chain.doFilter(request, response);
}

} ```

  1. Configure Spring Security:

```java import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@EnableWebSecurity class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService myUserDetailsService; @Autowired private JwtRequestFilter jwtRequestFilter;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(myUserDetailsService);
}

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable()
            .authorizeRequests().antMatchers("/authenticate").permitAll()
            .anyRequest().authenticated()
            .and().sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}

@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
}

@Bean
public PasswordEncoder passwordEncoder() {
    return NoOpPasswordEncoder.getInstance();
}

} ```

  1. Create a Controller:

```java import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController;

@RestController class HelloResource {

@Autowired
private AuthenticationManager authenticationManager;

@Autowired
private MyUserDetailsService userDetailsService;

@Autowired
private JwtUtil jwtUtil;

@PostMapping("/authenticate")
public ResponseEntity<?> createAuthenticationToken(@RequestBody AuthenticationRequest authenticationRequest) throws Exception {

    try {
        authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(authenticationRequest.getUsername(), authenticationRequest.getPassword())
        );
    }
    catch (BadCredentialsException e) {
        throw new Exception("Incorrect username or password", e);
    }


    final UserDetails userDetails = userDetailsService
            .loadUserByUsername(authenticationRequest.getUsername());

    final String jwt = jwtUtil.generateToken(userDetails);

    return ResponseEntity.ok(new AuthenticationResponse(jwt));
}

} ```

Testing the API

  1. Start the Spring Boot application.
  2. Send a POST request to /authenticate with the following JSON body:

```json { "username": "user", "password": "password" } ```

  1. The response will contain a JWT token.
  2. Include the JWT token in the Authorization header of subsequent requests to secured endpoints. The header should be in the format Bearer <token>.

This example provides a basic implementation of JWT authentication with Spring Security. You can customize it further to meet your specific requirements, such as adding roles and permissions, implementing token refresh, and integrating with a database. Remember to replace the hardcoded secret key with a strong, randomly generated key in a production environment.