79700: Auto-refreshing the token & Needed config

This commit is contained in:
Marie Verdonck
2021-05-28 10:33:51 +02:00
parent de8e306d9f
commit b23522d39f
6 changed files with 90 additions and 24 deletions

View File

@@ -130,7 +130,7 @@ export class AppComponent implements OnInit, AfterViewInit {
console.info(environment); console.info(environment);
} }
this.storeCSSVariables(); this.storeCSSVariables();
this.authService.trackTokenExpiration();
} }
ngOnInit() { ngOnInit() {

View File

@@ -28,7 +28,7 @@ import { AuthMethodType } from './models/auth.method-type';
@Injectable() @Injectable()
export class AuthInterceptor implements HttpInterceptor { export class AuthInterceptor implements HttpInterceptor {
// Intercetor is called twice per request, // Interceptor is called twice per request,
// so to prevent RefreshTokenAction is dispatched twice // so to prevent RefreshTokenAction is dispatched twice
// we're creating a refresh token request list // we're creating a refresh token request list
protected refreshTokenRequestUrls = []; protected refreshTokenRequestUrls = [];
@@ -216,23 +216,8 @@ export class AuthInterceptor implements HttpInterceptor {
let authorization: string; let authorization: string;
if (authService.isTokenExpired()) { if (authService.isTokenExpired()) {
authService.setRedirectUrl(this.router.url);
// The access token is expired
// Redirect to the login route
this.store.dispatch(new RedirectWhenTokenExpiredAction('auth.messages.expired'));
return observableOf(null); return observableOf(null);
} else if ((!this.isAuthRequest(req) || this.isLogoutResponse(req)) && isNotEmpty(token)) { } else if ((!this.isAuthRequest(req) || this.isLogoutResponse(req)) && isNotEmpty(token)) {
// Intercept a request that is not to the authentication endpoint
authService.isTokenExpiring().pipe(
filter((isExpiring) => isExpiring))
.subscribe(() => {
// If the current request url is already in the refresh token request list, skip it
if (isUndefined(find(this.refreshTokenRequestUrls, req.url))) {
// When a token is about to expire, refresh it
this.store.dispatch(new RefreshTokenAction(token));
this.refreshTokenRequestUrls.push(req.url);
}
});
// Get the auth header from the service. // Get the auth header from the service.
authorization = authService.buildAuthHeader(token); authorization = authService.buildAuthHeader(token);
let newHeaders = req.headers.set('authorization', authorization); let newHeaders = req.headers.set('authorization', authorization);

View File

@@ -33,7 +33,7 @@ import {
} from './selectors'; } from './selectors';
import { AppState } from '../../app.reducer'; import { AppState } from '../../app.reducer';
import { import {
CheckAuthenticationTokenAction, CheckAuthenticationTokenAction, RefreshTokenAction,
ResetAuthenticationMessagesAction, ResetAuthenticationMessagesAction,
RetrieveAuthMethodsAction, RetrieveAuthMethodsAction,
SetRedirectUrlAction SetRedirectUrlAction
@@ -46,6 +46,9 @@ import { getAllSucceededRemoteDataPayload } from '../shared/operators';
import { AuthMethod } from './models/auth.method'; import { AuthMethod } from './models/auth.method';
import { HardRedirectService } from '../services/hard-redirect.service'; import { HardRedirectService } from '../services/hard-redirect.service';
import { RemoteData } from '../data/remote-data'; import { RemoteData } from '../data/remote-data';
import { environment } from '../../../environments/environment';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
export const LOGIN_ROUTE = '/login'; export const LOGIN_ROUTE = '/login';
export const LOGOUT_ROUTE = '/logout'; export const LOGOUT_ROUTE = '/logout';
@@ -64,6 +67,11 @@ export class AuthService {
*/ */
protected _authenticated: boolean; protected _authenticated: boolean;
/**
* Timer to track time until token refresh
*/
private tokenRefreshTimer;
constructor(@Inject(REQUEST) protected req: any, constructor(@Inject(REQUEST) protected req: any,
@Inject(NativeWindowService) protected _window: NativeWindowRef, @Inject(NativeWindowService) protected _window: NativeWindowRef,
@Optional() @Inject(RESPONSE) private response: any, @Optional() @Inject(RESPONSE) private response: any,
@@ -73,7 +81,9 @@ export class AuthService {
protected routeService: RouteService, protected routeService: RouteService,
protected storage: CookieService, protected storage: CookieService,
protected store: Store<AppState>, protected store: Store<AppState>,
protected hardRedirectService: HardRedirectService protected hardRedirectService: HardRedirectService,
private notificationService: NotificationsService,
private translateService: TranslateService
) { ) {
this.store.pipe( this.store.pipe(
select(isAuthenticated), select(isAuthenticated),
@@ -298,7 +308,7 @@ export class AuthService {
*/ */
public getToken(): AuthTokenInfo { public getToken(): AuthTokenInfo {
let token: AuthTokenInfo; let token: AuthTokenInfo;
this.store.pipe(select(getAuthenticationToken)) this.store.pipe(take(1), select(getAuthenticationToken))
.subscribe((authTokenInfo: AuthTokenInfo) => { .subscribe((authTokenInfo: AuthTokenInfo) => {
// Retrieve authentication token info and check if is valid // Retrieve authentication token info and check if is valid
token = authTokenInfo || null; token = authTokenInfo || null;
@@ -306,6 +316,44 @@ export class AuthService {
return token; return token;
} }
/**
* Method that checks when the session token from store expires and refreshes it when needed
*/
public trackTokenExpiration(): void {
let token: AuthTokenInfo;
let currentlyRefreshingToken = false;
this.store.pipe(select(getAuthenticationToken)).subscribe((authTokenInfo: AuthTokenInfo) => {
// If new token is undefined an it wasn't previously => Refresh failed
if (currentlyRefreshingToken && token != undefined && authTokenInfo == undefined) {
// Token refresh failed => Error notification => 10 second wait => Page reloads & user logged out
this.notificationService.error(this.translateService.get('auth.messages.token-refresh-failed'));
setTimeout(() => this.navigateToRedirectUrl(this.hardRedirectService.getCurrentRoute()), 10000);
currentlyRefreshingToken = false;
}
// If new token.expires is different => Refresh succeeded
if (currentlyRefreshingToken && authTokenInfo != undefined && token.expires != authTokenInfo.expires) {
currentlyRefreshingToken = false;
}
// Check if/when token needs to be refreshed
if (!currentlyRefreshingToken) {
token = authTokenInfo || null;
if (token != undefined && token != null) {
let timeLeftBeforeRefresh = token.expires - new Date().getTime() - environment.auth.rest.timeLeftBeforeTokenRefresh;
if (timeLeftBeforeRefresh < 0) {
timeLeftBeforeRefresh = 0;
}
if (hasValue(this.tokenRefreshTimer)) {
clearTimeout(this.tokenRefreshTimer);
}
this.tokenRefreshTimer = setTimeout(() => {
this.store.dispatch(new RefreshTokenAction(token));
currentlyRefreshingToken = true;
}, timeLeftBeforeRefresh);
}
}
});
}
/** /**
* Check if a token is next to be expired * Check if a token is next to be expired
* @returns {boolean} * @returns {boolean}

View File

@@ -526,6 +526,8 @@
"auth.messages.expired": "Your session has expired. Please log in again.", "auth.messages.expired": "Your session has expired. Please log in again.",
"auth.messages.token-refresh-failed": "Refreshing your session token failed. Please log in again.",
"bitstream.download.page": "Now downloading {{bitstream}}..." , "bitstream.download.page": "Now downloading {{bitstream}}..." ,

View File

@@ -6,5 +6,18 @@ export interface AuthTarget {
} }
export interface AuthConfig extends Config { export interface AuthConfig extends Config {
target: AuthTarget; target?: AuthTarget;
ui: {
// The amount of time before the idle warning is shown
timeUntilIdle: number;
// The amount of time the user has to react after the idle warning is shown before they are logged out.
idleGracePeriod: number;
};
rest: {
// If the rest token expires in less than this amount of time, it will be refreshed automatically.
// This is independent from the idle warning.
timeLeftBeforeTokenRefresh: number;
};
} }

View File

@@ -2,7 +2,6 @@ import { GlobalConfig } from '../config/global-config.interface';
import { NotificationAnimationsType } from '../app/shared/notifications/models/notification-animations-type'; import { NotificationAnimationsType } from '../app/shared/notifications/models/notification-animations-type';
import { BrowseByType } from '../app/+browse-by/+browse-by-switcher/browse-by-decorator'; import { BrowseByType } from '../app/+browse-by/+browse-by-switcher/browse-by-decorator';
import { RestRequestMethod } from '../app/core/data/rest-request-method'; import { RestRequestMethod } from '../app/core/data/rest-request-method';
import { BASE_THEME_NAME } from '../app/shared/theme-support/theme.constants';
export const environment: GlobalConfig = { export const environment: GlobalConfig = {
production: true, production: true,
@@ -43,6 +42,25 @@ export const environment: GlobalConfig = {
timePerMethod: {[RestRequestMethod.PATCH]: 3} as any // time in seconds timePerMethod: {[RestRequestMethod.PATCH]: 3} as any // time in seconds
} }
}, },
// Authority settings
auth: {
// Authority UI settings
ui: {
// the amount of time before the idle warning is shown
// timeUntilIdle: 15 * 60 * 1000, // 15 minutes
timeUntilIdle: 1 * 60 * 1000, // 1 minutes
// the amount of time the user has to react after the idle warning is shown before they are logged out.
// idleGracePeriod: 5 * 60 * 1000, // 5 minutes
idleGracePeriod: 1 * 60 * 1000, // 1 minutes
},
// Authority REST settings
rest: {
// If the rest token expires in less than this amount of time, it will be refreshed automatically.
// This is independent from the idle warning.
// timeLeftBeforeTokenRefresh: 2 * 60 * 1000, // 2 minutes
timeLeftBeforeTokenRefresh: 0.25 * 60 * 1000, // 25 seconds
},
},
// Form settings // Form settings
form: { form: {
// NOTE: Map server-side validators to comparative Angular form validators // NOTE: Map server-side validators to comparative Angular form validators
@@ -267,8 +285,8 @@ export const environment: GlobalConfig = {
], ],
// Whether the UI should rewrite file download URLs to match its domain. Only necessary to enable when running UI and REST API on separate domains // Whether the UI should rewrite file download URLs to match its domain. Only necessary to enable when running UI and REST API on separate domains
rewriteDownloadUrls: false, rewriteDownloadUrls: false,
// Whether to enable media viewer for image and/or video Bitstreams (i.e. Bitstreams whose MIME type starts with "image" or "video"). // Whether to enable media viewer for image and/or video Bitstreams (i.e. Bitstreams whose MIME type starts with "image" or "video").
// For images, this enables a gallery viewer where you can zoom or page through images. // For images, this enables a gallery viewer where you can zoom or page through images.
// For videos, this enables embedded video streaming // For videos, this enables embedded video streaming
mediaViewer: { mediaViewer: {
image: false, image: false,