0

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

ShadowFlame
  • 2,996
  • 5
  • 26
  • 40

1 Answers1

0

You can look at this answer: https://stackoverflow.com/a/59340951 This will give you most control of when the filter is executed.

I would think (but I might be wrong) @Order(Ordered.HIGHEST_PRECEDENCE) might also do the trick. Although the wording is a bit strange, the Ordered.HIGHEST_PRECEDENCE gives you Integer.MIN_VALUE and the lower values are executed first.

As a side note, please also look at Spring Security's OAuth 2.0 capabilities before writing your own JWT validation filters (unless you have a specific case): https://docs.spring.io/spring-security/reference/6.0.0-M1/servlet/oauth2/resource-server/jwt.html#oauth2resourceserver-jwt-minimalconfiguration