Spring Boot: JWT Authentication with Spring Security
In this article, we’ll use Spring Security to secure a Spring Boot application.
We’ll use our demo app Stampy, a simple stamp-collecting app, to demonstrate securing an app. Stampy has a /stamps
endpoint that can currently be accessed by anyone and we’d like to make it so that only users that have signed up can access /stamps
. The secured /stamps
request will look like the diagram below i.e. a token is required to authenticate the user. The token is a JSON Web Token, JWT, we’ll talk more about this in a mo.
We’ll also create /signup
and /login
endpoints to create a user and get a new token for an existing user respectively. the login request is depicted below.
Check out the full example on GitHub https://github.com/minibuildsio/stampy.
Required Dependencies
We’ll need the spring-boot-starter-security
library which provides security features. com.auth0:java-jwt
is a library for creating JWTs others are available so feel free to swap out for an alternative if preferred, changes will be isolated to the JwtHelper
class.
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'com.auth0:java-jwt:4.3.0'
What are JWTs
A JSON Web Token, JWT, is a JSON object containing information about a user e.g. the username and the roles they have. A JWT is signed using a secret and a cryptographic algorithm e.g. HMAC which allows you to determine that you created the token and is therefore valid.
JWTs expire after a set amount of time which can be anything from minutes to days or even longer depending on the use case. In general shorter lifespans are advised due to the limited time window an attacker would have if they got access to a user’s JWT. Short-lived JWTs are sometimes accompanied by a longer-lived refresh token which allows the user to request new JWTs.
Creating a JWT
Below is the JwtHelper
class which is responsible for creating JWTs and parsing them from a string. The class requires a secret used for signing the JWT.
Most of the heavy lifting, creating, signing, and verifying the JWT, is done by the com.auth0:java-jwt
library. The cryptographic algorithm used to sign the JWT is HMAC-512, the strength of which depends on the length of the secret so a longer secret is recommended.
@Component
public class JwtHelper {
private static final String ROLES = "roles";
private final byte[] jwtSecret;
private final long jwtLifeSpan;
public JwtHelper(
@Value("${jwt.secret:a-secret}") String jwtSecret,
@Value("${jwt.life-span:86400}") long jwtLifeSpan
) {
this.jwtSecret = jwtSecret.getBytes();
this.jwtLifeSpan = jwtLifeSpan;
}
public String createJwt(String userId, List<String> roles) {
return JWT.create()
.withSubject(userId)
.withArrayClaim(ROLES, roles.toArray(new String[0]))
.withExpiresAt(new Date(System.currentTimeMillis() + jwtLifeSpan * 1000))
.sign(HMAC512(this.jwtSecret));
}
public User extractUser(String token) {
DecodedJWT jwt = JWT.require(Algorithm.HMAC512(this.jwtSecret))
.build()
.verify(token);
List<SimpleGrantedAuthority> roles = jwt.getClaim(ROLES).asList(String.class).stream()
.map(SimpleGrantedAuthority::new)
.toList();
return new User(jwt.getSubject(), "", roles);
}
}
Configuring Spring Security
Spring Security uses a security filter chain to process incoming requests. Filters can be configured to for example determine whether an endpoint can be accessed or set up the security context depending on the information in the request.
Securing Endpoints
The configuration below creates a filter chain that allows unauthenticated access to /login
and /signup
because these are the endpoint used for authentication and restricts access to all other endpoints. It also adds JwtFilter
to the filter chain which is responsible for extracting and verifying the token from the request.
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, JwtFilter jwtFilter)
throws Exception {
http.csrf().disable()
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login", "/signup").permitAll()
.anyRequest().authenticated()
)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
Extract the JWT from the Request Header and Set Up the Security Context
The JwtFilter
class extracts the JWT from the Authorization
header of the request and sets the authentication of the security context using it. The security context stores the details of the user for the current request.
@Component
public class JwtFilter extends OncePerRequestFilter {
public static final String AUTHORIZATION = "Authorization";
public static final String BEARER = "Bearer ";
private final JwtHelper jwtHelper;
public JwtFilter(JwtHelper jwtHelper) {
this.jwtHelper = jwtHelper;
}
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
String header = request.getHeader(AUTHORIZATION);
if (header == null || !header.startsWith(BEARER)) {
filterChain.doFilter(request, response);
return;
}
User user = this.jwtHelper.extractUser(header.replace(BEARER, ""));
Authentication authentication = new UsernamePasswordAuthenticationToken(
user, null, user.getAuthorities()
);
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
}
}
Login Endpoint to Retrieve a JWT
For this demo, we’ll add a dummy /login
endpoint that returns a JWT regardless of the details provided. In a real application, the details would be checked against details in a database.
@RestController
public class AuthController {
private final JwtHelper jwtHelper;
public AuthController(JwtHelper jwtHelper) {
this.jwtHelper = jwtHelper;
}
@PostMapping("/login")
public AuthResponse login(@RequestBody AuthRequest authRequest) {
// In a real app you'd check the user details via a database query/request to a service
String token = jwtHelper.createJwt(authRequest.username(), List.of());
return new AuthResponse(token);
}
}
Where AuthRequest and AuthResponse are records with fields like so:
public record AuthRequest(String username, String password) {
}
public record AuthResponse(String token) {
}
Using the JWT in a Request
We can now retrieve a JWT from the /login
endpoint and use it to make a request to the /stamps
endpoint by passing the token in the Authorization
header.
curl -H "Content-Type: application/json" http://localhost:8080/login \
--data '{ "username": "minibuilds", "password": "ducky" }'
# returns { "token": ... }
curl -H "Authorization: Bearer ..." http://localhost:8080/stamps
# [ {...}, {...} ]