Merge pull request #2187 from mspalti/shibboleth-refresh

Shibboleth page update after authentication
This commit is contained in:
Tim Donohue
2023-04-19 16:08:31 -05:00
committed by GitHub
9 changed files with 130 additions and 4 deletions

View File

@@ -17,6 +17,7 @@ export const AuthActionTypes = {
AUTHENTICATED_SUCCESS: type('dspace/auth/AUTHENTICATED_SUCCESS'), AUTHENTICATED_SUCCESS: type('dspace/auth/AUTHENTICATED_SUCCESS'),
CHECK_AUTHENTICATION_TOKEN: type('dspace/auth/CHECK_AUTHENTICATION_TOKEN'), CHECK_AUTHENTICATION_TOKEN: type('dspace/auth/CHECK_AUTHENTICATION_TOKEN'),
CHECK_AUTHENTICATION_TOKEN_COOKIE: type('dspace/auth/CHECK_AUTHENTICATION_TOKEN_COOKIE'), 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: type('dspace/auth/RETRIEVE_AUTH_METHODS'),
RETRIEVE_AUTH_METHODS_SUCCESS: type('dspace/auth/RETRIEVE_AUTH_METHODS_SUCCESS'), RETRIEVE_AUTH_METHODS_SUCCESS: type('dspace/auth/RETRIEVE_AUTH_METHODS_SUCCESS'),
RETRIEVE_AUTH_METHODS_ERROR: type('dspace/auth/RETRIEVE_AUTH_METHODS_ERROR'), 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; 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. * Sign out.
* @class LogOutAction * @class LogOutAction
@@ -425,6 +439,7 @@ export type AuthActions
| AuthenticationSuccessAction | AuthenticationSuccessAction
| CheckAuthenticationTokenAction | CheckAuthenticationTokenAction
| CheckAuthenticationTokenCookieAction | CheckAuthenticationTokenCookieAction
| SetAuthCookieStatus
| RedirectWhenAuthenticationIsRequiredAction | RedirectWhenAuthenticationIsRequiredAction
| RedirectWhenTokenExpiredAction | RedirectWhenTokenExpiredAction
| AddAuthenticationMessageAction | AddAuthenticationMessageAction

View File

@@ -214,12 +214,15 @@ describe('AuthEffects', () => {
authenticated: true authenticated: true
}) })
); );
spyOn((authEffects as any).authService, 'setExternalAuthStatus');
actions = hot('--a-', { a: { type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE } }); actions = hot('--a-', { a: { type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE } });
const expected = cold('--b-', { b: new RetrieveTokenAction() }); const expected = cold('--b-', { b: new RetrieveTokenAction() });
expect(authEffects.checkTokenCookie$).toBeObservable(expected); expect(authEffects.checkTokenCookie$).toBeObservable(expected);
authEffects.checkTokenCookie$.subscribe(() => { authEffects.checkTokenCookie$.subscribe(() => {
expect(authServiceStub.setExternalAuthStatus).toHaveBeenCalled();
expect(authServiceStub.isExternalAuthentication).toBeTrue();
expect((authEffects as any).authorizationsService.invalidateAuthorizationsRequestCache).toHaveBeenCalled(); expect((authEffects as any).authorizationsService.invalidateAuthorizationsRequestCache).toHaveBeenCalled();
}); });
}); });

View File

@@ -153,6 +153,7 @@ export class AuthEffects {
return this.authService.checkAuthenticationCookie().pipe( return this.authService.checkAuthenticationCookie().pipe(
map((response: AuthStatus) => { map((response: AuthStatus) => {
if (response.authenticated) { if (response.authenticated) {
this.authService.setExternalAuthStatus(true);
this.authorizationsService.invalidateAuthorizationsRequestCache(); this.authorizationsService.invalidateAuthorizationsRequestCache();
return new RetrieveTokenAction(); return new RetrieveTokenAction();
} else { } else {

View File

@@ -8,6 +8,7 @@ import {
AuthenticationErrorAction, AuthenticationErrorAction,
AuthenticationSuccessAction, AuthenticationSuccessAction,
CheckAuthenticationTokenAction, CheckAuthenticationTokenAction,
SetAuthCookieStatus,
CheckAuthenticationTokenCookieAction, CheckAuthenticationTokenCookieAction,
LogOutAction, LogOutAction,
LogOutErrorAction, LogOutErrorAction,
@@ -219,6 +220,28 @@ describe('authReducer', () => {
expect(newState).toEqual(state); 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', () => { it('should properly set the state, in response to a LOG_OUT action', () => {
initialState = { initialState = {
authenticated: true, authenticated: true,

View File

@@ -10,7 +10,7 @@ import {
RedirectWhenTokenExpiredAction, RedirectWhenTokenExpiredAction,
RefreshTokenSuccessAction, RefreshTokenSuccessAction,
RetrieveAuthenticatedEpersonSuccessAction, RetrieveAuthenticatedEpersonSuccessAction,
RetrieveAuthMethodsSuccessAction, RetrieveAuthMethodsSuccessAction, SetAuthCookieStatus,
SetRedirectUrlAction SetRedirectUrlAction
} from './auth.actions'; } from './auth.actions';
// import models // import models
@@ -59,6 +59,8 @@ export interface AuthState {
// all authentication Methods enabled at the backend // all authentication Methods enabled at the backend
authMethods?: AuthMethod[]; authMethods?: AuthMethod[];
externalAuth?: boolean,
// true when the current user is idle // true when the current user is idle
idle: boolean; idle: boolean;
@@ -73,6 +75,7 @@ const initialState: AuthState = {
blocking: true, blocking: true,
loading: false, loading: false,
authMethods: [], authMethods: [],
externalAuth: false,
idle: false idle: false
}; };
@@ -104,6 +107,11 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
loading: true, loading: true,
}); });
case AuthActionTypes.SET_AUTH_COOKIE_STATUS:
return Object.assign({}, state, {
externalAuth: (action as SetAuthCookieStatus).payload
});
case AuthActionTypes.AUTHENTICATED_ERROR: case AuthActionTypes.AUTHENTICATED_ERROR:
case AuthActionTypes.RETRIEVE_AUTHENTICATED_EPERSON_ERROR: case AuthActionTypes.RETRIEVE_AUTHENTICATED_EPERSON_ERROR:
return Object.assign({}, state, { return Object.assign({}, state, {

View File

@@ -25,7 +25,7 @@ import {
import { CookieService } from '../services/cookie.service'; import { CookieService } from '../services/cookie.service';
import { import {
getAuthenticatedUserId, getAuthenticatedUserId,
getAuthenticationToken, getAuthenticationToken, getExternalAuthCookieStatus,
getRedirectUrl, getRedirectUrl,
isAuthenticated, isAuthenticated,
isAuthenticatedLoaded, isAuthenticatedLoaded,
@@ -36,7 +36,7 @@ import { AppState } from '../../app.reducer';
import { import {
CheckAuthenticationTokenAction, CheckAuthenticationTokenAction,
RefreshTokenAction, RefreshTokenAction,
ResetAuthenticationMessagesAction, ResetAuthenticationMessagesAction, SetAuthCookieStatus,
SetRedirectUrlAction, SetRedirectUrlAction,
SetUserAsIdleAction, SetUserAsIdleAction,
UnsetUserAsIdleAction UnsetUserAsIdleAction
@@ -156,6 +156,24 @@ export class AuthService {
return this.store.pipe(select(isAuthenticatedLoaded)); return this.store.pipe(select(isAuthenticatedLoaded));
} }
/**
* Used to set the external authentication status when authenticating via an
* external authentication system (e.g. Shibboleth).
* @param external
*/
public setExternalAuthStatus(external: boolean) {
this.store.dispatch(new SetAuthCookieStatus(external));
}
/**
* Returns true if an external authentication system (e.g. Shibboleth) is being used
* for authentication. Returns false otherwise.
*/
public isExternalAuthentication(): Observable<boolean> {
return this.store.pipe(
select(getExternalAuthCookieStatus));
}
/** /**
* Returns the href link to authenticated user * Returns the href link to authenticated user
* @returns {string} * @returns {string}

View File

@@ -116,6 +116,8 @@ const _getRedirectUrl = (state: AuthState) => state.redirectUrl;
const _getAuthenticationMethods = (state: AuthState) => state.authMethods; const _getAuthenticationMethods = (state: AuthState) => state.authMethods;
const _getExternalAuthCookieStatus = (state: AuthState) => state.externalAuth;
/** /**
* Returns true if the user is idle. * Returns true if the user is idle.
* @function _isIdle * @function _isIdle
@@ -178,6 +180,16 @@ export const isAuthenticated = createSelector(getAuthState, _isAuthenticated);
*/ */
export const isAuthenticatedLoaded = createSelector(getAuthState, _isAuthenticatedLoaded); export const isAuthenticatedLoaded = createSelector(getAuthState, _isAuthenticatedLoaded);
/**
* Returns the authentication cookie status. Expect to be true 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. * Returns true if the authentication request is loading.
* @function isAuthenticationLoading * @function isAuthenticationLoading

View File

@@ -17,6 +17,7 @@ export class AuthServiceStub {
token: AuthTokenInfo = new AuthTokenInfo('token_test'); token: AuthTokenInfo = new AuthTokenInfo('token_test');
impersonating: string; impersonating: string;
private _tokenExpired = false; private _tokenExpired = false;
private _isExternalAuth = false;
private redirectUrl; private redirectUrl;
constructor() { constructor() {
@@ -122,6 +123,13 @@ export class AuthServiceStub {
checkAuthenticationCookie() { checkAuthenticationCookie() {
return; return;
} }
setExternalAuthStatus(externalCookie: boolean) {
this._isExternalAuth = externalCookie;
}
isExternalAuthentication(): Observable<boolean> {
return observableOf(this._isExternalAuth);
}
retrieveAuthMethodsFromAuthStatus(status: AuthStatus) { retrieveAuthMethodsFromAuthStatus(status: AuthStatus) {
return observableOf(authMethodsMock); return observableOf(authMethodsMock);

View File

@@ -26,16 +26,21 @@ import { AuthService } from '../../app/core/auth/auth.service';
import { ThemeService } from '../../app/shared/theme-support/theme.service'; import { ThemeService } from '../../app/shared/theme-support/theme.service';
import { StoreAction, StoreActionTypes } from '../../app/store.actions'; import { StoreAction, StoreActionTypes } from '../../app/store.actions';
import { coreSelector } from '../../app/core/core.selectors'; import { coreSelector } from '../../app/core/core.selectors';
import { find, map } from 'rxjs/operators'; import { filter, find, map } from 'rxjs/operators';
import { isNotEmpty } from '../../app/shared/empty.util'; import { isNotEmpty } from '../../app/shared/empty.util';
import { logStartupMessage } from '../../../startup-message'; import { logStartupMessage } from '../../../startup-message';
import { MenuService } from '../../app/shared/menu/menu.service'; import { MenuService } from '../../app/shared/menu/menu.service';
import { RootDataService } from '../../app/core/data/root-data.service';
import { firstValueFrom, Subscription } from 'rxjs';
/** /**
* Performs client-side initialization. * Performs client-side initialization.
*/ */
@Injectable() @Injectable()
export class BrowserInitService extends InitService { export class BrowserInitService extends InitService {
sub: Subscription;
constructor( constructor(
protected store: Store<AppState>, protected store: Store<AppState>,
protected correlationIdService: CorrelationIdService, protected correlationIdService: CorrelationIdService,
@@ -51,6 +56,7 @@ export class BrowserInitService extends InitService {
protected authService: AuthService, protected authService: AuthService,
protected themeService: ThemeService, protected themeService: ThemeService,
protected menuService: MenuService, protected menuService: MenuService,
private rootDataService: RootDataService
) { ) {
super( super(
store, store,
@@ -80,6 +86,7 @@ export class BrowserInitService extends InitService {
return async () => { return async () => {
await this.loadAppState(); await this.loadAppState();
this.checkAuthenticationToken(); this.checkAuthenticationToken();
this.externalAuthCheck();
this.initCorrelationId(); this.initCorrelationId();
this.checkEnvironment(); this.checkEnvironment();
@@ -134,4 +141,35 @@ export class BrowserInitService extends InitService {
protected initGoogleAnalytics() { protected initGoogleAnalytics() {
this.googleAnalyticsService.addTrackingIdToPage(); this.googleAnalyticsService.addTrackingIdToPage();
} }
/**
* During an external authentication flow invalidate the SSR transferState
* data in the cache. This allows the app to fetch fresh content.
* @private
*/
private externalAuthCheck() {
this.sub = this.authService.isExternalAuthentication().pipe(
filter((externalAuth: boolean) => externalAuth)
).subscribe(() => {
// Clear the transferState data.
this.rootDataService.invalidateRootCache();
this.authService.setExternalAuthStatus(false);
}
);
this.closeAuthCheckSubscription();
}
/**
* Unsubscribe the external authentication subscription
* when authentication is no longer blocking.
* @private
*/
private closeAuthCheckSubscription() {
firstValueFrom(this.authenticationReady$()).then(() => {
this.sub.unsubscribe();
});
}
} }