springauthenticationspring-securityauthorizationpre-authentication

How to redirect UsernameNotFoundException from PreAuthenticatedAuthenticationProvider when using multiple AuthenticationProviders?


Using Spring Security 4.02, can anyone help with some tips on how I can handle UsernameNotFoundException from PreAuthenticatedAuthenticationProvider when using multiple AuthenticationProviders so that authenticated requests, with the correct header, but which are unauthorized, are sent to a specific URL instead of the forms-login page?

Let me explain further what I'm trying to accomplish for accessing a web app being secured by SSO behind a proxy. Not all users who are authenticated by SSO will have access to this app. So I need to account for 3 access scenarios:

  1. authenticated user (header is present) is authorized (username/roles are present in app's db)
  2. authenticated user (header is present) is unauthorized (username/roles are not present in app's db)
  3. unauthenticated user with username/roles present in app's db

The actions when accessing the website should be:

  1. authenticated/authorized user proceeds directly to target URL
  2. authenticated/unauthorized user is redirected to error/info page
  3. unauthenticated user is redirected to forms-login page for authentication

With my current configuration, scenarios 1 & 3 appear to be working as desired. For scenario 2 I've tried setting RequestHeaderAuthenticationFilter#setExceptionIfHeaderMissing to both true and false.

If setExceptionIfHeaderMissing=false, authenticated/unauthorized request is handled by ExceptionTranslationFilter where AccessDeniedException is thrown and user is redirected to forms-login page.

If setExceptionIfHeaderMissing=true, authenticated/unauthorized request encounters PreAuthenticatedCredentialsNotFoundException from AbstractPreAuthenticatedProcessingFilter.doAuthenticate and HTTP 500 is returned.

So I've read and reread the Spring Security reference and api documents and scoured the web and just can't quite figure out what I need to do. I think I somehow need to enable some kind of filter or handler to trap the PreAuthenticatedCredentialsNotFoundException with a redirected response. But I can't seem to wrap my head around how to implement that with all the spring tools available. Can someone please offer some specifics? Many thanks in advance!!

Here is my configuration:

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

    private static final String AUTHENTICATION_HEADER_NAME = "PKE_SUBJECT";

    @Autowired
    CustomUserDetailsServiceImpl customUserDetailsServiceImpl;

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(preAuthenticatedAuthenticationProvider());
        auth.inMemoryAuthentication()
            .withUser("user").password("password").roles("USER").and()
            .withUser("admin").password("password").roles("USER", "ADMIN");
        auth.userDetailsService(customUserDetailsServiceImpl);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().and()
            .authorizeRequests()
                .antMatchers("/javax.faces.resource/**", "/resources/**", "/templates/**", "/public/**").permitAll()                
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .permitAll()
                .and()
            .logout()
                .logoutSuccessUrl("/public/welcome.xhtml")
                .and()
            .addFilter(requestHeaderAuthenticationFilter());    
    }

    @Bean PreAuthenticatedAuthenticationProvider preAuthenticatedAuthenticationProvider() throws Exception {
        PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
        provider.setPreAuthenticatedUserDetailsService(userDetailsServiceWrapper());
        return provider;
    }

    @Bean
    public RequestHeaderAuthenticationFilter requestHeaderAuthenticationFilter() throws Exception {
        RequestHeaderAuthenticationFilter filter = new RequestHeaderAuthenticationFilter();
        filter.setPrincipalRequestHeader(AUTHENTICATION_HEADER_NAME);
        filter.setAuthenticationManager(authenticationManagerBean());
        filter.setExceptionIfHeaderMissing(true);
        return filter;
    }

    @Bean
    public UserDetailsByNameServiceWrapper<PreAuthenticatedAuthenticationToken> 
            userDetailsServiceWrapper() throws Exception {

        UserDetailsByNameServiceWrapper<PreAuthenticatedAuthenticationToken> wrapper 
                = new UserDetailsByNameServiceWrapper<PreAuthenticatedAuthenticationToken>();
        wrapper.setUserDetailsService(customUserDetailsServiceImpl);
        return wrapper;
    }
}

My customized UserDetailsService:

@Service("customUserDetailsService")
public class CustomUserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    UserRepo userRepo;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        UserDetailDO userDetail = userRepo.getUserDetailById(username);
        if(userDetail == null) {
            throw new UsernameNotFoundException("user is not authorized for this application");         
        }

        List<UserRoleDO> roles = userRepo.getRolesByUsername(username);
        List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();

        if(CollectionUtils.isNotEmpty(roles)) {
            for(UserRoleDO role : roles) {
                SimpleGrantedAuthority authority = new SimpleGrantedAuthority(role.getRole());
                authorities.add(authority);             
            }
        }

        UserDetails user = new User(username, "N/A", authorities);      
        return user;
    }
}

Solution

  • I realized that I did not need to handle the exception. What I did was to shift my thinking on this. I realized that even if the username was not found by the customUserDetailsService, the request was still an authenticated request since the request is trusted to be authenticated by the SSO and the proxy server.

    So instead of returning a UsernameNotFoundException I returned the org.springframework.security.core.userdetails.User with an empty Authorities collection. And because the RequestHeaderAuthenticationFilter.setExceptionIfHeaderMissing = false by default, no exception is thrown and then the authenticated request is passed to the access filter where it is determined that the request has no authorization to access any resources. So instead of redirecting to the next authentication filter which would be the forms login provider, a 403 Access Denied http status is returned which I can then override to redirect to a user-friendly error page.