Announcement Announcement Module
Collapse
No announcement yet.
SpringSecurity 3.x - Custom ConcurrentSessionControl problem Page Title Module
Move Remove Collapse
This topic is closed
X
X
Conversation Detail Module
Collapse
  • Filter
  • Time
  • Show
Clear All
new posts

  • SpringSecurity 3.x - Custom ConcurrentSessionControl problem

    Hi all,

    I am moving my application from Spring Security 2.x to 3.x and I am having some issues.
    What I had earlier is that Spring Security was preventing that same user (same credentials) can log to my application from two different PC's (or same machine but two different browsers).
    For this I was using ConcurrentSessionControl.
    Now with Spring Security 3.0 I am almost done with moving existing features to new configuration but I have still this problem.

    First of all there is web-security-servlet.xml

    Code:
    	<http auto-config="true">
    		<intercept-url pattern="/login.htm" filters="none" />
    		<intercept-url pattern="/main_dashboard.htm" access="ROLE_SYSTEM_ADMIN,ROLE_STATISTICS_USER,ROLE_ZABBIX_USER,ROLE_PERFORMANCE_USER,ROLE_USER_MANAGEMENT_USER,ROLE_APPLICATION_MANAGEMENT_USER"/>
    		<intercept-url pattern="/statistics/**" access="ROLE_SYSTEM_ADMIN,ROLE_STATISTICS_USER"/>
    		<intercept-url pattern="/performance/system_monitoring.htm" access="ROLE_SYSTEM_ADMIN,ROLE_ZABBIX_USER"/>
    		<intercept-url pattern="/performance/**" access="ROLE_SYSTEM_ADMIN,ROLE_PERFORMANCE_USER"/>
    		<intercept-url pattern="/provisioning/user**" access="ROLE_SYSTEM_ADMIN,ROLE_USER_MANAGEMENT_USER"/>
    		<intercept-url pattern="/provisioning/**" access="ROLE_SYSTEM_ADMIN,ROLE_APPLICATION_MANAGEMENT_USER"/>
    		<form-login login-page="/login.htm" authentication-success-handler-ref="customAuthenticationSuccessHandler" authentication-failure-handler-ref="exceptionMappingAuthenticationFailureHandler" />
    		<session-management invalid-session-url="/error_session_timeout.htm">
    			<concurrency-control error-if-maximum-exceeded="true" max-sessions="1" session-registry-ref="sessionRegistry" />
    		</session-management>
    	</http>
    
    	<beans:bean id="sessionRegistry" class="org.springframework.security.core.session.SessionRegistryImpl" />
    	
    	<beans:bean id="customAuthenticationSuccessHandler" class="com.one2many.cmspgw.gui.web.handler.CustomAuthenticationSuccessHandler"
    		p:defaultTargetUrl="/main_dashboard.htm"/>
    	
    	<beans:bean id="exceptionMappingAuthenticationFailureHandler" class="com.one2many.cmspgw.gui.web.handler.CustomExceptionMappingAuthenticationFailureHandler">
    		<beans:property name="exceptionMappings">
    			<beans:props>				
    				<beans:prop key="org.springframework.security.web.authentication.session.SessionAuthenticationException">/error_concurent_session.htm</beans:prop>
    				<beans:prop key="org.springframework.security.authentication.BadCredentialsException">/login.htm?login_error=1</beans:prop>
    				<beans:prop key="org.springframework.security.authentication.CredentialsExpiredException">/login.htm?login_error=2</beans:prop>
    				<beans:prop key="org.springframework.security.authentication.LockedException">/login.htm?login_error=3</beans:prop>
    				<beans:prop key="org.springframework.security.authentication.DisabledException">/login.htm?login_error=4</beans:prop>
    				<beans:prop key="org.springframework.security.core.AuthenticationException">/login.htm?login_error=100</beans:prop>
    			</beans:props>
    		</beans:property>
    		<beans:property name="defaultFailureUrl" value="/login.htm?login_error=1"></beans:property>
    	</beans:bean>
    
    	<beans:bean id="securityContextLogoutHandler" class="org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler" 
    		p:invalidateHttpSession="true" />
    	
    	<authentication-manager alias="authenticationManager">
    		<authentication-provider>
    			<user-service id="userDetailService">
    				<user name="sysadmin" password="sysadmin" authorities="ROLE_SYSTEM_ADMIN"/>
    				<user name="zabbix" password="zabbix" authorities="ROLE_ZABBIX_USER" />
    				<user name="performance_gui" password="performance_gui" authorities="ROLE_PERFORMANCE_USER" />
    				<user name="statistics_gui" password="statistics_gui" authorities="ROLE_STATISTICS_USER" />
    				<user name="user_management" password="user_management" authorities="ROLE_USER_MANAGEMENT_USER" />
    				<user name="app_management" password="app_management" authorities="ROLE_APPLICATION_MANAGEMENT_USER" />
    			</user-service>
    		</authentication-provider>
    	</authentication-manager>
    </beans:beans>
    If user is successfully authenticated I want to insert that information in DB (for some later UserActions logs), but anyway here is the code:
    Code:
    ... ommited here for clarity ...
    public class CustomAuthenticationSuccessHandler extends
    		SimpleUrlAuthenticationSuccessHandler {
    
    ... omitted for clarity ... 
    	@Override
    	public void onAuthenticationSuccess(HttpServletRequest request,
    			HttpServletResponse response, Authentication authentication)
    			throws IOException, ServletException {
    		String username = ((UserDetails) authentication.getPrincipal())
    				.getUsername();
    		String loggedInIP = request.getRemoteAddr();
    		log
    				.info(
    						"+++ User with username: {} authenticated. IPAddress logging from: {}.",
    						username, loggedInIP);
    		userActionsBO.save(username, UserActionType.LOGIN,
    				"User logged in from IP Address: [" + loggedInIP + "]",
    				loggedInIP);
    		super.onAuthenticationSuccess(request, response, authentication);
    	}
    }
    In case that there is some exception during authentication then I have CustomExceptionMappingAuthenticationHandler. Why it is custom? Well I need info (ip address) where user was last successfully logged in, so I am tempering redirect URL (adding more params):
    Code:
     ... ommited here for clarity ... 
    public class CustomExceptionMappingAuthenticationFailureHandler extends
    		ExceptionMappingAuthenticationFailureHandler {
    
             ... omitted for clarity ...
    	/**
    	 * Must override in order to get last login IPAddress
    	 */
    	@Override
    	public void onAuthenticationFailure(HttpServletRequest request,
    			HttpServletResponse response, AuthenticationException exception)
    			throws IOException, ServletException {
    		String username = request.getParameter("j_username");
    		log.debug("+++ Authentication failure occurs for username: {}",
    				username);
    		String url = failureUrlMap.get(exception.getClass().getName());
    		String lastLoginIPAddress = userActionsBO
    				.getLastLoggedInIPAddress(username);
    		url = url + "?lastLoginIPAddress=" + lastLoginIPAddress + "&username="
    				+ username;
    		if (url != null) {
    			getRedirectStrategy().sendRedirect(request, response, url);
    		} else {
    			super.onAuthenticationFailure(request, response, exception);
    		}
    	}
     ... setting failure exceptions map, ommited for clarity ...
    
    }
    And now last but more interesting part I want to have custom LogoutController, so when I call "/logout.htm?username=[some_username]", I am using logoutHandler in order to invalidate previous session and to re-authenticate user with this new session.

    Here is the code:
    Code:
    ... ommited for clarity ...
    @Controller
    @RequestMapping("/logout.htm")
    public class LogoutController implements ApplicationContextAware {
    
     ... omitted for clarity ...
    	@Autowired
    	private AuthenticationManager authenticationManager;
    	@RequestMapping(method = RequestMethod.GET, params = { "username" })
    	public ModelAndView invalidateOtherSession(HttpServletRequest request, HttpServletResponse response,
    			@RequestParam(required = true) String username) {
    
    		List<SessionInformation> sessionInformations = sessionRegistry.getAllSessions(username, true);
    		for (SessionInformation sessionInformation : sessionInformations) {
    			log.debug("+++++ Session Information: " + sessionInformation.getSessionId());
    			if (request.getSession().getId().equals(sessionInformation.getSessionId())) {
    				log.debug("+++ Don't invalidate current session! +++");
    			} else {
    				log.debug("+++ Invalidate other session and continue to work in current session +++");
    				sessionInformation.expireNow();
    				// Need to reauthenticate
    				// FIXME: a.stoisavljevic - implement userBO.findByUsername(String username) return user
    				// password should be switched to use from User object as well
    				String password = request.getParameter("j_password");
    				Authentication authRequest = new UsernamePasswordAuthenticationToken(username, password);
    				Authentication authResult = ((ProviderManager)authenticationManager).getProviders().get(0).authenticate(authRequest);
    				SecurityContextHolder.getContext().setAuthentication(authResult);
    				if ( authResult.isAuthenticated() ) {
    					log.debug("+++ User is reauthenticated! +++");
    					appContext.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
    				} else {
    					log.debug("+++ Can't publish event! +++");
    				}
    			}
    		}
    
    		/*
    		for (int i = 0; sessionInformations != null && i < sessionInformations.length; i++) {
    			log.debug("+++++ Session Information: " + sessionInformations[i].getSessionId());
    			if (request.getSession().getId().equals(sessionInformations[i].getSessionId())) {
    				log.debug("+++++ Doesn't invalidate current session! +++++");
    			} else {
    				log.debug("+++++ Invalidate other session and continue to work! +++++");
    				sessionInformations[i].expireNow();
    				// Reauthenticate
    				User user = userBO.findByUsername(username);
    				Authentication authRequest = new UsernamePasswordAuthenticationToken(username, user.getPassword());
    				Authentication authResult = authenticationProvider.authenticate(authRequest);
    				SecurityContextHolder.getContext().setAuthentication(authResult);
    				if (authResult.isAuthenticated()) {
    					log.debug("+++++ User is reauthenticated! +++++");
    					appContext.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
    				} else {
    					log.debug("+++++ Can't publish event! +++++");
    				}
    			}
    		}
    		 **/
    		return new ModelAndView(MAIN_DASHBOARD_REDIRECT_URL);
    	}
    
    
    }

  • #2
    CONT: Spring Security 3.x - ConcurrentSessionControl

    My problem now lies in fact that LogoutController, line with List<SessionInformation> sessionInformations = sessionRegistry.getAllSessions(username, true);
    returns empty list, as there is no previous sessions for this user. But this is not the case since I am logging from FF and there I succeed, but I really get an error when I try to log from IE with same credentials. Can someone point me where I am making mistake and how to solve my problem ?

    Comment


    • #3
      Am I missing the bit of configuration when you've mapped the normal logout URL j_spring_security_logout to your logout controller? I guess it's not clear to me the sequence of events involved in your problem - can you try writing it up in sequence? e.g.

      1. User logs into FF.
      2. User ...

      That might help us figure out what's wrong.

      Comment


      • #4
        SpringSecurity 3.x - Custom ConcurrentSessionControl problem

        First of all thanks for your time and involvement.

        As response to your question, as you can assume I am not using "default" logout filter (as it should stand in <http> element). My option was to have custom LogoutController and same time I am having in my JSP page logout link "/logout.htm". LogoutController on plain RequestMethod.GET has following method:

        Code:
        	@RequestMapping(method = RequestMethod.GET)
        	public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) {
        		log.trace("+++ LogoutController - handleRequest method - ENTER +++");
        		Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        		String username = auth.getName();
        		log.debug("+++ Authentication to clear for user: {}", username);
        		request.getSession().invalidate();
        		logoutHandler.logout(request, response, auth);
        		userActionsBO.save(username, UserActionType.LOGOUT, "User logged out", EMPTY_IP_ADDRESS);
        		log.debug("+++ Logout handler executed for user!");
        		log.trace("+++ LogoutController - handleRequest method - LEAVE +++");
        		return new ModelAndView(LOGOUT_REDIRECT_URL);
        	}
        That part of LogoutController I intentionally left-out in first post since this part if working.

        Second method of this LogoutController, specially that sessionRegistry.getAllSessions(username, true) doesn't work for me as it worked in previous Spring Security 2.x configuration.

        One more thing that I suspect that is causing the problem is order of execution of filters.

        Previously (in Spring 2.x configuration my ConcurrentSession was first and authentication came after). Now as I suppose, Concurrent is coming after successful authentication.

        But I am not so good in Spring Security that I can be 100% sure what I am talking about

        Anyway thanks again for your will to help me out with this problem.

        Comment


        • #5
          SpringSecurity 3.x - Custom ConcurrentSessionControl problem

          Also here are two sequences:

          I Normal execution

          1. User enters URL in browser (e.g. localhost:8080/gui/).
          2. <form-login> element redirects user to custom login.htm page (there is LoginController and custom view). Custom view has form with post method /j_spring_security and two fields j_username and j_password
          3. In case that authentication succeed then customSuccessHandler enters information to DB and redirect user to main_dashboard.htm
          4. There is /logout.htm link on top of the page and when user clicks on that link LogoutController execute his own method (@see latest post) and user is loggedout, security context cleared and user sent to /login.htm page

          II Concurrent session failure execution
          1. User enters URL in browser (e.g. localhost:8080/gui/).
          2. <form-login> element redirects user to custom login.htm page (there is LoginController and custom view). Custom view has form with post method /j_spring_security and two fields j_username and j_password
          3. After user is authenticated it is redirected to /main_dashboard.htm

          4. Open now different browser (trying to simulate creating new session - intention is to prevent user to login from any other machine with same credentials)
          5. execute step 1. and 2.
          6. ConcurrentSession detects that there is already session with this user credentials so there is execption in Authentication
          7. since I've got failure, instead that customSuccessAuthenticationHandler is used, customExceptionMappingAuthenticationHandler is called.
          8. ConcurrentSessionException redirects view to "customErrorConcurentSession" view.
          9. That page except it shows that ConcurentSession occurred has two links
          1st link is 'Invalidate other session': that should trigger that previously taken session should be terminated
          2nd link is 'Cancel' and redirects user to "/login.htm" page
          10. Invalidate other session also calls LogoutHandler but with extra params (ipAddress - not important for this story, username - in order to get all opened sessions for this user in sessionRegistry)

          Hope this explanation helps

          Comment


          • #6
            SpringSecurity 3.x - Custom ConcurrentSessionControl problem

            I've isolated where I was making mistake.


            Code:
                    final Set<String> sessionsUsedByPrincipal = principals.get(principal);
            
                    if (sessionsUsedByPrincipal == null) {
                        return Collections.emptyList();
                    }
            this code is first few lines of getAllSessions method of SessionRegistryImpl class. I was passing String username, while Map of principals expected to have Object principal as key to this Map. Since this Set was null Collections returned empty list.

            Then I tried to get Principal as I always do:

            Code:
            SecurityContextHolder.getContext().getAuthentication().getPrincipal()
            but for later login this principal is "anonymousUser".

            With a little hack by using sessionRegistry.getAllPrincipals() and comparing usernames it worked.

            Good for me Anyway if you have any other good suggestion I am willing to accept it.

            Thank you.

            Comment


            • #7
              Congratulations! Sorry, I was tied up this weekend and didn't get a chance to review your very thorough response. I will review this evening and see if I have any other tips. One common pitfall here is to make sure that your UserDetails object (if it's not one of the out-of-the-box ones) needs to correctly implement equals and hashCode.

              Comment


              • #8
                SpringSecurity 3.x - Custom ConcurrentSessionControl problem

                Once again, thank you for your time. I didn't expect that you will find time for weekend.
                Even I managed to make some time after I put my kid to sleep on Sunday evening But what a hell, this one was on my mind whole weekend

                Anyway I think that I still have one bug (at least it seams that I have - it is not so obvious) but first of all I need to make it reproducable and then I will get back to you with my findings.

                Here you soon.

                Comment


                • #9
                  Actually sounds like an interesting strategy you've implemented. My only question is - at the "invalidate session" page, how is the user selecting the username to invalidate? A hidden form or URL parameter? This is a potential risk, because at this point the user isn't authenticated (right?).

                  Comment


                  • #10
                    SpringSecurity 3.x - Custom ConcurrentSessionControl problem

                    You are totaly right.

                    User ends up on ConcurrentSession error page in case that there is an error in authentication (@see customExceptionMappings bean).

                    Since I don't have username, in order to invalidate previous existing session, I am tempering redirect url, concatenating username as param.

                    Well, I don't know if this is best solution but ... It works.

                    Again, I think that I still have just one unwanted behaviour, but first let me make some dummy Maven project that I will attach and if you are still interested to discuss about my solution then you could downloaded it and have same project as me. But this is an option, not mandatory for you. Anyway your help is more then welcomed.

                    Comment


                    • #11
                      Sure, I'm always interested to see innovative ways people use Spr Sec

                      Comment

                      Working...
                      X