Search code examples
javaspring-bootspring-security

Implement Spring Security with multiple overlapping AuthenticationProvider


I'm trying to configure my WebSecurityConfigurerAdapter to be able to authenticate two different (and incompatible) authentication flows. To make it simple, the requests sent to the server can have two possible type of token in the header, each type has its own header key (ex: 'webAuth' and 'hardwareAuth').

Rigth now, I have an AuthenticationProvider and ProcessingFilter for each flow.

I set up my configuration as follow:

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.cors()
                .and()
                .sessionManagement()
                .sessionCreationPolicy(STATELESS)
                .and()
                .exceptionHandling()
                .defaultAuthenticationEntryPointFor(unauthorizedEntryPoint(), PROTECTED_URLS)
                .and()
                .authenticationProvider(webAuthenticationProvider)
                .authenticationProvider(hardwareAuthenticationProvider)
                .addFilterBefore(webAuthenticationFilter(), AnonymousAuthenticationFilter.class)
                .addFilterBefore(hardwareAuthenticationFilter(), AnonymousAuthenticationFilter.class)
                .authorizeRequests()
                .antMatchers(ANT_MATCHES).not().authenticated()
                .anyRequest().authenticated()
                .and()
                .csrf().disable()
                .formLogin().disable()
                .httpBasic().disable();
    }

webAuthenticationProvideris executed before hardwareAuthenticationProvider.

The problems are the following:

  • Even if the first provider correctly authenticate, the second one is executed and as - expected fails. As result, the request return 401.
  • If the first provider doesn't authenticate the request, the second one is not executed, and as result the request return 401.

I'd like to introduce a mechanism that allow me to either perform correctly both authentication or to only execute the 'correct' one programmatically checking the header beforhand.

Here the implementation of my providers:

@Component
public class HardwareAuthenticationProvider implements AuthenticationProvider {

    private HardwareTokenService hardwareTokenService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String vToken = (String) authentication.getCredentials();
        if (hardwareTokenService.auth(vToken)) {
            UserDetails userDetails = new User("hw", "", new ArrayList<>());
            return new UsernamePasswordAuthenticationToken(userDetails, vToken, userDetails.getAuthorities());
        } else {
            throw new BadCredentialsException("Invalid v-token");
        }
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }
}
@RequiredArgsConstructor
@Component
public class TokenAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

    @NonNull
    private AuthenticationService authenticationService;

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        Object token = authentication.getCredentials();
    }

    @Override
    protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        Object token = authentication.getCredentials();
        return Optional
                .ofNullable(token)
                .map(t -> authenticationService.loadUserByToken(String.valueOf(t)))
                .orElseThrow(() -> new BadCredentialsException("Invalid authentication token=" + token));
    }
}

Solution

  • As stated create your own custom Authentication object, or at least for the hardware one.

    public class HardwareAuthenticationToken extends AbstractAuthenticationToken {
    
        private final Object principal;
        private Object credentials;  
    
      public HardwareAuthenticationToken(String token) {
        super(null);
        this.credentials = token;
      }
    
      public HardwareAuthenticationToken(Object principal, Object credentials,
                Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true);
      }
    
        @Override
        public Object getCredentials() {
            return this.credentials;
        }
    
        public String getToken() {
          return (String) this.credentials;
        }
    
        @Override
        public Object getPrincipal() {
            return this.principal;
        }
    
        @Override
        public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
            Assert.isTrue(!isAuthenticated,
                    "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
            super.setAuthenticated(false);
        }
    
        @Override
        public void eraseCredentials() {
            super.eraseCredentials();
            this.credentials = null;
        }
    
    }
    

    Now modify the HardwareAuthenticationProvider to use this specific token.

    @Component
    public class HardwareAuthenticationProvider implements AuthenticationProvider {
    
        private HardwareTokenService hardwareTokenService;
    
        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            String vToken = authentication.getToken();
            if (hardwareTokenService.auth(vToken)) {
                UserDetails userDetails = new User("hw", "", new ArrayList<>());
                return new HardwareAuthenticationToken(userDetails, vToken, userDetails.getAuthorities());
            } else {
                throw new BadCredentialsException("Invalid v-token");
            }
        }
    
        @Override
        public boolean supports(Class<?> authentication) {
            return HardwareAuthenticationToken.class.isAssignableFrom(authentication);
        }
    }
    

    In your HardwareAuthenticationFilter construct the HardwareAuthenticationToken using the single arg constructor instead of creating a UsernamePasswordAuthenticationToken.

    You could do the same for the web based authentication and create a dedicated subclass of the UsernamePasswordAuthenticationToken like WebAuthenticationToken and let the other provider react to that specifically.