3
votes

Service works after gateway in trusted space (gateWay verifies OAuth token and gives to the service only unique user ID other case it redirects to authenticate service).

I want use spring security in the service to be able validate permissions for userId.

So I've added CustomUserDetailsService

@Service("userDetailsService")
public class CustomUserDetailsService implements UserDetailsService {
    @Autowired(required = false)
    private ContextSsoActiveProfileIdProvider contextSsoActiveProfileIdProvider;
    @Autowired
    private GrantedAuthorityService grantedAuthorityService;

    @Override
    public User loadUserByUsername(final String username) throws UsernameNotFoundException {
        // verify it with authentication service, but there is not token, userId only, so trust to gateway service.
        return new User(
                String.valueOf(contextSsoActiveProfileIdProvider.getSsoActiveProfileId()),
                "authenticatedWithGateWay",
                grantedAuthorityService.getGrantedAuthoritiesForCurrentUser()
        );
    }
}

Where contextSsoActiveProfileIdProvider.getSsoActiveProfileId() returns uniqueUserId and grantedAuthorityService.getGrantedAuthoritiesForCurrentUser() returns authorities.

The service starts in trusted zone so I have configured security in next way:

@EnableWebSecurity
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/**").permitAll();
    }

    @Override
    protected UserDetailsService userDetailsService() {
        return userDetailsService;
    }
}

I need provide free access for all users (without triggering login offer) for all URIs (http.authorizeRequests().antMatchers("/**").permitAll();) but it seems suppressed triggering handlers for next annotations @PreAuthorize, @PreFilter, @PostAuthorize and @PostFilter.

I suppose I mistook here with http.authorizeRequests().antMatchers("/**").permitAll(); or with other configuration part.

More issue symptoms:

  • CustomUserDetailsService.loadUserByUsername(..) is never called;
  • On REST API part @AuthenticationPrincipal User activeUser is null
  • On REST API part Principal principal is null also
1
You must overwrite another method for loadByUserName to be called. The method is " @Override protected void configure(AuthenticationManagerBuilder auth) {}"willermo
This information is not enough, because configure of this method doesn't solve my issue without additional tricks. There are a lot of elements to research via spring security sources (to find out information about additional tricks).Sergii

1 Answers

5
votes

Trusted space issue has similar solution to anonymous user identification (I've done this conclusion when I was working on it.)

Short answer

Trusted space does not need authorization, but no UserDetailsService will be called, because of using only AnonymousAuthenticationProvider and AnonymousAuthenticationFilter by default. It is good enough implement custom filter based on AnonymousAuthenticationFilter overriding createAuthentication and replace default (AnonymousAuthenticationFilter) with custom one (CustomAnonymousAuthenticationFilter):

    @Configuration
    public static class NoAuthConfigurationAdapter extends WebSecurityConfigurerAdapter {
        @Autowired
        private UserDetailsService userDetailsService;
        @Autowired
        private IdentifiableAnonymousAuthenticationFilter identifiableAnonymousAuthenticationFilter;

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.anonymous().authenticationFilter(identifiableAnonymousAuthenticationFilter);
            http.antMatcher("/**").authorizeRequests()
                    .anyRequest().permitAll();
        }
    }

Full answer

I found out that CustomUserDetailsService will never be called if user is not authorized. Continuing research pay attention on the AnonymousAuthenticationFilter which is responsible for creating anonymous user info. So in the very and purpose is to replace the AnonymousAuthenticationFilter with my IdentifiableAnonymousAuthenticationFilter where some methods should be overridden:

@Component
public class IdentifiableAnonymousAuthenticationFilter extends AnonymousAuthenticationFilter {
    public static final String KEY_IDENTIFIABLE_ANONYMOUS_AUTHENTICATION_FILTER
            = "Key.IdentifiableAnonymousAuthenticationFilter";
    @Autowired
    private CustomUserDetailsService userDetailsService;
    @Autowired
    private GrantedAuthorityService grantedAuthorityService;
    private AuthenticationDetailsSource authenticationDetailsSource
            = new WebAuthenticationDetailsSource();

    public IdentifiableAnonymousAuthenticationFilter() {
        this(KEY_IDENTIFIABLE_ANONYMOUS_AUTHENTICATION_FILTER);
    }

    public IdentifiableAnonymousAuthenticationFilter(String key) {
        super(key);
    }

    @Override
    protected Authentication createAuthentication(HttpServletRequest request) {
        AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(
                KEY_IDENTIFIABLE_ANONYMOUS_AUTHENTICATION_FILTER,
                userDetailsService.loadCurrentUser(request),
                grantedAuthorityService.getGrantedAuthoritiesForCurrentUser());
        auth.setDetails(authenticationDetailsSource.buildDetails(request));
        return auth;
    }
}

to inject it into configuration

@Configuration
public class IdentifyAnonymousConfigurationAdapter extends WebSecurityConfigurerAdapter {
    @Autowired
    private IdentifiableAnonymousAuthenticationFilter identifiableAnonymousAuthenticationFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.anonymous().authenticationFilter(identifiableAnonymousAuthenticationFilter);
    // ... some other configurations
    }
}

Now it seems much better, because identifiableAnonymousAuthenticationFilter is injected in AnonymousConfigurer. Pay your attention to your configurations based on WebSecurityConfigurerAdapter. If you have few ones and one of them will not set customAnonymousAuthenticationFilter but configured earlier than custom.. you'll get default instance of AnonymousAuthenticationFilter (configured in WebSecurityConfigurerAdapter by default):

  protected final HttpSecurity getHttp() throws Exception {
      //...
      http
        .csrf().and()
        .addFilter(new WebAsyncManagerIntegrationFilter())
        .exceptionHandling().and()
        .headers().and()
        .sessionManagement().and()
        .securityContext().and()
        .requestCache().and()
        .anonymous().and()
      // ...

I would not care about it if application fixed, but AnonymousAuthenticationFilter called earlier than IdentifiableAnonymousAuthenticationFilter. And doFilter puts into SecurityContextHolder incorrect Authentication.

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
    if(SecurityContextHolder.getContext().getAuthentication() == null) {
        SecurityContextHolder.getContext().setAuthentication(this.createAuthentication((HttpServletRequest)req));
        if(this.logger.isDebugEnabled()) {
            this.logger.debug("Populated SecurityContextHolder with anonymous token: '" + SecurityContextHolder.getContext().getAuthentication() + "'");
        }
    } else if(this.logger.isDebugEnabled()) {
        this.logger.debug("SecurityContextHolder not populated with anonymous token, as it already contained: '" + SecurityContextHolder.getContext().getAuthentication() + "'");
    }

    chain.doFilter(req, res);
}

So when next time doFilter is called for IdentifiableAnonymousAuthenticationFilter it does not replace the Authentication because of condition if(SecurityContextHolder.getContext().getAuthentication() == null) (see method before).

As result would be really good to provide configuration where fix for WebSecurityConfigurerAdapter configuration using magic annotation @Order to manage configuration loading order.

Warning

Or someone could think - add doFilter overriding in IdentifiableAnonymousAuthenticationFilter without condition (it is hack):

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        SecurityContextHolder.getContext().setAuthentication(createAuthentication((HttpServletRequest) req));
        if (logger.isDebugEnabled()) {
            logger.debug("Populated SecurityContextHolder with anonymous token: '"
                    + SecurityContextHolder.getContext().getAuthentication() + "'");
        }
        chain.doFilter(req, res);
    }

It is not acceptable if you need spring security with handling authorized/authenticated user but in some cases it is enough.

P.S.

Some parts of the solution could be improved but I hope that idea is clear in general.