How to delete an HttpOnly server-side session cookie when closing a browser window

2.6k Views Asked by At

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.

1

There are 1 best solutions below

0
On

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.