An Interest In:
Web News this Week
- April 2, 2024
- April 1, 2024
- March 31, 2024
- March 30, 2024
- March 29, 2024
- March 28, 2024
- March 27, 2024
Securing a REST API with Spring Security and JWT
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 application with JWT.
The concept of JWT
As a first step, a client must authenticates itself using a username and password, receiving a signed token (JWT) in exchange. 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.
{ "sub": "bootify", "exp": 2208988800}
Example Payload of a JSON Web Token
Authentication endpoint
Spring Security does not provide direct support for JWT, so a number 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:3.12.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.
@RestControllerpublic class AuthenticationController { // ... @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; }}public class AuthenticationRequest { @NotNull @Size(max = 255) private String login; @NotNull @Size(max = 255) private String password;}public class AuthenticationResponse { private String accessToken;}
Authentication endpoint defined in our RestController
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.
Custom services
As referenced in our controller, the JwtTokenService
is responsible for generating and validating the token. We store the secret used for these tasks in our application.properties
or application.yml
under jwt.secret=mySecr3t!
.
@Servicepublic class JwtTokenService { 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) { return JWT.create() .withSubject(userDetails.getUsername()) .withExpiresAt(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY)) .sign(this.hmac512); } public String validateTokenAndGetUsername(final String token) { try { return verifier.verify(token).getSubject(); } catch (final JWTVerificationException verificationEx) { log.warn("token invalid: {}", verificationEx.getMessage()); return null; } }}
JwtTokenService encapsulating token handling
We also provide an implementation of the UserDetailsService that is accessed by the AuthenticationManager - which we configure later. We access a database using ClientRepository
, but any other source can be used here. We only assign the role ROLE_USER
by default, although further roles and permissions could be added at this point as well.
@Servicepublic class JwtUserDetailsService implements UserDetailsService { public static final String USER = "USER"; 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")); return new User(username, client.getHash(), Collections.singletonList(new SimpleGrantedAuthority(ROLE_USER))); }}
Implementation of UserDetailsService
Authentication of the requests
To authenticate the requests going to our REST API, we need to define 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.
@Componentpublic class JwtRequestFilter extends OncePerRequestFilter { // ... @Override protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain) throws ServletException, IOException { // 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 String username = jwtTokenService.validateTokenAndGetUsername(token); if (username == null) { // validation failed or token expired chain.doFilter(request, response); return; } // set user details on spring security context final UserDetails userDetails = jwtUserDetailsService.loadUserByUsername(username); final UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); // continue with authenticated user chain.doFilter(request, response); }}
JwtRequestFilter to validate tokens
Authentication: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJib290aWZ5IiwiZXhwIjoyMjA4OTg4ODAwfQ.2yKAGRfprX1EZEcaXvoZI5Blp9ADXj0SPebCzpGztPaEjcmdVdaV8bdvyivitM_6Qv8rf1yeBIEqQhMMi3vORw
Example JWT submitted as a header
Spring Security config
This leads us to the heart of the matter, the configuration of Spring Security, which brings together all the previous components.
@Configuration@EnableWebSecuritypublic class JwtSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired public void configureGlobal(final AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder()); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(final HttpSecurity http) throws Exception { http.cors().and() .csrf().disable() .authorizeRequests(). antMatchers("/authenticate").permitAll(). anyRequest().hasRole(USER).and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class); }}
Spring Security Configuration
The JwtUserDetailsService
is to be used by the AuthenticationManager
, along with a PasswordEncoder
based on BCrypt. The PasswordEncoder
is exposed as a bean, so it can be used at other parts of our application as well, for example when registering a user and creating the hash from his password.
With antMatchers("/authenticate").permitAll()
our authentication endpoint is freely accessible, but because of anyRequest().hasRole(USER)
the role USER
is required for all other request. Spring Security automatically adds the ROLE_ prefix, so only the short form is passed here.
SessionCreationPolicy.STATELESS
is used to specify that Spring Security does not create or access a session. Also JwtRequestFilter
is added at a proper position in our filter chain, so the SecurityContext
can be updated before the role is actually checked.
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 role directly at our endpoints with @PreAuthorize("hasRole('ROLE_USER')")
.
In the professional plan of Bootify, a Spring Boot application with JWT setup can be generated using a table of the self-defined database schema. Get your runnable Spring Boot application with advanced features in minutes!
Further readings
Java JWT library
Encode and decode tokens online
Tutorial with more backgrounds
Original Link: https://dev.to/tleipzig/securing-a-rest-api-with-spring-security-and-jwt-3lip
Dev To
An online community for sharing and discovering great ideas, having debates, and making friendsMore About this Source Visit Dev To