From b026ba5cfa8f8fca75cdd10d92ce047d40c387d0 Mon Sep 17 00:00:00 2001 From: Daniel Bisgrove Date: Wed, 6 Mar 2024 10:42:05 -0500 Subject: [PATCH] Removing closeSession() as uses third-party cookies. Create signOut page and redirect there so we can redirect user back to their previous page before signOut. --- src/app/main/main.component.js | 6 + src/app/signOut/signOut.component.js | 52 ++++++++ src/app/signOut/signOut.spec.js | 100 +++++++++++++++ src/app/signOut/signOut.tpl.html | 13 ++ .../services/session/session.service.js | 40 +++--- .../services/session/session.service.spec.js | 121 ++++++++++-------- webpack.config.js | 1 + 7 files changed, 259 insertions(+), 74 deletions(-) create mode 100644 src/app/signOut/signOut.component.js create mode 100644 src/app/signOut/signOut.spec.js create mode 100644 src/app/signOut/signOut.tpl.html diff --git a/src/app/main/main.component.js b/src/app/main/main.component.js index b29ae75b7..90e18ca6e 100644 --- a/src/app/main/main.component.js +++ b/src/app/main/main.component.js @@ -8,6 +8,7 @@ import checkoutComponent from '../checkout/checkout.component' import thankYouComponent from '../thankYou/thankYou.component' import productConfigComponent from '../productConfig/productConfig.component' import signInComponent from '../signIn/signIn.component' +import signOutComponent from '../signOut/signOut.component' import searchResultsComponent from '../searchResults/searchResults.component' import designationEditorComponent from '../designationEditor/designationEditor.component' import yourGivingComponent from '../profile/yourGiving/yourGiving.component' @@ -46,6 +47,10 @@ const routingConfig = /* @ngInject */ function ($stateProvider, $locationProvide url: '/sign-in.html', template: '' }) + .state('sign-out', { + url: '/sign-out.html', + template: '' + }) .state('checkout', { url: '/checkout.html', template: '' @@ -96,6 +101,7 @@ export default angular yourGivingComponent.name, productConfigComponent.name, signInComponent.name, + signOutComponent.name, searchResultsComponent.name, profileComponent.name, designationEditorComponent.name, diff --git a/src/app/signOut/signOut.component.js b/src/app/signOut/signOut.component.js new file mode 100644 index 000000000..bb2512f0b --- /dev/null +++ b/src/app/signOut/signOut.component.js @@ -0,0 +1,52 @@ +import angular from 'angular' +import commonModule from 'common/common.module' +import sessionService, { Roles } from 'common/services/session/session.service' +import template from './signOut.tpl.html' + +const componentName = 'signOut' + +class SignOutController { + /* @ngInject */ + constructor ($window, sessionService) { + this.$window = $window + this.sessionService = sessionService + } + + $onInit () { + if (this.sessionService.getRole() !== Roles.public) { + this.redirectToHomepage() + } else { + this.redirectToLocationPriorToSignOut() + } + } + + redirectToHomepage () { + this.$window.location.href = '/' + } + + redirectToLocationPriorToSignOut () { + this.showRedirectingLoadingIcon = true + const locationToReturnUser = this.sessionService.hasLocationOnLogin() + if (locationToReturnUser) { + this.sessionService.removeLocationOnLogin() + this.$window.location.href = locationToReturnUser + } else { + this.redirectToHomepage() + } + } + + closeRedirectingLoading () { + this.showRedirectingLoadingIcon = false + this.redirectToHomepage() + } +} + +export default angular + .module(componentName, [ + commonModule.name, + sessionService.name + ]) + .component(componentName, { + controller: SignOutController, + templateUrl: template + }) diff --git a/src/app/signOut/signOut.spec.js b/src/app/signOut/signOut.spec.js new file mode 100644 index 000000000..e61ed7693 --- /dev/null +++ b/src/app/signOut/signOut.spec.js @@ -0,0 +1,100 @@ +import angular from 'angular' +import 'angular-mocks' +import module from './signOut.component' +import { Observable } from 'rxjs/Observable' +import 'rxjs/add/observable/of' +import 'rxjs/add/observable/throw' + +describe('signOut', function () { + beforeEach(angular.mock.module(module.name)) + let $ctrl + + beforeEach(inject(function (_$componentController_) { + $ctrl = _$componentController_(module.name, + { $window: { location: { href: '/sign-out.html'} } } + ) + })) + + it('to be defined', function () { + expect($ctrl).toBeDefined() + }) + + it('redirectToHomepage()', function () { + $ctrl.redirectToHomepage() + expect($ctrl.$window.location.href).toEqual('/') + }) + + describe('redirectToLocationPriorToSignOut()', () => { + beforeEach(() => { + jest.spyOn($ctrl, 'redirectToHomepage') + jest.spyOn($ctrl.sessionService, 'removeLocationOnLogin') + }) + + it('redirects to prior page if hasLocationOnLogin() returns value', () => { + jest.spyOn($ctrl.sessionService, 'hasLocationOnLogin').mockReturnValue('https://give-stage2.cru.org/search-results.html') + $ctrl.showRedirectingLoadingIcon = false + $ctrl.redirectToLocationPriorToSignOut() + expect($ctrl.showRedirectingLoadingIcon).toEqual(true) + expect($ctrl.sessionService.hasLocationOnLogin).toHaveBeenCalled() + expect($ctrl.sessionService.removeLocationOnLogin).toHaveBeenCalled() + expect($ctrl.$window.location.href).toEqual('https://give-stage2.cru.org/search-results.html') + }) + + it('redirects to home page if hasLocationOnLogin() returns no value', () => { + jest.spyOn($ctrl.sessionService, 'hasLocationOnLogin').mockReturnValue(undefined) + $ctrl.showRedirectingLoadingIcon = false + $ctrl.redirectToLocationPriorToSignOut() + expect($ctrl.showRedirectingLoadingIcon).toEqual(true) + expect($ctrl.sessionService.hasLocationOnLogin).toHaveBeenCalled() + expect($ctrl.sessionService.removeLocationOnLogin).not.toHaveBeenCalled() + expect($ctrl.$window.location.href).toEqual('/') + }) + }) + + + + describe('as \'IDENTIFIED\'', () => { + it('should redirect to the home page', () => { + jest.spyOn($ctrl.sessionService, 'getRole').mockReturnValue('IDENTIFIED') + jest.spyOn($ctrl, 'redirectToHomepage') + jest.spyOn($ctrl, 'redirectToLocationPriorToSignOut') + $ctrl.$onInit() + + expect($ctrl.redirectToHomepage).toHaveBeenCalled() + expect($ctrl.redirectToLocationPriorToSignOut).not.toHaveBeenCalled() + }) + }) + + describe('as \'REGISTERED\'', () => { + it('should redirect to the home page', () => { + jest.spyOn($ctrl.sessionService, 'getRole').mockReturnValue('REGISTERED') + jest.spyOn($ctrl, 'redirectToHomepage') + jest.spyOn($ctrl, 'redirectToLocationPriorToSignOut') + $ctrl.$onInit() + + expect($ctrl.redirectToHomepage).toHaveBeenCalled() + expect($ctrl.redirectToLocationPriorToSignOut).not.toHaveBeenCalled() + }) + }) + + + describe('as \'GUEST\'', () => { + it('should run redirectToLocationPriorToSignOut()', () => { + jest.spyOn($ctrl.sessionService, 'getRole').mockReturnValue('PUBLIC') + jest.spyOn($ctrl, 'redirectToLocationPriorToSignOut') + $ctrl.$onInit() + + expect($ctrl.redirectToLocationPriorToSignOut).toHaveBeenCalled() + }) + }) + + describe('closeRedirectingLoading()', () => { + it('should run redirectToHomepage()', () => { + jest.spyOn($ctrl, 'redirectToHomepage') + $ctrl.showRedirectingLoadingIcon = true + $ctrl.closeRedirectingLoading() + expect($ctrl.showRedirectingLoadingIcon).toEqual(false) + expect($ctrl.redirectToHomepage).toHaveBeenCalled() + }) + }) +}) diff --git a/src/app/signOut/signOut.tpl.html b/src/app/signOut/signOut.tpl.html new file mode 100644 index 000000000..1f2b5874a --- /dev/null +++ b/src/app/signOut/signOut.tpl.html @@ -0,0 +1,13 @@ +
+ +
diff --git a/src/common/services/session/session.service.js b/src/common/services/session/session.service.js index 0b559463b..3fea64ece 100644 --- a/src/common/services/session/session.service.js +++ b/src/common/services/session/session.service.js @@ -297,6 +297,19 @@ const session = /* @ngInject */ function ($cookies, $rootScope, $http, $timeout, } async function internalSignOut (redirectHome = true) { + const oktaSignOut = async () => { + console.log('oktaSignOut', redirectHome) + // Add session data so on return to page we can show an explaination to the user about what happened. + if (!redirectHome) { + $window.sessionStorage.setItem(forcedUserToLogout, true) + // Save location we need to redirect the user back to + $window.sessionStorage.setItem(locationOnLogin, $window.location.href) + } + return await authClient.signOut({ + postLogoutRedirectUri: redirectHome ? null : `${envService.read('oktaReferrer')}/sign-out.html` + }) + } + try { await $http({ method: 'DELETE', @@ -304,26 +317,18 @@ const session = /* @ngInject */ function ($cookies, $rootScope, $http, $timeout, withCredentials: true }) await clearCheckoutSavedData() + // Use revokeAccessToken, revokeRefreshToken to ensure authClient is cleared before logging out entirely. await authClient.revokeAccessToken() await authClient.revokeRefreshToken() - await authClient.closeSession() - // Add session data so on return to page we can show an explaination to the user about what happened. - if (!redirectHome) { - $window.sessionStorage.setItem('forcedUserToLogout', true) - } - return authClient.signOut({ - postLogoutRedirectUri: redirectHome ? null : $window.location.href - }) + + return await oktaSignOut() } catch { - // closeSession errors out due to CORS. to fix this temporarily, I've added the logout in a catch just in case. - if (!redirectHome) { - $window.sessionStorage.setItem('forcedUserToLogout', true) + try { + return await oktaSignOut() + } catch { + console.log('woof') + $window.location.href = `${envService.read('oktaUrl')}/login/signout?fromURI=${envService.read('oktaReferrer')}` } - return authClient.signOut({ - postLogoutRedirectUri: redirectHome ? null : $window.location.href - }).catch(() => { - $window.location = `https://signon.okta.com/login/signout?fromURI=${envService.read('oktaReferrer')}` - }) } } @@ -351,7 +356,7 @@ const session = /* @ngInject */ function ($cookies, $rootScope, $http, $timeout, function removeForcedUserToLogoutSessionData () { // Allow for 2 seconds, so component can show error to user. setTimeout(() => { - $window.sessionStorage.removeItem('forcedUserToLogout') + $window.sessionStorage.removeItem(forcedUserToLogout) }, 2000) } @@ -368,7 +373,6 @@ const session = /* @ngInject */ function ($cookies, $rootScope, $http, $timeout, .map((response) => response.data) authClient.revokeAccessToken() authClient.revokeRefreshToken() - authClient.closeSession() return skipEvent ? observable : observable.finally(() => { diff --git a/src/common/services/session/session.service.spec.js b/src/common/services/session/session.service.spec.js index cd7e0b366..bdd04d789 100644 --- a/src/common/services/session/session.service.spec.js +++ b/src/common/services/session/session.service.spec.js @@ -1,6 +1,6 @@ import angular from 'angular' import 'angular-mocks' -import module, { Roles, Sessions, SignOutEvent, SignInEvent, redirectingIndicator, checkoutSavedDataCookieName, locationOnLogin, locationSearchOnLogin, createAccountDataCookieName } from './session.service' +import module, { Roles, Sessions, SignOutEvent, SignInEvent, redirectingIndicator, checkoutSavedDataCookieName, locationOnLogin, locationSearchOnLogin, createAccountDataCookieName, forcedUserToLogout } from './session.service' import { cortexRole } from 'common/services/session/fixtures/cortex-role' import { giveSession } from 'common/services/session/fixtures/give-session' import { cruProfile } from 'common/services/session/fixtures/cru-profile' @@ -189,78 +189,87 @@ describe('session service', function () { beforeEach(() => { jest.spyOn(sessionService.authClient, 'revokeAccessToken') jest.spyOn(sessionService.authClient, 'revokeRefreshToken') - jest.spyOn(sessionService.authClient, 'closeSession') jest.spyOn(sessionService.authClient, 'signOut') - $httpBackend.expectDELETE('https://give-stage2.cru.org/okta/logout') - .respond(200, {}) - $window.sessionStorage.removeItem('forcedUserToLogout') + + $window.sessionStorage.removeItem(forcedUserToLogout) }) - it('makes a DELETE request to Cortex & sets postLogoutRedirectUri', () => { - jest.spyOn(sessionService.authClient, 'signOut').mockImplementationOnce(() => Promise.resolve({ - data: {} - })) - sessionService - .signOut(false) - .subscribe((response) => { - expect(response.data).toEqual({}) - expect(sessionService.authClient.signOut).toHaveBeenCalledWith({ - postLogoutRedirectUri: `https://URL.org?utm_source=text` + + describe('Successful cortex logout', () => { + beforeEach(() => { + $httpBackend.expectDELETE('https://give-stage2.cru.org/okta/logout') + .respond(200, {}) + }) + + it('makes a DELETE request to Cortex & sets postLogoutRedirectUri', () => { + jest.spyOn(sessionService.authClient, 'signOut').mockImplementationOnce(() => Promise.resolve({ + data: {} + })) + sessionService + .signOut(false) + .subscribe((response) => { + expect(response.data).toEqual({}) + expect(sessionService.authClient.signOut).toHaveBeenCalledWith({ + postLogoutRedirectUri: `https://URL.org?utm_source=text` + }) }) - }) - }) + }) + + it('should revoke all tokens & run signOut returning the user home', () => { + sessionService + .signOut(true) + .subscribe(() => { + expect(sessionService.authClient.revokeAccessToken).toHaveBeenCalled() + expect(sessionService.authClient.revokeRefreshToken).toHaveBeenCalled() + expect(sessionService.authClient.signOut).toHaveBeenCalledWith({ + postLogoutRedirectUri: null + }) + }) + }) - it('should revoke all tokens & run signOut returning the user home', () => { - sessionService - .signOut(true) + it('should still sign user out if error during signout', () => { + jest.spyOn(sessionService.authClient, 'signOut').mockRejectedValueOnce() + sessionService + .signOut() .subscribe(() => { expect(sessionService.authClient.revokeAccessToken).toHaveBeenCalled() expect(sessionService.authClient.revokeRefreshToken).toHaveBeenCalled() - expect(sessionService.authClient.closeSession).toHaveBeenCalled() - expect(sessionService.authClient.signOut).toHaveBeenCalledWith({ - postLogoutRedirectUri: null - }) + expect(sessionService.authClient.signOut).toHaveBeenCalled() }) - }); + }) - it('should still sign user out if error during signout', () => { - jest.spyOn(sessionService.authClient, 'revokeAccessToken').mockImplementationOnce(() => Promise.reject()) - sessionService - .signOut() - .subscribe(() => { - expect(sessionService.authClient.revokeAccessToken).toHaveBeenCalled() - expect(sessionService.authClient.revokeRefreshToken).not.toHaveBeenCalled() - expect(sessionService.authClient.closeSession).not.toHaveBeenCalled() - expect(sessionService.authClient.signOut).toHaveBeenCalled() + it('should add forcedUserToLogout session data', () => { + sessionService + .signOut(false) + .subscribe(() => { + expect($window.sessionStorage.getItem(forcedUserToLogout)).toEqual('true') + }) }) - }); + }) - it('should add forcedUserToLogout session data', () => { - sessionService - .signOut(false) - .subscribe(() => { - expect($window.sessionStorage.getItem('forcedUserToLogout')).toEqual('true') + describe('Failed cortex logout', () => { + beforeEach(() => { + $httpBackend.expectDELETE('https://give-stage2.cru.org/okta/logout') + .respond(200, {}) }) - }); - it('should add forcedUserToLogout if error', () => { - jest.spyOn(sessionService.authClient, 'revokeAccessToken').mockImplementationOnce(() => Promise.reject()) - sessionService - .signOut(false) - .subscribe(() => { - expect($window.sessionStorage.getItem('forcedUserToLogout')).toEqual('true') + it('should add forcedUserToLogout if error', () => { + sessionService + .signOut(false) + .subscribe(() => { + expect($window.sessionStorage.getItem(forcedUserToLogout)).toEqual('true') + }) }) - }); - it('should redirect the user to okta if all else fails', () => { - jest.spyOn(sessionService.authClient, 'revokeAccessToken').mockImplementationOnce(() => Promise.reject()) - jest.spyOn(sessionService.authClient, 'signOut').mockImplementationOnce(() => Promise.reject()) - sessionService - .signOut() - .subscribe(() => { - expect($window.location).toEqual(`https://signon.okta.com/login/signout?fromURI=${envService.read('oktaReferrer')}`) + it('should redirect the user to okta if all else fails', () => { + jest.spyOn(sessionService.authClient, 'signOut').mockRejectedValue() + sessionService + .signOut() + .subscribe(() => { + expect($window.location.href).toEqual(`${envService.read('oktaUrl')}/login/signout?fromURI=${envService.read('oktaReferrer')}`) + }) }) - }); + }) afterEach(() => { $httpBackend.flush() diff --git a/webpack.config.js b/webpack.config.js index 8fb4400af..ce50c20b7 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -14,6 +14,7 @@ const giveComponents = [ 'app/thankYou/thankYou.component.js', 'app/productConfig/productConfig.component.js', 'app/signIn/signIn.component.js', + 'app/signOut/signOut.component.js', 'app/searchResults/searchResults.component.js', 'app/profile/yourGiving/yourGiving.component.js', 'app/profile/profile.component.js',