mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
79700: Auto-refreshing the token & Needed config
This commit is contained in:
@@ -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() {
|
||||||
|
@@ -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);
|
||||||
|
@@ -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}
|
||||||
|
@@ -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}}..." ,
|
||||||
|
@@ -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;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@@ -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,
|
||||||
|
Reference in New Issue
Block a user