0

I'm migrating a quite old application in Spring 3.1.1 + Spring Security 3.1.8, that works fine in JBossEAP 7.3 + Java8,
to Spring 5.3.29 + Spring Security 5.7.9, executed by WildFly26 + Java11.
I use xml notation for Spring context declaration.

The application starts and the context is loaded fine with no errors, the problem comes after a success login call, I get an Anonymous user at the default-target-url="/loginSuccessful.htm page, due to a null securityContext in the SecurityContextHolderFilter just before that page (in the following configuration see that I commented the "isFullyAuthenticated" intercept-url check to loginSuccessful.htm for debugging, alternatively I've been redirected to login page).

In the migration I enabled the security-context-explicit-save and added HttpSessionSecurityContextRepository as securityContextRepository (that is the default) as requested by documentation and disabled headers and csrf.

From debug I see that the POST to /j_spring_security_check produces an authentication with success and the permissions are retrieved correctly: the AbstractAuthenticationProcessingFilter, in successfulAuthentication performs the context saving by calling this.securityContextRepository.saveContext by passing a UsernamePasswordAuthenticationToken (where this.securityContextRepository is a HttpSessionSecurityContextRepository object saved to SPRING_SECURITY_CONTEXT property name), and finally calls success authentication function.
In the next SecurityContextHolderFilter, just before the /loginSuccessful.htm, the following line generates a null: SecurityContext securityContext = this.securityContextRepository.loadContext(request).get(); its try to retrieve an HttpSessionSecurityContextRepository by getting a property with name SPRING_SECURITY_CONTEXT

I know that there is something wrong in my configutation, but I don't know what, I have read many question similar to mine, in particular this one, or this other, that, or that other and many other, tried with no luck its solutions. Is it needed to implement a custom UsernamePasswordAuthenticationFilter in substitution to <security:form-login /> tag? In what way? I didn't find any examples

Here my code:
SecurityContext.xml

<security:http
        auto-config="true"
        use-expressions="true"
        disable-url-rewriting="true"
        security-context-explicit-save="true"
        security-context-repository-ref="securityContextRepository"
    >

    <security:intercept-url pattern="/login.**" access="permitAll" />
<!--    <security:intercept-url pattern="/loginSuccessful.htm" access="isFullyAuthenticated()" /> -->

    <security:headers disabled="true"/>
    <security:csrf disabled="true"/>        
    <security:form-login
            login-page="/login.htm"
            login-processing-url="/j_spring_security_check"
            username-parameter="j_username"
            password-parameter="j_password"
            authentication-failure-url="/loginFailed.htm"
            default-target-url="/loginSuccessful.htm"
            authentication-details-source-ref="customAuthenticationDetailsSource"
            always-use-default-target="false"
    />
    <security:logout
            invalidate-session="true"
            logout-success-url="/logoutSuccessful.htm"
            delete-cookies="JSESSIONID"
    />
    <security:session-management>
        <security:concurrency-control max-sessions="1" />
    </security:session-management>      
    <security:access-denied-handler error-page="/accessDenied.htm" />
</security:http>

<security:debug/>

<security:authentication-manager alias="authenticationManager">
    <security:authentication-provider ref='ldapAuthProvider' />
</security:authentication-manager>

<bean id="securityContextRepository" class="org.springframework.security.web.context.HttpSessionSecurityContextRepository" />

<bean id="contextSource" class="org.springframework.security.ldap.DefaultSpringSecurityContextSource">
    <constructor-arg value="${ldap.ldapUrl}" />
</bean>

<bean id="customAuthenticationDetailsSource" class="my.class.path.security.CustomAuthenticationDetailsSource"/>

<bean id="ldapAuthProvider" class="org.springframework.security.ldap.authentication.LdapAuthenticationProvider">
    <constructor-arg>
        <bean class="my.class.path.web.security.CustomLdapBindAuthenticator">
            <constructor-arg ref="contextSource" />
            <property name="userDnPatterns">
                <list>
                    <value>cn={0},${ldap.fullyQualifiedDN}</value>
                </list>
            </property>
        </bean>
    </constructor-arg>
    <constructor-arg>
        <bean class="my.class.path.security.CustomLdapAuthoritiesPopulator" />
    </constructor-arg>
</bean>

LoginController.java

@Controller
public class LoginController {

    @RequestMapping(value="/login.htm", method = RequestMethod.GET)
    public String showLogin(Model model,HttpSession httpSession) {
        httpSession.invalidate();
        
        Boolean fromLogin = true;   
        model.addAttribute("fromLogin", fromLogin);
        
        return "login";
    }
    
    @RequestMapping(value="/loginSuccessful.htm", method = RequestMethod.GET)
    public ModelAndView loginAccess(Model model, HttpServletRequest request, HttpServletResponse response, Locale locale, Authentication authen, Principal princ) throws NoSuchMessageException, Exception {

        HttpSessionSecurityContextRepository secRepo = new HttpSessionSecurityContextRepository();
        Authentication auth = secRepo.loadContext(request).get().getAuthentication();  
    
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        Authentication authentication3 = SecurityContextHolder.getContextHolderStrategy().getContext().getAuthentication();
}

See that I tried to get the Principal and Authentication in many ways, but always null

Here is the filter registration from logs:

[INFO] 2023-07-20 16:43:21,906 HttpSecurityBeanDefinitionParser.checkFilterChainOrder:283 -> Checking sorted filter chain: [
Root bean: class [org.springframework.security.web.session.DisableEncodeUrlFilter]; scope=; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null, order = 100,
Root bean: class [org.springframework.security.web.session.ForceEagerSessionCreationFilter]; scope=; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null, order = 200,
Root bean: class [org.springframework.security.web.context.SecurityContextHolderFilter]; scope=; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null, order = 400,
Root bean: class [org.springframework.security.web.session.ConcurrentSessionFilter]; scope=; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null, order = 500,
Root bean: class [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter]; scope=; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null, order = 600,
Root bean: class [org.springframework.security.web.authentication.logout.LogoutFilter]; scope=; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null, order = 1300, <org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter#0>, order = 2100,
Root bean: class [org.springframework.security.web.authentication.www.BasicAuthenticationFilter]; scope=; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null, order = 2700,
Root bean: class [org.springframework.security.web.savedrequest.RequestCacheAwareFilter]; scope=; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null, order = 2800,
Root bean: class [null]; scope=; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.security.config.http.HttpConfigurationBuilder$SecurityContextHolderAwareRequestFilterBeanFactory#0; factoryMethodName=getBean; initMethodName=null; destroyMethodName=null, order = 2900,
Root bean: class [org.springframework.security.web.authentication.AnonymousAuthenticationFilter]; scope=; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null, order = 3200,
Root bean: class [org.springframework.security.web.session.SessionManagementFilter]; scope=; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null, order = 3500,
Root bean: class [org.springframework.security.web.access.ExceptionTranslationFilter]; scope=; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null, order = 3600, <org.springframework.security.web.access.intercept.FilterSecurityInterceptor#0>, order = 3700]

Security Filter Chain from logs:

Security filter chain: [
  DisableEncodeUrlFilter
  ForceEagerSessionCreationFilter
  SecurityContextHolderFilter
  ConcurrentSessionFilter
  WebAsyncManagerIntegrationFilter
  LogoutFilter
  UsernamePasswordAuthenticationFilter
  BasicAuthenticationFilter
  RequestCacheAwareFilter
  SecurityContextHolderAwareRequestFilter
  AnonymousAuthenticationFilter
  SessionManagementFilter
  ExceptionTranslationFilter
  FilterSecurityInterceptor
]

All seems to be OK as I see in the debugger, but where is the problem? any help is appreciated

fl4l
  • 1,580
  • 4
  • 21
  • 27

1 Answers1

0

After days of debugging, I definitively associated the behavior to the session management. I noticed on the browser cookies that now are present multiple entries of JSESSIONID. The old build in Spring 3 / Java 8 running on JBoss 7 was having only one occurrence:

Cookie: JSESSIONID=0F7B1332F8F3D06021746ED547F58B02;JSESSIONID=0F7B1332F8F3D06021746ED547F58B02.20200749-ait; JSESSIONID=S4OXEvR5XykB1YXwIJPnTClFLhiDhyZ7CmsETvkP.20200749-ait; JSESSIONID=y0nsoBTMQumwTi78Zldue-wLx8qPmDnJKJSBUQzz.20200749-ait; _ga=GA1.1.493020884.1689330358; _ga_SR3H6G8EBP=GS1.1.1689330374.1.1.1689330525.0.0.0; preferredLanguage=en

In order to solve, even if probably it is not the best solution, I simply needed to customize the application cookie name in web.xml:

  <session-config>
    <session-timeout>15</session-timeout>
    <cookie-config>
      <name>CUSTOM_SESSION_ID</name>
      <path>/myApplication</path>
      <http-only>true</http-only>
      <secure>true</secure>
    </cookie-config>
    <tracking-mode>COOKIE</tracking-mode>
  </session-config>

Now the cookie is something similar to:

Cookie: JSESSIONID=0F7B1332F8F3D06021746ED547F58B02 JSESSIONID=0F7B1332F8F3D06021746ED547F58B02.20200749-ait; CUSTOM_SESSION_ID=S4OXEvR5XykB1YXwIJPnTClFLhiDhyZ7CmsETvkP.20200749-ait;  _ga=GA1.1.493020884.1689330358; _ga_SR3H6G8EBP=GS1.1.1689330374.1.1.1689330525.0.0.0; preferredLanguage=en

Now I have the custom cookie and also several JSESSIONID, as if it is managed outside the application. Consider that I'm testing the application on my laptop running Wildfly locally, not in an enterprise environment.
I'm currently not able to say if the above behavior of multiple entries of JSESSIONID was generated by WildFly 26 or the newer version of Spring/SpringSecurity, in my researches I don't find anything about it.
Who set the JSESSIONID?
I hope this helps, any clarification is appreciated.

fl4l
  • 1,580
  • 4
  • 21
  • 27