Dmytro Grynets
Dmytro Grynets

Reputation: 953

Spring security use wrong security context when there are two of them

I have 2 types of users, Schools and Volunteers, Each have their own UserDetails implementation, and UserDetailsService implementations, also each have different http configuration:

This one is for schools

protected void configure(HttpSecurity http) throws Exception {
        http
                .antMatcher("/school/**")
                .userDetailsService(service)
                .formLogin().loginPage("/school/login").permitAll().usernameParameter("username")
                .passwordParameter("password").failureUrl("/school/login?error=true")
                .and()
                .authorizeRequests()
                .antMatchers("/school/register").permitAll()
                .antMatchers("/school/confirm**").permitAll()
                .antMatchers("/school/**").hasRole(School.class.getSimpleName())
                .and()
                .rememberMe().tokenValiditySeconds(2592000).rememberMeParameter("remember-me")
                .userDetailsService(service).key("S").rememberMeCookieName("S")
                .and()
                .logout().logoutSuccessUrl("/").logoutUrl("/school/logout");
    }

And this one is for volunteers

protected void configure(HttpSecurity http) throws Exception {
            http
                    .antMatcher("/volunteer/**")
                    .userDetailsService(service)
                    .formLogin().loginPage("/volunteer/login").permitAll().usernameParameter("username")
                    .passwordParameter("password").failureUrl("/volunteer/login?error=true")
                    .and()
                    .authorizeRequests()
                    .antMatchers("/volunteer/register").permitAll()
                    .antMatchers("/volunteer/confirm**").permitAll()
                    .antMatchers("/volunteer/**").hasRole(Volunteer.class.getSimpleName())
                    .and()
                    .rememberMe().tokenValiditySeconds(2592000).rememberMeParameter("remember-me")
                    .userDetailsService(service).key("V").rememberMeCookieName("V")
                    .and()
                    .logout().logoutSuccessUrl("/").logoutUrl("/volunteer/logout");
        }

In both service is their user details service implementation They are configured in two nested classes that extends WebSecurityConfigurerAdapter. So my problem is that when i log in as school, and then go to (for example) http://localhost/volunteer, it shows 403 forbidden instead of login form. I turned on debug and saw that it just found authenticated user in security context and use it, and while it has no required role it fails with 403, then if i explicitly go to login page for volunteers and login, it then couldn't login as school with the same 403. Here is logs:

This and etc. for login as school:

    17:11:50.628 [http-nio-8081-exec-9] DEBUG security.web.util.matcher.AntPathRequestMatcher: Checking match of request : '/school/login'; against '/volunteer/**'
17:11:50.629 [http-nio-8081-exec-9] DEBUG security.web.util.matcher.AntPathRequestMatcher: Checking match of request : '/school/login'; against '/school/**'
17:11:50.629 [http-nio-8081-exec-9] DEBUG security.web.FilterChainProxy: /school/login at position 1 of 15 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'
17:11:50.629 [http-nio-8081-exec-9] DEBUG security.web.FilterChainProxy: /school/login at position 2 of 15 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
17:11:50.629 [http-nio-8081-exec-9] DEBUG security.web.context.HttpSessionSecurityContextRepository: HttpSession returned null object for SPRING_SECURITY_CONTEXT
17:11:50.629 [http-nio-8081-exec-9] DEBUG security.web.context.HttpSessionSecurityContextRepository: No SecurityContext was available from the HttpSession: org.apache.catalina.session.StandardSessionFacade@1dd229eb. A new one will be created.
17:11:50.629 [http-nio-8081-exec-9] DEBUG security.web.FilterChainProxy: /school/login at position 3 of 15 in additional filter chain; firing Filter: 'HeaderWriterFilter'
17:11:50.630 [http-nio-8081-exec-9] DEBUG security.web.header.writers.HstsHeaderWriter: Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher@19d9486b
17:11:50.630 [http-nio-8081-exec-9] DEBUG security.web.FilterChainProxy: /school/login at position 4 of 15 in additional filter chain; firing Filter: 'CharacterEncodingFilter'
17:11:50.630 [http-nio-8081-exec-9] DEBUG security.web.FilterChainProxy: /school/login at position 5 of 15 in additional filter chain; firing Filter: 'CsrfFilter'
17:11:50.630 [http-nio-8081-exec-9] DEBUG security.web.FilterChainProxy: /school/login at position 6 of 15 in additional filter chain; firing Filter: 'LogoutFilter'
17:11:50.630 [http-nio-8081-exec-9] DEBUG security.web.util.matcher.AntPathRequestMatcher: Checking match of request : '/school/login'; against '/school/logout'
17:11:50.630 [http-nio-8081-exec-9] DEBUG security.web.FilterChainProxy: /school/login at position 7 of 15 in additional filter chain; firing Filter: 'UsernamePasswordAuthenticationFilter'
17:11:50.630 [http-nio-8081-exec-9] DEBUG security.web.util.matcher.AntPathRequestMatcher: Checking match of request : '/school/login'; against '/school/login'
17:11:50.630 [http-nio-8081-exec-9] DEBUG security.web.authentication.UsernamePasswordAuthenticationFilter: Request is to process authentication

And then when i go to volunteer section:

 17:15:03.860 [http-nio-8081-exec-5] DEBUG security.web.util.matcher.AntPathRequestMatcher: Checking match of request : '/volunteer'; against '/volunteer/**'
17:15:03.861 [http-nio-8081-exec-5] DEBUG security.web.FilterChainProxy: /volunteer at position 1 of 15 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'
17:15:03.861 [http-nio-8081-exec-5] DEBUG security.web.FilterChainProxy: /volunteer at position 2 of 15 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
17:15:03.861 [http-nio-8081-exec-5] DEBUG security.web.context.HttpSessionSecurityContextRepository: Obtained a valid SecurityContext from SPRING_SECURITY_CONTEXT: 'org.springframework.security.core.context.SecurityContextImpl@b35de31c: Authentication: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@b35de31c: Principal: com.primaryrun.model.school.SecureSchool@16d; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@166c8: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: EC164B454F5406F933CCB3C6CFEF2E1D; Granted Authorities: ROLE_School'
17:15:03.861 [http-nio-8081-exec-5] DEBUG security.web.FilterChainProxy: /volunteer at position 3 of 15 in additional filter chain; firing Filter: 'HeaderWriterFilter'
17:15:03.861 [http-nio-8081-exec-5] DEBUG security.web.header.writers.HstsHeaderWriter: Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher@439cf926
17:15:03.861 [http-nio-8081-exec-5] DEBUG security.web.FilterChainProxy: /volunteer at position 4 of 15 in additional filter chain; firing Filter: 'CharacterEncodingFilter'
17:15:03.861 [http-nio-8081-exec-5] DEBUG security.web.FilterChainProxy: /volunteer at position 5 of 15 in additional filter chain; firing Filter: 'CsrfFilter'
17:15:03.861 [http-nio-8081-exec-5] DEBUG security.web.FilterChainProxy: /volunteer at position 6 of 15 in additional filter chain; firing Filter: 'LogoutFilter'
17:15:03.861 [http-nio-8081-exec-5] DEBUG security.web.util.matcher.AntPathRequestMatcher: Request 'GET /volunteer' doesn't match 'POST /volunteer/logout
17:15:03.861 [http-nio-8081-exec-5] DEBUG security.web.FilterChainProxy: /volunteer at position 7 of 15 in additional filter chain; firing Filter: 'UsernamePasswordAuthenticationFilter'
17:15:03.864 [http-nio-8081-exec-5] DEBUG security.web.util.matcher.AntPathRequestMatcher: Request 'GET /volunteer' doesn't match 'POST /volunteer/login
17:15:03.864 [http-nio-8081-exec-5] DEBUG security.web.FilterChainProxy: /volunteer at position 8 of 15 in additional filter chain; firing Filter: 'BasicAuthenticationFilter'
17:15:03.864 [http-nio-8081-exec-5] DEBUG security.web.FilterChainProxy: /volunteer at position 9 of 15 in additional filter chain; firing Filter: 'RequestCacheAwareFilter'
17:15:03.864 [http-nio-8081-exec-5] DEBUG security.web.FilterChainProxy: /volunteer at position 10 of 15 in additional filter chain; firing Filter: 'SecurityContextHolderAwareRequestFilter'
17:15:03.864 [http-nio-8081-exec-5] DEBUG security.web.FilterChainProxy: /volunteer at position 11 of 15 in additional filter chain; firing Filter: 'RememberMeAuthenticationFilter'
17:15:03.864 [http-nio-8081-exec-5] DEBUG security.web.authentication.rememberme.RememberMeAuthenticationFilter: SecurityContextHolder not populated with remember-me token, as it already contained: 'org.springframework.security.authentication.UsernamePasswordAuthenticationToken@b35de31c: Principal: com.primaryrun.model.school.SecureSchool@16d; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@166c8: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: EC164B454F5406F933CCB3C6CFEF2E1D; Granted Authorities: ROLE_School'
17:15:03.864 [http-nio-8081-exec-5] DEBUG security.web.FilterChainProxy: /volunteer at position 12 of 15 in additional filter chain; firing Filter: 'AnonymousAuthenticationFilter'
17:15:03.864 [http-nio-8081-exec-5] DEBUG security.web.authentication.AnonymousAuthenticationFilter: SecurityContextHolder not populated with anonymous token, as it already contained: 'org.springframework.security.authentication.UsernamePasswordAuthenticationToken@b35de31c: Principal: com.primaryrun.model.school.SecureSchool@16d; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@166c8: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: EC164B454F5406F933CCB3C6CFEF2E1D; Granted Authorities: ROLE_School'
17:15:03.864 [http-nio-8081-exec-5] DEBUG security.web.FilterChainProxy: /volunteer at position 13 of 15 in additional filter chain; firing Filter: 'SessionManagementFilter'
17:15:03.864 [http-nio-8081-exec-5] DEBUG security.web.FilterChainProxy: /volunteer at position 14 of 15 in additional filter chain; firing Filter: 'ExceptionTranslationFilter'
17:15:03.864 [http-nio-8081-exec-5] DEBUG security.web.FilterChainProxy: /volunteer at position 15 of 15 in additional filter chain; firing Filter: 'FilterSecurityInterceptor'
17:15:03.864 [http-nio-8081-exec-5] DEBUG security.web.util.matcher.AntPathRequestMatcher: Checking match of request : '/volunteer'; against '/volunteer/register'
17:15:03.864 [http-nio-8081-exec-5] DEBUG security.web.util.matcher.AntPathRequestMatcher: Checking match of request : '/volunteer'; against '/volunteer/confirm**'
17:15:03.865 [http-nio-8081-exec-5] DEBUG security.web.util.matcher.AntPathRequestMatcher: Checking match of request : '/volunteer'; against '/volunteer/**'
17:15:03.865 [http-nio-8081-exec-5] DEBUG security.web.access.intercept.FilterSecurityInterceptor: Secure object: FilterInvocation: URL: /volunteer; Attributes: [hasRole('ROLE_Volunteer')]
17:15:03.865 [http-nio-8081-exec-5] DEBUG security.web.access.intercept.FilterSecurityInterceptor: Previously Authenticated: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@b35de31c: Principal: com.primaryrun.model.school.SecureSchool@16d; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@166c8: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: EC164B454F5406F933CCB3C6CFEF2E1D; Granted Authorities: ROLE_School
17:15:03.865 [http-nio-8081-exec-5] DEBUG security.access.vote.AffirmativeBased: Voter: org.springframework.security.web.access.expression.WebExpressionVoter@3466a837, returned: -1
17:15:03.887 [http-nio-8081-exec-5] DEBUG context.support.ReloadableResourceBundleMessageSource: No properties file found for [classpath:localization/messages_en_US] - neither plain properties nor XML
17:15:03.888 [http-nio-8081-exec-5] DEBUG context.support.ReloadableResourceBundleMessageSource: Re-caching properties for filename [classpath:localization/messages_en] - file hasn't been modified
17:15:03.889 [http-nio-8081-exec-5] DEBUG context.support.ReloadableResourceBundleMessageSource: No properties file found for [classpath:localization/messages] - neither plain properties nor XML
17:15:03.890 [http-nio-8081-exec-5] DEBUG security.web.access.ExceptionTranslationFilter: Access is denied (user is not anonymous); delegating to AccessDeniedHandler

Is it possible in spring security to be logged in as different users at the same time?

If so, how to achieve this? Do I have to somehow create new security context, authentication provider or some other stuff?

Why does it use Schools' security context if i am at the volunteers side?

Upvotes: 0

Views: 1751

Answers (1)

Dmytro Grynets
Dmytro Grynets

Reputation: 953

Solved by adding a filter to spring security filter chain

http.addFilterBefore(new MultipleLoginFilter(), SecurityContextPersistenceFilter.class);

That store/extract security context in session under different name. Used annotation to describe dimension via reflection:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface SecurityDimension {
    String name();
}

That annotate user details implementations. Also configured logout to invalidate only the user that pressed logout:

http.clearAuthentication(true) .invalidateHttpSession(false).logoutSuccessHandler(new SimpleLoginInvalidator())

Where SimpleLoginInvalidator is:

public class SimpleLoginInvalidator implements LogoutSuccessHandler {

private static final Logger logger = LoggerFactory.getLogger(SimpleLoginInvalidator.class);

@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
    HttpSession session = request.getSession();
    try {
        String dimensionName = authentication.getPrincipal().getClass().getAnnotation(SecurityDimension.class).name();
        session.removeAttribute(dimensionName + "_" + SPRING_SECURITY_CONTEXT_KEY);
        logger.debug("Cleared security context for dimension: " + dimensionName);
    } catch (NullPointerException npe) {
        logger.debug("Could not clear custom security context attribute", npe);
    }
}

}

Hope it helps someone, still looking for more 'out of the box' answer

MultipleLoginFilter :

public class MultipleLoginFilter extends OncePerRequestFilter {

private static final Pattern pattern = Pattern.compile("(?<!/)/([^/?#]+)");

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    logger.debug("Performing multiple login management");
    HttpSession session = request.getSession(false);

    String path = request.getRequestURI().replace(request.getContextPath(), "");
    String desiredDimension = getDesiredDimension(path);
    logger.debug("Requesting security context for security dimension: " + desiredDimension);

    SecurityContext context = (SecurityContext) session.getAttribute(SPRING_SECURITY_CONTEXT_KEY);
    if (context == null) {
        logger.debug("Current context is empty, trying to find and associate the one stored session...");
        populateSecurityContextByDimension(session, desiredDimension);
    } else {
        logger.debug("Extracted non-null current security context: " + context);
        String dimensionName = context.getAuthentication().getPrincipal().getClass().getAnnotation(SecurityDimension.class).name();
        if (!path.matches("/" + dimensionName + "(/.+)?")) {
            logger.debug("Extracted security context is not associated with desired security dimension(" + desiredDimension + "). Storing in session...");
            session.setAttribute(dimensionName + "_" + SPRING_SECURITY_CONTEXT_KEY, context);
            populateSecurityContextByDimension(session, desiredDimension);
        } else {
            logger.debug("Current security context is associated with desired security dimension, proceed as default.");
        }
    }
    filterChain.doFilter(request, response);
}

private String getDesiredDimension(String requestUrl) {

    Matcher m = pattern.matcher(requestUrl);
    String desiredDimension = "";
    if (m.find() && m.groupCount() == 1) {
        desiredDimension = m.group(1);
    } else {
        logger.error("Could not identify desired security dimension from url: " + requestUrl);
    }
    return desiredDimension;
}

private void populateSecurityContextByDimension(HttpSession session, String desiredDimension) {
    logger.debug("Extracting stored security context associated with desired dimension(" + desiredDimension + ")...");
    SecurityContext localContext = (SecurityContext) session.getAttribute(desiredDimension + "_" + SPRING_SECURITY_CONTEXT_KEY);
    session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, localContext);
    if(localContext != null) {
        logger.debug("Storing extracted context as current, context: " + localContext);
    } else {
        logger.debug("No security context for dimension(" + desiredDimension + ") in session, assigning null.");
    }
}

}

Upvotes: 1

Related Questions