Last updated: 2025-04-08
Securing a REST API with Spring Security and JWT
Create your customized Spring Boot prototype with database schema, Thymeleaf or Angular frontend and much more - best practices included. ♥
Discover more
Spring Security is the de facto standard for securing Spring Boot applications. JSON Web Token (JWT) is a good choice for protecting a REST API - the following article will show the minimal steps to setup a Spring Boot app protected with JWT.
The concept of JWT
A JWT is a JSON object that has been signed using a specific procedure. This allows to validate that the JWT is actually valid.
{
"sub": "bootify",
"iss": "app",
"exp": 1675699808,
"iat": 1675698608
}
Example Payload of a JSON Web Token
The process of authentication normally looks like the following. First, a client submits username and password to an unprotected endpoint. If the credentials are valid, it receives a signed token (JWT) in return. This token is stored locally at the client and is passed to the server with every further request, typically in the header. Since the token is signed using a key that only the server knows, the token and thus the client can be validated safely.
This approach makes the whole process stateless and very suitable for REST APIs, since no data about the state of the client (e.g. a session) needs to be stored. The username and the expiration date of the token are stored in the payload.
Authentication endpoint
Spring Security does not provide direct support for JWT, so a couple of additions to our Spring Boot application are necessary. In our build.gradle
or pom.xml
, we need to add the following two dependencies. Using the java-jwt
library, we will later generate and validate the tokens.
implementation('org.springframework.boot:spring-boot-starter-security')
implementation('com.auth0:java-jwt:4.5.0')
Adding new dependencies
The following controller defines the first step from a client's perspective: an endpoint for authentication to obtain a valid token. To keep the code snippet short, the constructor with the field assignments is omitted.
@RestController
public class AuthenticationResource {
// ...
@PostMapping("/authenticate")
public AuthenticationResponse authenticate(@RequestBody @Valid final AuthenticationRequest authenticationRequest) {
try {
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(
authenticationRequest.getLogin(), authenticationRequest.getPassword()));
} catch (final BadCredentialsException ex) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
}
final UserDetails userDetails = jwtUserDetailsService.loadUserByUsername(authenticationRequest.getLogin());
final AuthenticationResponse authenticationResponse = new AuthenticationResponse();
authenticationResponse.setAccessToken(jwtTokenService.generateToken(userDetails));
return authenticationResponse;
}
}
Authentication endpoint defined in our RestController
We also need to following request and response objects.
public class AuthenticationRequest {
@NotNull
@Size(max = 255)
private String login;
@NotNull
@Size(max = 255)
private String password;
}
public class AuthenticationResponse {
private String accessToken;
}
The request is validated and then passed to the authenticationManager for authentication. If successful, the JSON web token is generated and returned. There are no further details in the response, since the token itself should contain all relevant information.
Token generation and user loading
As already referenced in our controller, the JwtTokenService
is responsible for generating and validating the token. Here we use a single secret for both operations - find out how to use a public and private key for signing and validating JWTs. We store the secret used for these tasks in our application.properties
or application.yml
under the key jwt.secret
. Make sure the key is at least 512 bits long, as this is required for this algorithm.
@Service
public class JwtTokenService {
private static final Duration JWT_TOKEN_VALIDITY = Duration.ofMinutes(30);
private final Algorithm hmac512;
private final JWTVerifier verifier;
public JwtTokenService(@Value("${jwt.secret}") final String secret) {
this.hmac512 = Algorithm.HMAC512(secret);
this.verifier = JWT.require(this.hmac512).build();
}
public String generateToken(final UserDetails userDetails) {
final Instant now = Instant.now();
return JWT.create()
.withSubject(userDetails.getUsername())
.withIssuer("app")
.withIssuedAt(now)
.withExpiresAt(now.plus(JWT_TOKEN_VALIDITY))
.sign(this.hmac512);
}
public DecodedJWT validateToken(final String token) {
try {
return verifier.verify(token);
} catch (final JWTVerificationException verificationEx) {
log.warn("token invalid: {}", verificationEx.getMessage());
return null;
}
}
}
JwtTokenService encapsulating token handling
We also provide an implementation of the UserDetailsService interface that is accessed by the AuthenticationManager - which we configure later on. We access a table client
using the ClientRepository
, but any other source can be used here. We only assign the default role ROLE_USER
, although different roles and permissions could be used at this point as well.
@Service
public class JwtUserDetailsService implements UserDetailsService {
public static final String ROLE_USER = "ROLE_USER";
// ...
@Override
public UserDetails loadUserByUsername(final String username) {
final Client client = clientRepository.findByLogin(username).orElseThrow(
() -> new UsernameNotFoundException("User " + username + " not found"));
final List<SimpleGrantedAuthority> roles = List.of(new SimpleGrantedAuthority(UserRoles.ROLE_USER));
return new JwtUserDetails(client.getId(), username, client.getHash(), roles);
}
}
Implementation of UserDetailsService
We're also using our own implementation of the UserDetails
interface by extending the Spring Security org.springframework.security.core.userdetails.User
class. While this is not strictly needed, it allows us to keep the primary key within the authentication details.
public class JwtUserDetails extends User {
public final Long id;
public JwtUserDetails(final Long id, final String username, final String hash,
final Collection<? extends GrantedAuthority> authorities) {
super(username, hash, authorities);
this.id = id;
}
}
Extension of UserDetails
Authentication of the requests
To authenticate the requests arriving at our REST API, we need to provide JwtRequestFilter
. This filter ensures that a valid token is passed in the header and will store the UserDetails
in the SecurityContext
for the duration of the request.
public class JwtRequestFilter extends OncePerRequestFilter {
// ...
@Override
protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response,
final FilterChain chain) throws IOException, ServletException {
// look for Bearer auth header
final String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (header == null || !header.startsWith("Bearer ")) {
chain.doFilter(request, response);
return;
}
final String token = header.substring(7);
final DecodedJWT jwt = jwtTokenService.validateToken(token);
if (jwt == null || jwt.getSubject() == null) {
// validation failed or token expired
chain.doFilter(request, response);
return;
}
final UserDetails userDetails;
try {
userDetails = userDetailsService.loadUserByUsername(jwt.getSubject());
} catch (final UsernameNotFoundException userNotFoundEx) {
// user not found
chain.doFilter(request, response);
return;
}
final UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// set user details on spring security context
SecurityContextHolder.getContext().setAuthentication(authentication);
// continue with authenticated user
chain.doFilter(request, response);
}
}
JwtRequestFilter to validate tokens
The following header must be present at a request so that our filter can validate the JWT and authenticate the request.
Authentication: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJib290aWZ5IiwiZXhwIjoyMjA4OTg4ODAwfQ.2yKAGRfprX1EZEcaXvoZI5Blp9ADXj0SPebCzpGztPaEjcmdVdaV8bdvyivitM_6Qv8rf1yeBIEqQhMMi3vORw
Example JWT submitted as a header
There is also the possibility to use the BearerTokenAuthenticationFilter
of the library spring-boot-starter-oauth2-resource-server
. However using our own filter gives us more flexibility and the user is loaded with its current data and roles from the JwtUserDetailsService
on every request.
Spring Security config
This leads us to the heart of the matter, the configuration of Spring Security, which brings together all the previous components. Since Spring Boot 3 we should return the SecurityFilterChain
and don't use the WebSecurityConfigurerAdapter
anymore. With the current version 3.4.4 we should always define a Lamda or use withDefaults()
for configuring each part of our config.
@Configuration
public class JwtSecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(
final AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
public JwtRequestFilter jwtRequestFilter(final JwtUserDetailsService jwtUserDetailsService,
final JwtTokenService jwtTokenService) {
return new JwtRequestFilter(jwtUserDetailsService, jwtTokenService);
}
@Bean
public SecurityFilterChain configure(final HttpSecurity http,
final JwtUserDetailsService jwtUserDetailsService,
final JwtTokenService jwtTokenService) throws Exception {
return http.cors(withDefaults())
.csrf((csrf) -> csrf.disable())
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/", "/authenticate").permitAll()
.anyRequest().hasAuthority(UserRoles.ROLE_USER))
.sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtRequestFilter(jwtUserDetailsService, jwtTokenService), UsernamePasswordAuthenticationFilter.class)
.build();
}
}
Spring Security configuration for JWT
With requestMatchers("/authenticate").permitAll()
our authentication endpoint is freely accessible, but because of anyRequest().hasAuthority(UserRoles.ROLE_USER)
the role ROLE_USER
is required for all other request. This would be similar to hasRole("USER")
, where Spring Security would automatically add the "ROLE_"
prefix.
With SessionCreationPolicy.STATELESS
Spring Security does not create or access a session. JwtRequestFilter
is added at a proper position in our filter chain, so that the token is validated and the SecurityContext
is updated before the required role is actually checked.
The JwtRequestFilter is not provided as a @Bean
in our config, because Spring Boot would then register this filter for the entire application. With our configuration the filter is only part of the Spring Security setup instead.
Conclusion
With this minimal setup, our application is secured using Spring Security and JWT. It can be extended according to our own requirements, for example to define the required roles directly at our endpoints with @PreAuthorize("hasAuthority('ROLE_USER')")
.
In the Professional plan of Bootify, a Spring Boot app with JWT setup can be generated using a table of your custom database schema. All roles can be defined together with the endpoints they should protect, and so much more.
See Pricing
or read quickstart
Further readings
Java JWT library
Encode and decode tokens online
Tutorial with more backgrounds