Search code examples
grailsspring-securitygrails-plugin

Grails Spring Security max concurrent session


I have grails 2.5.1 app with spring security plugin(2.0-RC5). I would like to block the number of current session per user. I have read some blog and it doesn't work.(http://www.block-consult.com/blog/2012/01/20/restricting-concurrent-user-sessions-in-grails-2-using-spring-security-core-plugin/) my resources.groovy

beans = {
  sessionRegistry(SessionRegistryImpl)

    concurrencyFilter(ConcurrentSessionFilter,sessionRegistry,'/main/index'){
        logoutHandlers = [ref("rememberMeServices"), ref("securityContextLogoutHandler")]
    }
    concurrentSessionControlStrategy(ConcurrentSessionControlAuthenticationStrategy, sessionRegistry) {
        exceptionIfMaximumExceeded = true
        maximumSessions = 1

    }
}

In my boostrap.groovy

 def init = { servletContext ->
    SpringSecurityUtils.clientRegisterFilter('concurrencyFilter', SecurityFilterPosition.CONCURRENT_SESSION_FILTER)
  }

and my config.groovy I have added this:

grails.plugin.springsecurity.useHttpSessionEventPublisher = true

Thanks..


Solution

  • To start with, let me warn you if you decided to continue with my solution.

    • SessionRegistryImpl is not scalable. You need to create your own scalable implementation based on your scaling plan(e.g. data grid). Session Replication just not enough.
    • Currently, default logout handlers did not remove SessionRegistry properly. So i have created a sample Logout handler called CustomSessionLogoutHandler.
    • You have to override grails spring-security-core Login Controller to handle SessionAuthenticationException.
    • You can change number of users that can login maximumSessions = 1 to -1 for unlimited sessions.

    first, in resources.groovy

    import org.springframework.security.core.session.SessionRegistryImpl;
    import org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy;
    import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy;
    import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
    import org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy;
    import com.basic.CustomSessionLogoutHandler
    
    
    // Place your Spring DSL code here
    beans = {
    
    sessionRegistry(SessionRegistryImpl)
    
    customSessionLogoutHandler(CustomSessionLogoutHandler,ref('sessionRegistry')    )
    
    concurrentSessionControlAuthenticationStrategy(ConcurrentSessionControlAuthenticationStrategy,ref('sessionRegistry')){
        exceptionIfMaximumExceeded = true
        maximumSessions = 1
    }
    
    sessionFixationProtectionStrategy(SessionFixationProtectionStrategy){
        migrateSessionAttributes = true
        alwaysCreateSession = true
    }
    registerSessionAuthenticationStrategy(RegisterSessionAuthenticationStrategy,ref('sessionRegistry'))
    
    sessionAuthenticationStrategy(CompositeSessionAuthenticationStrategy,[ref('concurrentSessionControlAuthenticationStrategy'),ref('sessionFixationProtectionStrategy'),ref('registerSessionAuthenticationStrategy')])
    
    }
    

    in config.groovy make sure customSessionLogoutHandler is first before securityContextLogoutHandler:

    grails.plugin.springsecurity.logout.handlerNames = ['customSessionLogoutHandler','securityContextLogoutHandler'] 
    

    ConcurrentSessionControlAuthenticationStrategy uses this i18n. So you can have it in your language:

    ConcurrentSessionControlAuthenticationStrategy.exceededAllowed = Maximum sessions for this principal exceeded. {0}
    

    This is my sample CustomSessionLogoutHandler you can save it in src/groovy/com/basic/CustomSessionLogoutHandler.groovy:

    /*
    * Copyright 2002-2013 the original author or authors.
     *
     * Licensed under the Apache License, Version 2.0 (the "License");
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at
     *
     *      http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */
    package com.basic;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.springframework.security.core.Authentication;
    import org.springframework.security.web.authentication.logout.LogoutHandler;
    import org.springframework.util.Assert;
    import org.springframework.security.core.session.SessionRegistry;
    
    /**
     * {@link CustomSessionLogoutHandler} is in charge of removing the {@link SessionRegistry} upon logout. A
     * new {@link SessionRegistry} will then be generated by the framework upon the next request.
     *
     * @author Mohd Qusyairi
     * @since 0.1
     */
    public final class CustomSessionLogoutHandler implements LogoutHandler {
        private final SessionRegistry sessionRegistry;
    
        /**
         * Creates a new instance
         * @param sessionRegistry the {@link SessionRegistry} to use
         */
        public CustomSessionLogoutHandler(SessionRegistry sessionRegistry) {
            Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
            this.sessionRegistry = sessionRegistry;
        }
    
        /**
         * Clears the {@link SessionRegistry}
         *
         * @see org.springframework.security.web.authentication.logout.LogoutHandler#logout(javax.servlet.http.HttpServletRequest,
         * javax.servlet.http.HttpServletResponse,
         * org.springframework.security.core.Authentication)
         */
        public void logout(HttpServletRequest request, HttpServletResponse response,
                Authentication authentication) {
            this.sessionRegistry.removeSessionInformation(request.getSession().getId());
        }
    }
    

    My Sample Login Controller (I copied from the source) if you need it too. Just save as normal controller in your project as it will override the default. See line 115 below as i handle the SessionAuthenticationException:

    /* Copyright 2013-2016 the original author or authors.
     *
     * Licensed under the Apache License, Version 2.0 (the "License");
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at
     *
     *      http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */
    package com.basic
    
    import grails.converters.JSON
    import org.springframework.security.access.annotation.Secured
    import org.springframework.security.authentication.AccountExpiredException
    import org.springframework.security.authentication.AuthenticationTrustResolver
    import org.springframework.security.authentication.CredentialsExpiredException
    import org.springframework.security.authentication.DisabledException
    import org.springframework.security.authentication.LockedException
    import org.springframework.security.core.Authentication
    import org.springframework.security.core.context.SecurityContextHolder
    import org.springframework.security.web.WebAttributes
    import org.springframework.security.web.authentication.session.SessionAuthenticationException
    import javax.servlet.http.HttpServletResponse
    import grails.plugin.springsecurity.SpringSecurityUtils
    
    @Secured('permitAll')
    class LoginController {
    
        /** Dependency injection for the authenticationTrustResolver. */
        AuthenticationTrustResolver authenticationTrustResolver
    
        /** Dependency injection for the springSecurityService. */
        def springSecurityService
    
        /** Default action; redirects to 'defaultTargetUrl' if logged in, /login/auth otherwise. */
        def index() {
            if (springSecurityService.isLoggedIn()) {
                redirect uri: conf.successHandler.defaultTargetUrl
            }
            else {
                redirect action: 'auth', params: params
            }
        }
    
        /** Show the login page. */
        def auth() {
    
            def conf = getConf()
    
            if (springSecurityService.isLoggedIn()) {
                redirect uri: conf.successHandler.defaultTargetUrl
                return
            }
    
            String postUrl = request.contextPath + conf.apf.filterProcessesUrl
            render view: 'auth', model: [postUrl: postUrl,
                                         rememberMeParameter: conf.rememberMe.parameter,
                                         usernameParameter: conf.apf.usernameParameter,
                                         passwordParameter: conf.apf.passwordParameter,
                                         gspLayout: conf.gsp.layoutAuth]
        }
    
        /** The redirect action for Ajax requests. */
        def authAjax() {
            response.setHeader 'Location', conf.auth.ajaxLoginFormUrl
            render(status: HttpServletResponse.SC_UNAUTHORIZED, text: 'Unauthorized')
        }
    
        /** Show denied page. */
        def denied() {
            if (springSecurityService.isLoggedIn() && authenticationTrustResolver.isRememberMe(authentication)) {
                // have cookie but the page is guarded with IS_AUTHENTICATED_FULLY (or the equivalent expression)
                redirect action: 'full', params: params
                return
            }
    
            [gspLayout: conf.gsp.layoutDenied]
        }
    
        /** Login page for users with a remember-me cookie but accessing a IS_AUTHENTICATED_FULLY page. */
        def full() {
            def conf = getConf()
            render view: 'auth', params: params,
                   model: [hasCookie: authenticationTrustResolver.isRememberMe(authentication),
                           postUrl: request.contextPath + conf.apf.filterProcessesUrl,
                           rememberMeParameter: conf.rememberMe.parameter,
                           usernameParameter: conf.apf.usernameParameter,
                           passwordParameter: conf.apf.passwordParameter,
                           gspLayout: conf.gsp.layoutAuth]
        }
    
        /** Callback after a failed login. Redirects to the auth page with a warning message. */
        def authfail() {
    
            String msg = ''
            def exception = session[WebAttributes.AUTHENTICATION_EXCEPTION]
            if (exception) {
                if (exception instanceof AccountExpiredException) {
                    msg = message(code: 'springSecurity.errors.login.expired')
                }
                else if (exception instanceof CredentialsExpiredException) {
                    msg = message(code: 'springSecurity.errors.login.passwordExpired')
                }
                else if (exception instanceof DisabledException) {
                    msg = message(code: 'springSecurity.errors.login.disabled')
                }
                else if (exception instanceof LockedException) {
                    msg = message(code: 'springSecurity.errors.login.locked')
                }
                else if (exception instanceof SessionAuthenticationException){
                    msg = exception.getMessage()
                }
                else {
                    msg = message(code: 'springSecurity.errors.login.fail')
                }
            }
    
            if (springSecurityService.isAjax(request)) {
                render([error: msg] as JSON)
            }
            else {
                flash.message = msg
                redirect action: 'auth', params: params
            }
        }
    
        /** The Ajax success redirect url. */
        def ajaxSuccess() {
            render([success: true, username: authentication.name] as JSON)
        }
    
        /** The Ajax denied redirect url. */
        def ajaxDenied() {
            render([error: 'access denied'] as JSON)
        }
    
        protected Authentication getAuthentication() {
            SecurityContextHolder.context?.authentication
        }
    
        protected ConfigObject getConf() {
            SpringSecurityUtils.securityConfig
        }
    }