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);
}
this.storeCSSVariables();
this.authService.trackTokenExpiration();
}
ngOnInit() {

View File

@@ -28,7 +28,7 @@ import { AuthMethodType } from './models/auth.method-type';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
// Intercetor is called twice per request,
// Interceptor is called twice per request,
// so to prevent RefreshTokenAction is dispatched twice
// we're creating a refresh token request list
protected refreshTokenRequestUrls = [];
@@ -216,23 +216,8 @@ export class AuthInterceptor implements HttpInterceptor {
let authorization: string;
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);
} 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.
authorization = authService.buildAuthHeader(token);
let newHeaders = req.headers.set('authorization', authorization);

View File

@@ -33,7 +33,7 @@ import {
} from './selectors';
import { AppState } from '../../app.reducer';
import {
CheckAuthenticationTokenAction,
CheckAuthenticationTokenAction, RefreshTokenAction,
ResetAuthenticationMessagesAction,
RetrieveAuthMethodsAction,
SetRedirectUrlAction
@@ -46,6 +46,9 @@ import { getAllSucceededRemoteDataPayload } from '../shared/operators';
import { AuthMethod } from './models/auth.method';
import { HardRedirectService } from '../services/hard-redirect.service';
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 LOGOUT_ROUTE = '/logout';
@@ -64,6 +67,11 @@ export class AuthService {
*/
protected _authenticated: boolean;
/**
* Timer to track time until token refresh
*/
private tokenRefreshTimer;
constructor(@Inject(REQUEST) protected req: any,
@Inject(NativeWindowService) protected _window: NativeWindowRef,
@Optional() @Inject(RESPONSE) private response: any,
@@ -73,7 +81,9 @@ export class AuthService {
protected routeService: RouteService,
protected storage: CookieService,
protected store: Store<AppState>,
protected hardRedirectService: HardRedirectService
protected hardRedirectService: HardRedirectService,
private notificationService: NotificationsService,
private translateService: TranslateService
) {
this.store.pipe(
select(isAuthenticated),
@@ -298,7 +308,7 @@ export class AuthService {
*/
public getToken(): AuthTokenInfo {
let token: AuthTokenInfo;
this.store.pipe(select(getAuthenticationToken))
this.store.pipe(take(1), select(getAuthenticationToken))
.subscribe((authTokenInfo: AuthTokenInfo) => {
// Retrieve authentication token info and check if is valid
token = authTokenInfo || null;
@@ -306,6 +316,44 @@ export class AuthService {
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
* @returns {boolean}

View File

@@ -526,6 +526,8 @@
"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}}..." ,

View File

@@ -6,5 +6,18 @@ export interface AuthTarget {
}
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 { BrowseByType } from '../app/+browse-by/+browse-by-switcher/browse-by-decorator';
import { RestRequestMethod } from '../app/core/data/rest-request-method';
import { BASE_THEME_NAME } from '../app/shared/theme-support/theme.constants';
export const environment: GlobalConfig = {
production: true,
@@ -43,6 +42,25 @@ export const environment: GlobalConfig = {
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: {
// NOTE: Map server-side validators to comparative Angular form validators