From 290a89909e20f7cfd200abbbe86f24376593e22b Mon Sep 17 00:00:00 2001 From: Michael Spalti Date: Wed, 5 Apr 2023 19:44:46 -0700 Subject: [PATCH] Added check for external auth and page refresh --- src/app/core/auth/auth.actions.ts | 15 +++++++++++++ src/app/core/auth/auth.effects.spec.ts | 2 ++ src/app/core/auth/auth.effects.ts | 1 + src/app/core/auth/auth.reducer.spec.ts | 23 +++++++++++++++++++ src/app/core/auth/auth.reducer.ts | 10 ++++++++- src/app/core/auth/auth.service.ts | 13 +++++++++-- src/app/core/auth/selectors.ts | 12 ++++++++++ src/app/shared/testing/auth-service.stub.ts | 7 ++++++ src/modules/app/browser-init.service.ts | 25 ++++++++++++++++++++- 9 files changed, 104 insertions(+), 4 deletions(-) diff --git a/src/app/core/auth/auth.actions.ts b/src/app/core/auth/auth.actions.ts index 60440d371e..6bc4565682 100644 --- a/src/app/core/auth/auth.actions.ts +++ b/src/app/core/auth/auth.actions.ts @@ -17,6 +17,7 @@ export const AuthActionTypes = { AUTHENTICATED_SUCCESS: type('dspace/auth/AUTHENTICATED_SUCCESS'), CHECK_AUTHENTICATION_TOKEN: type('dspace/auth/CHECK_AUTHENTICATION_TOKEN'), CHECK_AUTHENTICATION_TOKEN_COOKIE: type('dspace/auth/CHECK_AUTHENTICATION_TOKEN_COOKIE'), + SET_AUTH_COOKIE_STATUS: type('dspace/auth/SET_AUTH_COOKIE_STATUS'), RETRIEVE_AUTH_METHODS: type('dspace/auth/RETRIEVE_AUTH_METHODS'), RETRIEVE_AUTH_METHODS_SUCCESS: type('dspace/auth/RETRIEVE_AUTH_METHODS_SUCCESS'), RETRIEVE_AUTH_METHODS_ERROR: type('dspace/auth/RETRIEVE_AUTH_METHODS_ERROR'), @@ -150,6 +151,19 @@ export class CheckAuthenticationTokenCookieAction implements Action { public type: string = AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE; } +/** + * Sets the authentication cookie status to flag an external authentication response. + */ +export class SetAuthCookieStatus implements Action { + public type: string = AuthActionTypes.SET_AUTH_COOKIE_STATUS; + + payload = false; + + constructor(exists: boolean) { + this.payload = exists; + } +} + /** * Sign out. * @class LogOutAction @@ -425,6 +439,7 @@ export type AuthActions | AuthenticationSuccessAction | CheckAuthenticationTokenAction | CheckAuthenticationTokenCookieAction + | SetAuthCookieStatus | RedirectWhenAuthenticationIsRequiredAction | RedirectWhenTokenExpiredAction | AddAuthenticationMessageAction diff --git a/src/app/core/auth/auth.effects.spec.ts b/src/app/core/auth/auth.effects.spec.ts index f09db04d99..7153c5ebbd 100644 --- a/src/app/core/auth/auth.effects.spec.ts +++ b/src/app/core/auth/auth.effects.spec.ts @@ -214,12 +214,14 @@ describe('AuthEffects', () => { authenticated: true }) ); + spyOn((authEffects as any).authService, 'setExternalAuthStatus'); actions = hot('--a-', { a: { type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE } }); const expected = cold('--b-', { b: new RetrieveTokenAction() }); expect(authEffects.checkTokenCookie$).toBeObservable(expected); authEffects.checkTokenCookie$.subscribe(() => { + expect(authServiceStub.setExternalAuthStatus).toHaveBeenCalledWith(true); expect((authEffects as any).authorizationsService.invalidateAuthorizationsRequestCache).toHaveBeenCalled(); }); }); diff --git a/src/app/core/auth/auth.effects.ts b/src/app/core/auth/auth.effects.ts index 22d1bf35e7..281355b769 100644 --- a/src/app/core/auth/auth.effects.ts +++ b/src/app/core/auth/auth.effects.ts @@ -153,6 +153,7 @@ export class AuthEffects { return this.authService.checkAuthenticationCookie().pipe( map((response: AuthStatus) => { if (response.authenticated) { + this.authService.setExternalAuthStatus(true); this.authorizationsService.invalidateAuthorizationsRequestCache(); return new RetrieveTokenAction(); } else { diff --git a/src/app/core/auth/auth.reducer.spec.ts b/src/app/core/auth/auth.reducer.spec.ts index 8ebc9f6cb0..c0619adf79 100644 --- a/src/app/core/auth/auth.reducer.spec.ts +++ b/src/app/core/auth/auth.reducer.spec.ts @@ -8,6 +8,7 @@ import { AuthenticationErrorAction, AuthenticationSuccessAction, CheckAuthenticationTokenAction, + SetAuthCookieStatus, CheckAuthenticationTokenCookieAction, LogOutAction, LogOutErrorAction, @@ -219,6 +220,28 @@ describe('authReducer', () => { expect(newState).toEqual(state); }); + it('should set the authentication cookie status in response to a SET_AUTH_COOKIE_STATUS action', () => { + initialState = { + authenticated: true, + loaded: false, + blocking: false, + loading: true, + externalAuth: false, + idle: false + }; + const action = new SetAuthCookieStatus(true); + const newState = authReducer(initialState, action); + state = { + authenticated: true, + loaded: false, + blocking: false, + loading: true, + externalAuth: true, + idle: false + }; + expect(newState).toEqual(state); + }); + it('should properly set the state, in response to a LOG_OUT action', () => { initialState = { authenticated: true, diff --git a/src/app/core/auth/auth.reducer.ts b/src/app/core/auth/auth.reducer.ts index acdb8ef812..ba9c41326a 100644 --- a/src/app/core/auth/auth.reducer.ts +++ b/src/app/core/auth/auth.reducer.ts @@ -10,7 +10,7 @@ import { RedirectWhenTokenExpiredAction, RefreshTokenSuccessAction, RetrieveAuthenticatedEpersonSuccessAction, - RetrieveAuthMethodsSuccessAction, + RetrieveAuthMethodsSuccessAction, SetAuthCookieStatus, SetRedirectUrlAction } from './auth.actions'; // import models @@ -59,6 +59,8 @@ export interface AuthState { // all authentication Methods enabled at the backend authMethods?: AuthMethod[]; + externalAuth?: boolean, + // true when the current user is idle idle: boolean; @@ -73,6 +75,7 @@ const initialState: AuthState = { blocking: true, loading: false, authMethods: [], + externalAuth: false, idle: false }; @@ -104,6 +107,11 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut loading: true, }); + case AuthActionTypes.SET_AUTH_COOKIE_STATUS: + return Object.assign({}, state, { + externalAuth: (action as SetAuthCookieStatus).payload + }); + case AuthActionTypes.AUTHENTICATED_ERROR: case AuthActionTypes.RETRIEVE_AUTHENTICATED_EPERSON_ERROR: return Object.assign({}, state, { diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index 3034c00197..fae5872c89 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -25,7 +25,7 @@ import { import { CookieService } from '../services/cookie.service'; import { getAuthenticatedUserId, - getAuthenticationToken, + getAuthenticationToken, getExternalAuthCookieStatus, getRedirectUrl, isAuthenticated, isAuthenticatedLoaded, @@ -36,7 +36,7 @@ import { AppState } from '../../app.reducer'; import { CheckAuthenticationTokenAction, RefreshTokenAction, - ResetAuthenticationMessagesAction, + ResetAuthenticationMessagesAction, SetAuthCookieStatus, SetRedirectUrlAction, SetUserAsIdleAction, UnsetUserAsIdleAction @@ -156,6 +156,15 @@ export class AuthService { return this.store.pipe(select(isAuthenticatedLoaded)); } + public setExternalAuthStatus(external: boolean) { + this.store.dispatch(new SetAuthCookieStatus(external)); + } + + public isExternalAuthentication(): Observable { + return this.store.pipe( + select(getExternalAuthCookieStatus)); + } + /** * Returns the href link to authenticated user * @returns {string} diff --git a/src/app/core/auth/selectors.ts b/src/app/core/auth/selectors.ts index ce8d38d6ba..16246fb14c 100644 --- a/src/app/core/auth/selectors.ts +++ b/src/app/core/auth/selectors.ts @@ -116,6 +116,8 @@ const _getRedirectUrl = (state: AuthState) => state.redirectUrl; const _getAuthenticationMethods = (state: AuthState) => state.authMethods; +const _getExternalAuthCookieStatus = (state: AuthState) => state.externalAuth; + /** * Returns true if the user is idle. * @function _isIdle @@ -178,6 +180,16 @@ export const isAuthenticated = createSelector(getAuthState, _isAuthenticated); */ export const isAuthenticatedLoaded = createSelector(getAuthState, _isAuthenticatedLoaded); +/** + * Returns the authentication cookie status. Expect to be ture when external authentication + * is used. + * @function getExternalAuthCookieStatus + * @param {AuthState} state + * @param {any} props + * @return {boolean} + */ +export const getExternalAuthCookieStatus = createSelector(getAuthState, _getExternalAuthCookieStatus); + /** * Returns true if the authentication request is loading. * @function isAuthenticationLoading diff --git a/src/app/shared/testing/auth-service.stub.ts b/src/app/shared/testing/auth-service.stub.ts index b8d822252d..e5ae355658 100644 --- a/src/app/shared/testing/auth-service.stub.ts +++ b/src/app/shared/testing/auth-service.stub.ts @@ -122,6 +122,13 @@ export class AuthServiceStub { checkAuthenticationCookie() { return; } + setExternalAuthStatus(externalCookie: boolean) { + return; + } + + isExternalAuthentication(): Observable { + return; + } retrieveAuthMethodsFromAuthStatus(status: AuthStatus) { return observableOf(authMethodsMock); diff --git a/src/modules/app/browser-init.service.ts b/src/modules/app/browser-init.service.ts index 687ecf0547..c7559363a3 100644 --- a/src/modules/app/browser-init.service.ts +++ b/src/modules/app/browser-init.service.ts @@ -26,10 +26,11 @@ import { AuthService } from '../../app/core/auth/auth.service'; import { ThemeService } from '../../app/shared/theme-support/theme.service'; import { StoreAction, StoreActionTypes } from '../../app/store.actions'; import { coreSelector } from '../../app/core/core.selectors'; -import { find, map } from 'rxjs/operators'; +import { filter, find, map, switchMap } from 'rxjs/operators'; import { isNotEmpty } from '../../app/shared/empty.util'; import { logStartupMessage } from '../../../startup-message'; import { MenuService } from '../../app/shared/menu/menu.service'; +import { RootDataService } from '../../app/core/data/root-data.service'; /** * Performs client-side initialization. @@ -51,6 +52,7 @@ export class BrowserInitService extends InitService { protected authService: AuthService, protected themeService: ThemeService, protected menuService: MenuService, + private rootDatatService: RootDataService ) { super( store, @@ -80,6 +82,7 @@ export class BrowserInitService extends InitService { return async () => { await this.loadAppState(); this.checkAuthenticationToken(); + this.externalAuthCheck(); this.initCorrelationId(); this.checkEnvironment(); @@ -134,4 +137,24 @@ export class BrowserInitService extends InitService { protected initGoogleAnalytics() { this.googleAnalyticsService.addTrackingIdToPage(); } + + /** + * When authenticated during the external authentication flow invalidate + * the cache so the app is rehydrated with fresh data. + * @private + */ + private externalAuthCheck() { + + this.authenticationReady$().pipe( + switchMap(() => this.authService.isExternalAuthentication().pipe( + filter((externalAuth: boolean) => externalAuth) + )) + ).subscribe(() => { + this.authService.setExternalAuthStatus(false); + this.rootDatatService.invalidateRootCache(); + } + ); + + } + }