I have a Spring Boot/Spring Security app where I want to use JWT auth together with JdbcUserDetailsManager, provided by Spring Security.
Here's what I have so far:
ExampleApplication.java
package org.example.app;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ExampleApplication {
public static void main(String[] args) {
SpringApplication.run(ExampleApplication.class, args);
}
}
JwtTokenFilter.java (uses UserDetailsService)
package org.example.app.auth;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
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;
import java.util.List;
import static org.springframework.util.ObjectUtils.isEmpty;
@Component
public class JwtTokenFilter extends OncePerRequestFilter {
private final JwtTokenUtil jwtTokenUtil;
private final UserDetailsService userDetailsService;
public JwtTokenFilter(JwtTokenUtil jwtTokenUtil, JdbcUserDetailsManager jdbcUserDetailsManager) {
this.jwtTokenUtil = jwtTokenUtil;
this.userDetailsService = jdbcUserDetailsManager;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
// Get authorization header and validate
final String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (isEmpty(header) || !header.startsWith("Bearer ")) {
chain.doFilter(request, response);
return;
}
// Get jwt token and validate
final String token = header.split(" ")[1].trim();
if (!jwtTokenUtil.validate(token)) {
chain.doFilter(request, response);
return;
}
// Get user identity and set it on the spring security context
String username = jwtTokenUtil.getUsername(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken
authentication = new UsernamePasswordAuthenticationToken(
userDetails, null,
userDetails == null ?
List.of() : userDetails.getAuthorities()
);
authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
}
}
JwtTokenUtil.java (not that important for the example)
package org.example.app.auth;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Date;
import static java.lang.String.format;
@Component
public class JwtTokenUtil {
Logger logger = LoggerFactory.getLogger(JwtTokenUtil.class);
private final String jwtSecret = "aYEFtKMCn0xCg5caH1nnFuHfdAB0lBOvdonxq80VqOGNnG6QcyagXWOLrUdqJnzexUXYceMhGNFNYsA" +
"6rblSibUEh0yRsJ3XO1um1iMdoekOPzj4zKlokcu9TxTbz5DHYVLkqX3q9JrLgbLZFXD8ynOHfRHRL5Ge64iFZBVm9X517fwZrNornOm" +
"K2L7hUz10SgZpxAz6";
private final String jwtIssuer = "example.org";
public String generateAccessToken(User user) {
byte[] keyBytes = Decoders.BASE64.decode(jwtSecret);
Key key = Keys.hmacShaKeyFor(keyBytes);
return Jwts.builder()
.setSubject(format("%s", user.getUsername()))
.setIssuer(jwtIssuer)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 60 * 1000)) // 1 hour
.signWith(key, SignatureAlgorithm.HS512)
.compact();
}
public String getUsername(String token) {
JwtParser jwtParser = Jwts.parserBuilder()
.setSigningKey(jwtSecret).build();
Claims claims = jwtParser
.parseClaimsJws(token)
.getBody();
return claims.getSubject().split(",")[1];
}
public boolean validate(String token) {
try {
JwtParser jwtParser = Jwts.parserBuilder()
.setSigningKey(jwtSecret).build();
jwtParser.parseClaimsJws(token);
return true;
} catch (SecurityException ex) {
logger.error("Invalid JWT signature - {}", ex.getMessage());
} catch (MalformedJwtException ex) {
logger.error("Invalid JWT token - {}", ex.getMessage());
} catch (ExpiredJwtException ex) {
logger.error("Expired JWT token - {}", ex.getMessage());
} catch (UnsupportedJwtException ex) {
logger.error("Unsupported JWT token - {}", ex.getMessage());
} catch (IllegalArgumentException ex) {
logger.error("JWT claims string is empty - {}", ex.getMessage());
}
return false;
}
}
SecurityConfiguration.java (uses JwtTokenFilter)
package org.example.app.config;
import org.example.app.auth.JwtTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configurers.provisioning.JdbcUserDetailsManagerConfigurer;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.provisioning.JdbcUserDetailsManager;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.sql.DataSource;
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final JwtTokenFilter jwtTokenFilter;
private final DataSource dataSource;
public SecurityConfiguration(JwtTokenFilter jwtTokenFilter, DataSource dataSource) {
this.jwtTokenFilter = jwtTokenFilter;
this.dataSource = dataSource;
}
public void configureHttpSecurity(HttpSecurity http) {
http.addFilterBefore(
jwtTokenFilter,
UsernamePasswordAuthenticationFilter.class
);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Autowired
@Bean
public JdbcUserDetailsManager userDetailsManager(AuthenticationManager authenticationManager,
AuthenticationManagerBuilder authenticationManagerBuilder)
throws Exception {
JdbcUserDetailsManagerConfigurer<AuthenticationManagerBuilder> jdbcUserDetailsManagerConfigurer =
authenticationManagerBuilder.jdbcAuthentication().dataSource(dataSource);
JdbcUserDetailsManager jdbcUserDetailsManager = jdbcUserDetailsManagerConfigurer.getUserDetailsService();
jdbcUserDetailsManager.setAuthenticationManager(authenticationManager);
return jdbcUserDetailsManager;
}
}
When the application attempts to start, I see the following:
***************************
APPLICATION FAILED TO START
***************************
Description:
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| jwtTokenFilter defined in file [/example-app/build/classes/java/main/org/example/app/auth/JwtTokenFilter.class]
↑ ↓
| securityConfiguration defined in file [/example-app/build/classes/java/main/org/example/app/config/SecurityConfiguration.class]
└─────┘
I'm wondering how I can resolve this cyclical dependency while keeping in mind that:
- JwtTokenFilter needs to have a UserDetailsManager, because I want to look the user up in the database to see if the user still exists
- SecurityConfiguration needs to have a JwtTokenFilter that it can set on HttpSecurity
Disclaimer: JWT-related code inspired by https://www.toptal.com/spring/spring-security-tutorial
N.B.: I'm not sure it makes sense to verify user authorities in a database, as one of the selling points of JWT is that it's stateless... I could put the authorities in the JWT token instead.