Mini Builds

Spring Boot JWT Authentication and Authorisation

April 7, 2021

In this article we will demonstrate how to secure a Spring Boot application with JSON Web Tokens (JWT).

A JWT contains information such as who the user is (authentication) and what the user can do i.e. roles (authorization). Depending on info in the JWT access certain endpoints will be available or restricted.

Checkout on GitHub

You can find a full application in the GitHub repo: https://github.com/mini-builds/springboot-jwt

Dependencies

This projects requires the Spring Security dependency when created using the Spring Initialzr and the auth0 jwt library:

implementation 'com.auth0:java-jwt:3.10.3'

Overview

Getting a token

  1. A request is a made to /login with username and password provided.
  2. UsernamePasswordAuthenticationFilter.attemptAuthentication(…) is called which extracts the user credentials from the request and passes them to the auth manager.
  3. The auth manager uses UserDetailsService to get the actual user credentials (e.g. from a database).
  4. If the supplied credentials match the actual credentials then UsernamePasswordAuthenticationFilter.successfulAuthentication(…) which writes the JWT to the response.

Making an request to a restricted endpoint

  1. When a request is made for a secured endpoint with the JWT in the ‘Authorization’ header.
  2. BasicAuthenticationFilter.doFilter(…) is called which extracts the user details from the JWT.
  3. If the provided JWT is valid and has authorities required by the endpoint (specified using annotations e.g. @PreAuthorize("hasAuthority('ADMIN')")) the endpoint will be called.

Username Password Authentication Filter

This filter extracts the user credentials from the request and passes them to the auth manager and builds the response with the JWT.

public class JwtUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

  private final AuthenticationManager authenticationManager;
  private final JwtHelper jwtHelper;
  private static final ObjectMapper mapper = new ObjectMapper();

  public JwtUsernamePasswordAuthenticationFilter(AuthenticationManager authManager, JwtHelper jwtHelper) {
    this.authenticationManager = authManager;
    this.jwtHelper = jwtHelper;
  }

  @Override
  public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException {
    try {
      Map<String, String> creds = mapper.readValue(req.getInputStream(), Map.class);

      return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(creds.get("username"), creds.get("password")));
    } catch (IOException e) {
      throw new RuntimeException("Expected username and password");
    }
  }

  @Override
  protected void successfulAuthentication(HttpServletRequest req,
                                          HttpServletResponse res,
                                          FilterChain chain,
                                          Authentication auth) throws IOException {
    User user = (User) auth.getPrincipal();

    Map<String, String> response = Map.of("token", jwtHelper.createJwt(user));
    String tokenJson = mapper.writeValueAsString(response);

    res.setContentType("application/json");

    var writer = res.getWriter();
    writer.println(tokenJson);
    writer.close();
  }
}

User Details Service

A service which returns user details for a provided username. In a real world implementation this would lookup details from a database or similar.

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

  private final BCryptPasswordEncoder bCryptPasswordEncoder;
  private final Map<String, String> users;
  private final Map<String, List<GrantedAuthority>> authorities;

  public UserDetailsServiceImpl(BCryptPasswordEncoder bCryptPasswordEncoder) {
    this.bCryptPasswordEncoder = bCryptPasswordEncoder;

    users = new HashMap<>();
    users.put("hp", bCryptPasswordEncoder.encode("password"));
    users.put("admin", bCryptPasswordEncoder.encode("admin_password"));

    authorities = new HashMap<>();
    authorities.put("admin", List.of(new SimpleGrantedAuthority("ADMIN")));
  }

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    if (!users.containsKey(username)) {
      throw new UsernameNotFoundException(String.format("Could not find player with username = %s", username));
    }

    return new User(username, users.get(username), authorities.getOrDefault(username, List.of()));
  }

Basic Authentication Filter

This filter extracts the user details and authorities from the ‘Authorization’ header.

public class JwtBasicAuthenticationFilter extends BasicAuthenticationFilter {

  public static final String AUTHORIZATION = "Authorization";
  public static final String BEARER = "Bearer ";

  private final JwtHelper jwtHelper;

  public JwtBasicAuthenticationFilter(AuthenticationManager authManager, JwtHelper jwtHelper) {
    super(authManager);
    this.jwtHelper = jwtHelper;
  }

  @Override
  protected void doFilterInternal(HttpServletRequest req,
                                  HttpServletResponse res,
                                  FilterChain chain) throws IOException, ServletException {
    String header = req.getHeader(AUTHORIZATION);

    if (header == null || !header.startsWith(BEARER)) {
      chain.doFilter(req, res);
      return;
    }

    SecurityContextHolder.getContext().setAuthentication(getAuthentication(header));
    chain.doFilter(req, res);
  }

  private UsernamePasswordAuthenticationToken getAuthentication(String header) {
    if (header != null) {
      User user = this.jwtHelper.extractUser(header.replace(BEARER, ""));

      if (user != null) {
        return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
      }
    }
    return null;
  }
}

WebSecurityConfigurerAdapter

Finally configure Spring Boot to use the filters.

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
  private final UserDetailsService userDetailsService;
  private final JwtHelper jwtHelper;
  private final BCryptPasswordEncoder bCryptPasswordEncoder;

  public WebSecurityConfig(UserDetailsService userDetailsService, JwtHelper jwtHelper, BCryptPasswordEncoder bCryptPasswordEncoder) {
    this.userDetailsService = userDetailsService;
    this.jwtHelper = jwtHelper;
    this.bCryptPasswordEncoder = bCryptPasswordEncoder;
  }

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.cors().and().csrf().disable()
        .authorizeRequests()
        .anyRequest().authenticated()
        .and()
        .addFilter(new JwtUsernamePasswordAuthenticationFilter(authenticationManager(), this.jwtHelper, ""))
        .addFilter(new JwtBasicAuthenticationFilter(authenticationManager(), this.jwtHelper))
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
  }

  @Override
  public void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
  }
}

Application

In the application class you’ll need to provide a BCryptPasswordEncoder to be injected into filters and configurer.

@SpringBootApplication
public class SpringBootJwtExampleApplication {

	@Bean
	public BCryptPasswordEncoder bCryptPasswordEncoder() {
		return new BCryptPasswordEncoder();
	}

	public static void main(String[] args) {
		SpringApplication.run(SpringBootJwtExampleApplication.class, args);
	}

}

Controller

This example controller has 2 secured endpoints:

Information about the user, e.g. the username, is provided as a Authentication object as demonstrated in the /me endpoint.

@RestController
public class ExampleController {

  @GetMapping("/me")
  public Map<String, String> me(Authentication authentication) {
    return Map.of("message", "Hello " + authentication.getPrincipal());
  }

  @PreAuthorize("hasAuthority('ADMIN')")
  @GetMapping("/adminonly")
  public Map<String, String> adminOnly() {
    return Map.of("message", "Hello you are special");
  }
}

JWT Helper

The helper below contains methods for creating and verifying JWTs. The secret used to sign the token and lifespan of the token can be configured in the application properties.

@Service
public class JwtHelper {

  private static final String ROLES = "roles";

  private final byte[] jwtSecret;
  private final long jwtLifeSpan;

  public JwtHelper(@Value("${jwt.secret}") String jwtSecret, @Value("${jwt.life-span}") long jwtLifeSpan) {
    this.jwtSecret = jwtSecret.getBytes();
    this.jwtLifeSpan = jwtLifeSpan;
  }

  public String createJwt(User user) {
    return JWT.create()
        .withSubject(user.getUsername())
        .withArrayClaim(ROLES, user.getAuthorities().stream().map(GrantedAuthority::getAuthority).toArray(String[]::new))
        .withExpiresAt(new Date(System.currentTimeMillis() + jwtLifeSpan))
        .sign(HMAC512(this.jwtSecret));
  }

  public User extractUser(String token) {
    DecodedJWT jwt = JWT.require(Algorithm.HMAC512(this.jwtSecret))
        .build()
        .verify(token);

    return new User(jwt.getSubject(), "", jwt.getClaim(ROLES).asList(String.class).stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()));
  }
}

The values jwt.secret and jwt.life-span are set in the application.yaml file:

jwt:
  secret: ${JWT_SECRET:secret}
  life-span: ${JWT_LIFE_SPAN:1800000} # milliseconds 30mins

Example requests accessing a secured endpoint

Below is an example of requesting a JWT for the user ‘hp’. To access a secured endpoint the JWT must be sent in the Authorization prefixed with ‘Bearer ' e.g. -H 'Authorization: Bearer abc123...'

The token is then used to successfully access the secured endpoint /me and unsuccessfully access the /adminonly endpoint because the user does not have the admin role.

curl -X POST 'http://localhost:8080/login' \
  -H 'Content-Type: application/json' \
  -d '{ "username": "hp", "password": "password" }'

# { "token": "..." }

curl -X POST 'http://localhost:8080/me' -H 'Authorization: Bearer ...'

# { "message": "Hello hp" }

curl -X POST 'http://localhost:8080/adminonly' -H 'Authorization: Bearer ...'

# 403

Accessing the /adminonly endpoint with a JWT for the ‘admin’ is successful as they have the admin role.

curl -X POST 'http://localhost:8080/login' \
  -H 'Content-Type: application/json' \
  -d '{ "username": "admin", "password": "admin_password" }'

# { "token": "..." }

curl -X POST 'http://localhost:8080/adminonly' -H 'Authorization: Bearer ...'

# { "message": "Hello you are special" }