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'),
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

View File

@@ -214,12 +214,15 @@ 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).toHaveBeenCalled();
expect(authServiceStub.isExternalAuthentication).toBeTrue();
expect((authEffects as any).authorizationsService.invalidateAuthorizationsRequestCache).toHaveBeenCalled();
});
});

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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, {

View File

@@ -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,24 @@ export class AuthService {
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 {string}

View File

@@ -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 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.
* @function isAuthenticationLoading

View File

@@ -17,6 +17,7 @@ export class AuthServiceStub {
token: AuthTokenInfo = new AuthTokenInfo('token_test');
impersonating: string;
private _tokenExpired = false;
private _isExternalAuth = false;
private redirectUrl;
constructor() {
@@ -122,6 +123,13 @@ export class AuthServiceStub {
checkAuthenticationCookie() {
return;
}
setExternalAuthStatus(externalCookie: boolean) {
this._isExternalAuth = externalCookie;
}
isExternalAuthentication(): Observable<boolean> {
return observableOf(this._isExternalAuth);
}
retrieveAuthMethodsFromAuthStatus(status: AuthStatus) {
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 { StoreAction, StoreActionTypes } from '../../app/store.actions';
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 { logStartupMessage } from '../../../startup-message';
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.
*/
@Injectable()
export class BrowserInitService extends InitService {
sub: Subscription;
constructor(
protected store: Store<AppState>,
protected correlationIdService: CorrelationIdService,
@@ -51,6 +56,7 @@ export class BrowserInitService extends InitService {
protected authService: AuthService,
protected themeService: ThemeService,
protected menuService: MenuService,
private rootDataService: RootDataService
) {
super(
store,
@@ -80,6 +86,7 @@ export class BrowserInitService extends InitService {
return async () => {
await this.loadAppState();
this.checkAuthenticationToken();
this.externalAuthCheck();
this.initCorrelationId();
this.checkEnvironment();
@@ -134,4 +141,35 @@ export class BrowserInitService extends InitService {
protected initGoogleAnalytics() {
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();
});
}
}