Multiple Security adapter - addFilterBefore not work as expected

Viewed 2213

I would like to have a configuration for API and MVC. Authorization to API is JWT while authorization to MVC is x509 client certificate (separetley both configuration works well).

Expected behaviour:

  1. request /v2/api/** would be filtered with filter given in addFilterBefore where jwt is verified and context created
  2. All other request would use ssl client certificate authentication.

How it works now - any request fire addFilterBefore and is rejected cause of lack of jwt token.

my config class:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class X509AuthenticationServer extends WebSecurityConfigurerAdapter
{

    @Configuration
    @Order(1)
    public static class JwtAuth extends WebSecurityConfigurerAdapter
    {

        @Autowired
        private JwtRequestFilter jwtRequestFilter;

        @Override
        protected void configure(HttpSecurity http) throws Exception
        {
            http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class)
                .antMatcher("/v2/api/**").authorizeRequests()
                .antMatchers("/v2/api/**").authenticated()
                .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and().csrf().disable();
        }
    }

    @Configuration
    @Order(4)
    public class x509Authenticator extends X509AuthenticationServer
    {

        @Override
        protected void configure(HttpSecurity http) throws Exception
        {
            http.authorizeRequests().anyRequest().authenticated()
                .and().x509().subjectPrincipalRegex("CN=(.*?)(?:,|$)")
                .userDetailsService(userDetailsService())
                .and().exceptionHandling().accessDeniedPage("/forbidden");

        }

        @Bean
        public UserDetailsService userDetailsService() 
        {
            return new UserDetailsService() 
            {
                @Override
                public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException 
                {
                    return new User(username, "", AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
                }
            }
        }
    }
}
1 Answers

Please Note: Information/Code you provided is not sufficient. As you have not provided codes of X509AuthenticationServer and JwtRequestFilter i can't tell what exactly wrong.

Assuming it is your urgent requirement trying to explain all possibility.

How it works now - any request fire addFilterBefore and is rejected cause of lack of jwt token.

Yes it will pass throw the filter for any request. Only one thing you should take care in filter is check for header, if present then proceed for JWT authentication else skip JWT authentication(Check my filter class code if block).

As your spring security is configured with x509, authentication object will be set by certificate subject using regex .x509().subjectPrincipalRegex("CN=(.*?)(?:,|$)")

  • For X509 request principal will be from client certificate subject

  • For JWT request as it consists of server certificate, principal will be set from server certificate subject. Now override principal by getting username from JWT token and grant authorities required for those user.

Below given server certificate subject & principal received from regex is "Praveen"
Subject: EMAILADDRESS=nlpraveennl@gmail.com, CN=Praveen, OU=Answer, O=StackOverflow, L=BENGALURU, ST=KA, C=IN
Below given client certificate details and look at subject. principal received from regex is "vedanta"
[
  Version: V1
  Subject: EMAILADDRESS=vedanta@gmail.com, CN=vedanta, OU=some unit, O=some comapany, L=San diego, ST=CA, C=US
  Signature Algorithm: SHA256withRSA, OID = 1.2.840.113549.1.1.11

  Key:  Sun RSA public key, 4096 bits
  modulus: 817322829240490927539679977649457347113079769252522527800627995161015683461174014845370356721299781132807751160825513223084246773438383264091041820195243162665509891042158368380019167357193944418022840037166629645037683573001749034046239097004884878919482801656531508586406926436161114752390080130151471441072696492233900694526232243678291240183028778626646828176141484812179759739175161094296327915519918417719601837669497535100237474334129104633049344560273440876841464270970566837970617275659841740418106346304917775719024288908219661239022501256531355020518204348890248534705892780384737192506755315365883201062844995260628679776392945804218936346471987181753817158168697446990117525268883019172878686864407803654159029932574084328051385395658876802073285425506794524283532870369321962543974623256690683454729681498079311854836252232196330777070325603711372859419866719120909302184446204160932841096818392866335724975192880990513954025684813917171551040959488266330875554906980729293764667616531866907648957912796417658137011975409928985274876102141544954375822680116494691313951508398067207453068155470604238471192479465455519868221517071480577973522583550201343549400093003081375335896091143428006322327934212827941149315676683
  public exponent: 65537
  Validity: [From: Tue Oct 15 09:04:58 IST 2019,
               To: Fri Oct 09 09:04:58 IST 2020]
  Issuer: EMAILADDRESS=nlpraveennl@gmail.com, CN=Praveen, OU=Answer, O=StackOverflow, L=BENGALURU, ST=KA, C=IN
  SerialNumber: [    baafa655 dd56be52]

]



What else may be wrong

1. For v2 API(JWT):You are not sending server certificate in your request. I agree it does not needs client certificate, But server accepts communication only through SSL connection.

  • You can not test it by curl cmd. It works only if your SSL is signed by CA.

  • You can not test this in google chrome as google chrome also needs SSL signed by CA.

Hack for testing:
Use the server certificate and security key along with your JWT token as given below

curl -ik --cert server.crt --key serverPrivateKey.pem -H "Authorization:Bearer jwtToken" "https://localhost:8443/v2/api/yourpath"

It should work.

2. for API's other than v2(Without JWT) : You are not including client certificate in request. Correct way to test is.

curl -ik --cert pavel.crt --key myPrivateKey.pem "https://localhost:8443/v1/hello"
curl -ik --cert pavel.crt --key myPrivateKey.pem "https://localhost:8443/v1.5/hello"

I have tested from my end it is working for me without any glitch. Only thing i have

FYI addinng the code here.

configuration
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig 
{

    @Configuration
    @Order(1)
    public static class JwtConfiguration extends WebSecurityConfigurerAdapter
    {

        @Autowired
        private JwtAuthenticationTokenFilter jwtRequestFilter;

        @Override
        protected void configure(HttpSecurity http) throws Exception
        {
            http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class)
                .antMatcher("/v2/**").authorizeRequests()
                .antMatchers("/v2/**").authenticated()
                .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and().csrf().disable();
        }
    }

    @Configuration
    @Order(4)
    public static class X509Configuration extends WebSecurityConfigurerAdapter
    {

        @Override
        protected void configure(HttpSecurity http) throws Exception
        {
            http.authorizeRequests().anyRequest().authenticated()
                .and().x509().subjectPrincipalRegex("CN=(.*?)(?:,|$)")
                .userDetailsService(userDetailsService())
                .and().exceptionHandling().accessDeniedPage("/forbidden");

        }

        @Bean
        public UserDetailsService userDetailsService() 
        {
            return new UserDetailsService() 
            {
                @Override
                public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException 
                {
                    System.out.println("[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]");
                    System.out.println(username);
                    System.out.println("[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]");
                    return new User(username, "", AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
                }
            };
        }
    }
}
Jwt auth token filter
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException
    {
        System.out.println("(((((((((((((((())))))))))))))");
        final String header = request.getHeader("Authorization");

        if (header != null && header.startsWith("Bearer ")) 
        {
            String authToken = header.substring(7);
            System.out.println(authToken);

            try
            {
                String username = jwtTokenUtil.getUsernameFromToken(authToken);
                if (username != null && SecurityContextHolder.getContext().getAuthentication() == null)
                {
                    // here username should be validated with database and get authorities from database if valid
                    // Say just to hard code sending same username received
                    if (jwtTokenUtil.validateToken(authToken, username))
                    {
                        List<GrantedAuthority> authList = new ArrayList<>();
                        authList.add(new SimpleGrantedAuthority("ROLE_APIUSER"));

                        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, null, authList);
                        usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

                        SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
                    }
                }
            }
            catch (Exception e)
            {
                System.out.println("Unable to get JWT Token, possibly expired");
            }
        }

        chain.doFilter(request, response);
    }
}
JwtTokenUtility class
@Component
public class JwtTokenUtil implements Serializable
{
    private static final long   serialVersionUID    = 8544329907338151549L;
    public static final long    JWT_TOKEN_VALIDITY  = 5 * 60 * 60;
    private String              secret              = "my-secret";

    public String getUsernameFromToken(String token)
    {
        return getClaimFromToken(token, Claims::getSubject);
    }

    public Date getExpirationDateFromToken(String token)
    {
        return getClaimFromToken(token, Claims::getExpiration);
    }

    public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver)
    {
        final Claims claims = getAllClaimsFromToken(token);
        return claimsResolver.apply(claims);
    }

    private Claims getAllClaimsFromToken(String token)
    {
        return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
    }

    private Boolean isTokenExpired(String token)
    {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }

    public String generateToken(String username)
    {
        Map<String, Object> claims = new HashMap<>();
        return doGenerateToken(claims, username);
    }

    private String doGenerateToken(Map<String, Object> claims, String subject)
    {
        return "Bearer "+Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000)).signWith(SignatureAlgorithm.HS512, secret).compact();
    }

    public Boolean validateToken(String token, String usernameFromToken)
    {
        final String username = getUsernameFromToken(token);
        return (username.equals(usernameFromToken) && !isTokenExpired(token));
    }

    //To generate token for testing
    public static void main(String[] args)
    {
        JwtTokenUtil tu = new JwtTokenUtil();
        String s1 = tu.generateToken("hello");
        System.out.println(s1);
        String user = tu.getUsernameFromToken(s1);
        System.out.println(user);
    }
}
Controller/API's
@RestController
public class HelloController
{
    //Client certificate
    @RequestMapping(path = "/v1/hello")
    public String helloV1()
    {

        return "HELLO Version 1 - "+((User)SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUsername();
    }

    //Client certificate
    @RequestMapping(path = "/v1.5/hello")
    public String helloV1Dot5()
    {
        return "HELLO Version 1.5 - "+((User)SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUsername();
    }

    //Jwt
    @RequestMapping(path = "/v2/hello")
    public String helloV2()
    {
        return "HELLO Version 2 - "+SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    }
}
Related