Issue
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
Solution
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.
Answered By - Sergii
Answer Checked By - Willingham (JavaFixing Volunteer)