Refresh Token OAuth Authentication Angular 4+

后端 未结 2 1646
遥遥无期
遥遥无期 2020-12-10 09:50

I was working with the Http clase from Angular but I decide to make the migration and work with the new HttpClient, and I was trying to create a so

2条回答
  •  野趣味
    野趣味 (楼主)
    2020-12-10 10:35

    For anyone else looking for a solution to this in Angular 4 (maybe slight changes needed for Angular 5+), I came up with the following solution:

    @Injectable()
    export class AuthInterceptorService implements HttpInterceptor {
        private _refreshRequest: Observable> | null = null;
    
        constructor(
            private _router: Router, 
            private _tokenStorage: TokenStorageService,
            private _injector: Injector) {
        }
    
        private _addTokenHeader(request: HttpRequest) {
            const authToken = this._tokenStorage.authToken;
    
            if (!authToken) {
                return request;
            }
    
            return request.clone({setHeaders: {'Authorization': 'Bearer ' + authToken.value}});
        }
    
        private _fail() {
            this._tokenStorage.clearTokens();
            this._router.navigate(['/login']);
            return throwError(new HttpErrorResponse({status: 401}));
        }
    
        private _refreshAuthToken(request: HttpRequest, next: HttpHandler) {
            // AuthService has the following dependency chain:
            // ApiService -> HttpClient -> HTTP_INTERCEPTORS
            // If injected at the constructor this causes a circular dependency error. 
            const authService = this._injector.get(AuthService);
    
            if (this._refreshRequest === null) {
                // Send the auth token refresh request
                this._refreshRequest = authService.refreshAuthToken();
                this._refreshRequest.subscribe(() => this._refreshRequest = null);
            }
    
            // Wait for the auth token refresh request to finish before sending the pending request
            return this._refreshRequest
                .flatMap(result => {
                    if (result.success) {
                        // Auth token was refreshed, continue with pending request
                        return this._sendRequest(this._addTokenHeader(request), next);
                    }
    
                    // Refreshing the auth token failed, fail the pending request
                    return this._fail();
                });
        }
    
        private _sendRequest(request: HttpRequest, next: HttpHandler) {
            return next.handle(request).catch((err: HttpErrorResponse, caught) => {
                // Send the user to the login page If there are any 'Unauthorized' responses
                if (err.status === 401) {
                    this._router.navigate(['/login']);
                }
    
                return Observable.throw(err);
            });
        }
    
        intercept(request: HttpRequest, next: HttpHandler): Observable> {
            if (request.url.indexOf('/api/auth') !== -1) {
                // This interceptor should not be applied to auth requests
                return this._sendRequest(request, next);
            }
    
            const authToken = this._tokenStorage.authToken;
            const refreshToken = this._tokenStorage.refreshToken;
    
            // Attempt to refresh the auth token if it is expired or about to expire
            if (authToken && authToken.expiresWithinSeconds(60)) {
                if (refreshToken && !refreshToken.isExpired) {
                    return this._refreshAuthToken(request, next);
                }
                else {
                    // Auth token has expired and cannot be refreshed
                    return this._fail();
                }
            }
    
            return this._sendRequest(this._addTokenHeader(request), next);
        }
    }
    

    This will issue an auth token refresh request to the server if the current auth token is expired, but there is a valid refresh token. Further requests are buffered until the pending refresh request completes.

    Not shown is the source to:
    - TokenStorageService which just uses localStorage
    - Jwt class that wraps a token and makes the token claims like expiry date easy to access
    - ApiResult which is just a simple wrapper around HttpResponse for my application, and not particularly relevant to anything here

    Edit: Angular 6/7

    import { Injectable, Inject, Injector } from '@angular/core';
    import { Router } from '@angular/router';
    import { 
        HttpEvent, 
        HttpInterceptor, 
        HttpHandler, 
        HttpRequest, 
        HttpErrorResponse, 
    } from '@angular/common/http';
    import { Observable, of, throwError } from 'rxjs';
    import { catchError, flatMap } from 'rxjs/operators';
    
    import { ApiResult } from '../../api';
    
    import { TokenStorageService } from './token-storage.service';
    import { AuthService } from './auth.service';
    
    
    @Injectable()
    export class AuthInterceptorService implements HttpInterceptor {
        private _refreshRequest: Observable | null = null;
    
        constructor(
            private _router: Router, 
            private _tokenStorage: TokenStorageService,
            @Inject('BASE_URL') private _baseUrl: string,
            private _injector: Injector) {
        }
    
        private _addTokenHeader(request: HttpRequest) {
            const authToken = this._tokenStorage.authToken;
    
            if (!authToken) {
                return request;
            }
    
            return request.clone({setHeaders: {'Authorization': 'Bearer ' + authToken.value}});
        }
    
        private _forceLogin() {
            this._tokenStorage.clearTokens();
    
            this._router.navigate(['/account/login'], { queryParams: {
                message: 'Your session has expired. Please re-enter your credentials.'
            }});
        }
    
        private _fail() {
            this._forceLogin();
            return throwError(new HttpErrorResponse({status: 401}));
        }
    
        private _refreshAuthToken(request: HttpRequest, next: HttpHandler) {
            // AuthService has the following dependency chain:
            // ApiService -> HttpClient -> HTTP_INTERCEPTORS
            // If injected at the constructor this causes a circular dependency error. 
            const authService = this._injector.get(AuthService);
    
            if (this._refreshRequest === null) {
                // Send the auth token refresh request
                this._refreshRequest = authService.refreshAuthToken();
                this._refreshRequest.subscribe(() => this._refreshRequest = null);
            }
    
            // Wait for the auth token refresh request to finish before sending the pending request
            return this._refreshRequest.pipe(flatMap(result => {
                if (result.success) {
                    // Auth token was refreshed, continue with pending request
                    return this._sendRequest(this._addTokenHeader(request), next);
                }
    
                // Refreshing the auth token failed, fail the pending request
                return this._fail();
            }));
        }
    
        private _sendRequest(request: HttpRequest, next: HttpHandler) {
            return next.handle(request).pipe(catchError(err => {
                // Send the user to the login page If there are any 'Unauthorized' responses
                if (err.status === 401) {
                    this._forceLogin();
                }
    
                return throwError(err);
            }));
        }
    
        intercept(request: HttpRequest, next: HttpHandler): Observable> {
            if (request.url.indexOf(this._baseUrl) === -1  || request.url.indexOf('/api/auth') !== -1) {
                // This interceptor should not be applied to non-api requests or auth requests
                return this._sendRequest(request, next);
            }
    
            const authToken = this._tokenStorage.authToken;
            const refreshToken = this._tokenStorage.refreshToken;
    
            // Attempt to refresh the auth token if it is expired or about to expire
            if (authToken && authToken.expiresWithinSeconds(60)) {
                if (refreshToken && !refreshToken.isExpired) {
                    return this._refreshAuthToken(request, next);
                }
                else {
                    // Auth token has expired and cannot be refreshed
                    return this._fail();
                }
            }
    
            return this._sendRequest(this._addTokenHeader(request), next);
        }
    }
    

提交回复
热议问题