1

I have a requirement to close a session held by a JSESSIONID cookie when the user closes the browser window. This cookie is set at server-side by Spring Security:

@Configuration
@EnableWebSecurity
public class SpringSecurityConfiguration {

    @Configuration
    public static class ApplicationApiSecurityConfiguration extends WebSecurityConfigurerAdapter {

        @Override
        protected void configure(final HttpSecurity http) {
        http
                    .antMatcher("/**")
          .csrf()
          .disable() // Disable Cross Site Request Forgery Protection
          .formLogin() // Enable form based/cookie based/session based login
          .loginPage(LOGIN_PAGE) // Set our custom login page
          .and()
            .authorizeRequests()
            .antMatchers("/login", "/logout")
            .permitAll() // Allow anyone to access the login and logout page
            .anyRequest()
            .authenticated() //All other request require authentication
          .and()
            .logout()
            .deleteCookies("JSESSIONID") // Delete JSESSIONID cookie on logout
            .clearAuthentication(true) // Clean authentication on logout
            .invalidateHttpSession(true); // Invalidate Http Session on logout
        }
    }
}

At client side I have AngularJS 1.7 and TypeScript and I am already capturing the beforeunload event by setting up @module.ts

module app.myapp {

    import IModule = angular.IModule;

    import IStateProvider = angular.ui.IStateProvider;
    import IUrlRouterProvider = angular.ui.IUrlRouterProvider;

    import LockService = lib.common.LockService;

    export class MyApp {

        static NAME = 'app.myapp';

        static module(): IModule {
            return angular.module(MyApp.NAME);
        }
    }

    angular.module(MyApp.NAME, ['ui.router'])
        .config([
            '$stateProvider', '$urlRouterProvider',
            ($stateProvider: IStateProvider, $urlRouterProvider: IUrlRouterProvider) => {

                $urlRouterProvider.when('', '/home');

                $stateProvider.state('base', {
                    url: '',
                    abstract: true,
                    template: '<ui-view/>',
                    resolve: {
                        initEventListenerForGlobalUnlock: [
                            LockService.ID,
                            (lockService: LockService) => lockService.initListenerForGlobalUnlock()
                        ]
                    }
                });

                $stateProvider.state('personalProfile', {
                    url: '/profile',
                    parent: 'base',
                    component: PersonalProfile.ID
                });
            }
        ]);
}

and then for my LockService implementation:

module lib.common {

    import IWindowService = angular.IWindowService;
    import IRootScopeService = angular.IRootScopeService;

    export class LockService {

        static ID = 'lockService';
        static $inject: string[] = ['$window', '$rootScope'];

        constructor(
            private readonly $window: IWindowService,
            private readonly $rootScope: IRootScopeService
        ) { }

        private readonly globalUnlockHandler = (ev: BeforeUnloadEvent) => {
            // This does not work with HttpOnly cookies
            document.cookie = 'JSESSIONID=;path=/;domain=' + window.location.hostname + ';expires=Thu, 01 Jan 1970 00:00:01 GMT';
            this.$rootScope.$digest();
            return null;
        }

        /**
         * This is called upon app initialisation,
         * an event listener for browser close is registered,
         * which will use an API call to unlock all locked sections before exiting
         */
        initListenerForGlobalUnlock(): void {
            this.$window.addEventListener('beforeunload', this.globalUnlockHandler);
        }
    }

    LibCommon.module().service(LockService.ID, LockService);
}

Currently, there is a log-out funtionality based on redirecting the page by doing this.$window.location.href = 'logout'

But what I want is to delete the session cookie (or else invalidate somehow the session) when the browser window is closed by clicking [x] so that even if the user comes back to the same page by pasting the URL in the address bar he is asked to log in again.

The problem is that JSESSIONID cookie is set to be HttpOnly and therefore cannot be deleted from JavaScript. And I do not know how to tell Spring Security on server side to invalidate the session.

Serg M Ten
  • 5,568
  • 4
  • 25
  • 48
  • 1
    I'm a little confused, isn't JSESSIONID a session cookie, meaning that it will be dropped once the browser closes? Maybe you are looking for something like https://stackoverflow.com/Questions/805895/why-doesnt-closing-a-tab-delete-a-session-cookie – jzheaux Jan 10 '20 at 17:07
  • AFAIR session cookies are transient, after restarting your browser they are gone.However, the session is still alive until the user is logged out. – dur Jan 10 '20 at 17:39
  • The JSESSION cookie does not disappear when the browser window is closed. This can be verified with Firefox local storage inspector. I do not know why Spring has methods `clearAuthentication()` and `invalidateHttpSession()` on top of `deleteCookie()` on logout. I do not need to do something specifically with JSESSION cookie what I need is the session totally wiped out on window close, it doesn't matter how. – Serg M Ten Jan 10 '20 at 18:19
  • 2
    > The JSESSION cookies does not disappear Just checking here. First, are all Firefox windows closed? Second, is it the same JSESSIONID value in the cookie from when you close and then reopen? Third, what does the expires field on that cookie say? If it says "Session", then the browser is supposed to remove it once the process is terminated. As for your comment about invalidating the session, you'd have to somehow attach yourself to the window closing event and make an HTTP call to the service to logout the user. Not sure if Javascript can do that. – jzheaux Jan 10 '20 at 22:41
  • @jzheaux The JSESSIONID changes due to what I suspect is Spring SessionFixationProtectionStrategy. Setting it to None might do the job but I've not tested yet. Performing an HTTP call to the logout page during the unload event processing might be a solution as well. I shall test tomorrow. Thanks. – Serg M Ten Jan 11 '20 at 05:28

1 Answers1

0

Answering my own question.

The only way I found to achieve session expiration on browser window close was to perform a syncronous XHR request to the logout URL inside the globalUnlockHandler onBeforeUnload event listener.

AngularJS $http.get does not work probably because it is asynchronous and the invocation is cancelled before it completes.

Here is a TypeScript example of the legacy cross-browser XHR object creation:

createXMLHttpRequest(): XMLHttpRequest {
    var req: XMLHttpRequest = null;
    if (this.$window.XMLHttpRequest) {
        try {
            req = new XMLHttpRequest();
        } catch (e) { }
    } else if (this.$window.ActiveXObject) {
        try {
            req = new ActiveXObject('Msxml2.XMLHTTP'); // tslint:disable-line
        } catch (e) {
            try {
                req = new ActiveXObject('Microsoft.XMLHTTP'); // tslint:disable-line
            } catch (e) { }
        }
    } // fi
    if (!req) {
        console.warn('createXMLHttpRequest() failed');
    }
    return req;
}

Then to make the call:

var xhr = new XMLHttpRequest();
if (xhr) {
    xhr.open('GET', 'http://www.domain.name/logout', false);
    try {
        xhr.send(null);
    } catch (e) {
        console.warn(e);
    }
}

Note that XHR is deprecated and that this approach is against HTTP principles because the user could open a new browser tab and copy/paste the URL from the existing. Then if the first tab is closed the session will be terminated but the second tab will still be open without a valid session.

Serg M Ten
  • 5,568
  • 4
  • 25
  • 48