I have a setup where I need to de tenant-aware authentication and authorization. To facilitate this, I have setup a few filters. The first I set up is my TenantFilter
@Order(Ordered.LOWEST_PRECEDENCE - 100)
public class TenantFilter extends OncePerRequestFilter {
private static final Logger logger = LoggerFactory.getLogger(TenantFilter.class);
private static final String TENANT_HEADER = "X-Tenant";
private static final String CONNECTION_STRING = "ObfuscatedConnectionString";
private static final String TENANT_REPLACEMENT = "TENANT";
@Override
protected void doFilterInternal (HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String dbConnectionString = CONNECTION_STRING.replace(TENANT_REPLACEMENT, "");
ConnectionStorage.setConnection(dbConnectionString);
filterChain.doFilter(request, response);
ConnectionStorage.clear();
}
}
while I know that there are a few points that can be improved, this is a placeholder to be able to test the mechanism. acutal implementation will follow later. When tested without adding the need for a JWT, this works as expected, and switches tenants per request given the correct preconditions.
However, I don't want to send the username+password on every request, and want to add a JWT to the mix. For this I implemented the following items:
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
private AuthenticationManager authenticationManager;
public JwtAuthenticationFilter (AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
@Override
public Authentication attemptAuthentication (HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
logger.info("Attempting authentication");
try {
UserRequest creds = new ObjectMapper()
.readValue(request.getInputStream(), UserRequest.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
creds.getUsername(),
creds.getPassword(),
new ArrayList<>())
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
protected void successfulAuthentication (HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication auth) throws IOException, ServletException {
logger.info("Authentication successful");
String token = JWT.create()
.withSubject(((User) auth.getPrincipal()).getUsername())
.withExpiresAt(new Date(System.currentTimeMillis() + (JWTConstants.ACCESS_TOKEN_VALIDITY_SECONDS * 1000)))
.sign(Algorithm.HMAC512(JWTConstants.SIGNING_KEY.getBytes()));
response.addHeader(JWTConstants.HEADER_STRING, JWTConstants.TOKEN_PREFIX + token);
}
}
and
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {
private static final Logger logger = LoggerFactory.getLogger(JwtAuthorizationFilter.class);
public JwtAuthorizationFilter (AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal (HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
logger.info("Doing filter internal");
String header = request.getHeader(JWTConstants.HEADER_STRING);
if (header == null || !header.startsWith(JWTConstants.TOKEN_PREFIX)) {
chain.doFilter(request, response);
return;
}
UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
}
private UsernamePasswordAuthenticationToken getAuthentication (HttpServletRequest request) {
String token = request.getHeader(JWTConstants.HEADER_STRING);
if (token != null) {
// parse the token.
String user = JWT.require(Algorithm.HMAC512(JWTConstants.SIGNING_KEY.getBytes()))
.build()
.verify(token.replace(JWTConstants.TOKEN_PREFIX, ""))
.getSubject();
if (user != null) {
return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
}
return null;
}
return null;
}
}
to provide the authentication for my requests. A username password in /api/user/login, and a jwt token for everything else.
However, it seems as if security filters are executed first, and my tenant filter is rather vital to the setting of the connectionstring of the correct DB to go to.
version: springframework.boot: 2.6.2