Skip to content

Commit

Permalink
Merge pull request #2187 from mspalti/shibboleth-refresh
Browse files Browse the repository at this point in the history
Shibboleth page update after authentication
  • Loading branch information
tdonohue authored Apr 19, 2023
2 parents d9f6386 + b1f3b78 commit cefe1bf
Show file tree
Hide file tree
Showing 9 changed files with 130 additions and 4 deletions.
15 changes: 15 additions & 0 deletions src/app/core/auth/auth.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -425,6 +439,7 @@ export type AuthActions
| AuthenticationSuccessAction
| CheckAuthenticationTokenAction
| CheckAuthenticationTokenCookieAction
| SetAuthCookieStatus
| RedirectWhenAuthenticationIsRequiredAction
| RedirectWhenTokenExpiredAction
| AddAuthenticationMessageAction
Expand Down
3 changes: 3 additions & 0 deletions src/app/core/auth/auth.effects.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
Expand Down
1 change: 1 addition & 0 deletions src/app/core/auth/auth.effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
23 changes: 23 additions & 0 deletions src/app/core/auth/auth.reducer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
AuthenticationErrorAction,
AuthenticationSuccessAction,
CheckAuthenticationTokenAction,
SetAuthCookieStatus,
CheckAuthenticationTokenCookieAction,
LogOutAction,
LogOutErrorAction,
Expand Down Expand Up @@ -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,
Expand Down
10 changes: 9 additions & 1 deletion src/app/core/auth/auth.reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
RedirectWhenTokenExpiredAction,
RefreshTokenSuccessAction,
RetrieveAuthenticatedEpersonSuccessAction,
RetrieveAuthMethodsSuccessAction,
RetrieveAuthMethodsSuccessAction, SetAuthCookieStatus,
SetRedirectUrlAction
} from './auth.actions';
// import models
Expand Down Expand Up @@ -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;

Expand All @@ -73,6 +75,7 @@ const initialState: AuthState = {
blocking: true,
loading: false,
authMethods: [],
externalAuth: false,
idle: false
};

Expand Down Expand Up @@ -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, {
Expand Down
22 changes: 20 additions & 2 deletions src/app/core/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
import { CookieService } from '../services/cookie.service';
import {
getAuthenticatedUserId,
getAuthenticationToken,
getAuthenticationToken, getExternalAuthCookieStatus,
getRedirectUrl,
isAuthenticated,
isAuthenticatedLoaded,
Expand All @@ -36,7 +36,7 @@ import { AppState } from '../../app.reducer';
import {
CheckAuthenticationTokenAction,
RefreshTokenAction,
ResetAuthenticationMessagesAction,
ResetAuthenticationMessagesAction, SetAuthCookieStatus,
SetRedirectUrlAction,
SetUserAsIdleAction,
UnsetUserAsIdleAction
Expand Down Expand Up @@ -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}
Expand Down
12 changes: 12 additions & 0 deletions src/app/core/auth/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions src/app/shared/testing/auth-service.stub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export class AuthServiceStub {
token: AuthTokenInfo = new AuthTokenInfo('token_test');
impersonating: string;
private _tokenExpired = false;
private _isExternalAuth = false;
private redirectUrl;

constructor() {
Expand Down Expand Up @@ -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);
Expand Down
40 changes: 39 additions & 1 deletion src/modules/app/browser-init.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -51,6 +56,7 @@ export class BrowserInitService extends InitService {
protected authService: AuthService,
protected themeService: ThemeService,
protected menuService: MenuService,
private rootDataService: RootDataService
) {
super(
store,
Expand Down Expand Up @@ -80,6 +86,7 @@ export class BrowserInitService extends InitService {
return async () => {
await this.loadAppState();
this.checkAuthenticationToken();
this.externalAuthCheck();
this.initCorrelationId();

this.checkEnvironment();
Expand Down Expand Up @@ -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();
});
}

}

0 comments on commit cefe1bf

Please sign in to comment.