Hello,
I have 3 different apis for different services.
Each API has you own Auth Token.
I want to create 3 http interceptors, one for each api.
I know create a httpInterceptor for all project, but how I can create a different interceptor for each api service?
You can use a single interceptor, and since in this one you have access to read the URL, you can create a function that depending on this uses a different token, for example:
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
let token;
if (request.url.indexOf(PATH_ROUTE_ONE) !== -1) {
token = localStorage.getItem(TK1);
} else if(request.url.indexOf(PATH_ROUTE_TWO) !== -1) {
token = localStorage.getItem(TK2);
} else {
token = localStorage.getItem(TK3);
}
if (token) {
request = request.clone({
setHeaders: {
authorization: `Bearer ${token}`,
},
});
}
return next.handle(request).pipe(
tap((res) => {
if (res instanceof HttpResponse) {
// TODO: Update token info
}
}),
catchError((err: HttpErrorResponse) => throwError(err)),
);
}
If you want to use 3 paths you can do the same and you only read the URL to know if you apply it or let it continue long
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
let token = localStorage.getItem(TK1)
if (request.url.indexOf(PATH_ROUTE_ONE) !== -1) {
request = request.clone({
setHeaders: {
authorization: `Bearer ${token}`,
},
});
}
return next.handle(request).pipe(
tap((res) => {
if (res instanceof HttpResponse) {
// TODO: Update token info
}
}),
catchError((err: HttpErrorResponse) => throwError(err)),
);
}
I am using angular 9 + universal. No errors while i run ng serve , then I build the app with npm run build:ssr and try to run with node : node dist/app/server/main.js and get the following error in terminal :
Node Express server listening on http://localhost:4000 TypeError: You
provided 'undefined' where a stream was expected. You can provide an
Observable, Promise, Array, or Iterable.
at subscribeTo (C:\Users\andri\OneDrive\Desktop\devfox\autorent\ng-videocms\dist\autorent\server\main.js:1:2547459)
at subscribeToResult (C:\Users\andri\OneDrive\Desktop\devfox\autorent\ng-videocms\dist\autorent\server\main.js:1:2775326)
at CatchSubscriber.error (C:\Users\andri\OneDrive\Desktop\devfox\autorent\ng-videocms\dist\autorent\server\main.js:1:1997435)
at Observable_Observable._trySubscribe (C:\Users\andri\OneDrive\Desktop\devfox\autorent\ng-videocms\dist\autorent\server\main.js:1:1952954)
at Observable_Observable.subscribe (C:\Users\andri\OneDrive\Desktop\devfox\autorent\ng-videocms\dist\autorent\server\main.js:1:1952574)
at CatchOperator.call (C:\Users\andri\OneDrive\Desktop\devfox\autorent\ng-videocms\dist\autorent\server\main.js:1:1996823)
at Observable_Observable.subscribe (C:\Users\andri\OneDrive\Desktop\devfox\autorent\ng-videocms\dist\autorent\server\main.js:1:1952428)
at _task (C:\Users\andri\OneDrive\Desktop\devfox\autorent\ng-videocms\dist\autorent\server\main.js:1:1751796)
at Observable_Observable.Observable.a.observer [as _subscribe] (C:\Users\andri\OneDrive\Desktop\devfox\autorent\ng-videocms\dist\autorent\server\main.js:1:1752141)
at Observable_Observable._trySubscribe (C:\Users\andri\OneDrive\Desktop\devfox\autorent\ng-videocms\dist\autorent\server\main.js:1:1952792)
As I've explored, my app does 2 api calls on start :
app.component.ts :
ngOnInit(){
get1();
get2();
}
get1() {
const loc = this.locationService.getPickupLocations().subscribe((data: Location[]) => {
this.pickupLocations = data;
this.formGroup.get(LocationFields.pickup).setValue(data[0].getId());
this.pickupLocationsList = this.pickupLocations.map((data): ISelectOption => {
return {
label: data.getName(),
value: data.getId(),
};
});
},
(error)=> {
console.log(error)
},
() => {
this.subs.add(loc);
this.pickupDateChange(this.formGroup.get(this.LocationFields.pickupDate).value);
});
}
get2() {
const drop = this.locationService.getDropOffLocations().subscribe((data: Location[]) => {
this.dropoffLocations = data;
this.formGroup.get(LocationFields.dropoff).setValue(data[1].getId());
this.dropoffLocationsList = this.dropoffLocations.map((data): ISelectOption => {
return {
label: data.getName(),
value: data.getId(),
};
});
},(error)=> {
console.log(error)
},
() => {
this.subs.add(drop);
});
}
LocationService.ts :
static locationsEndpoint = 'public/locations/rental';
getPickupLocations(): Observable<Location[]> {
const reqHeader = new HttpHeaders({ 'Content-Type': 'application/json', 'No-Auth': 'True' });
return this.http.get(`${LocationsService.locationsEndpoint}/pickup`, { headers: reqHeader }).pipe(
map((data: ILocationResponse) => this.hydrateCollectionData(data, LocationsHydrator))
);
}
getDropOffLocations(): Observable<Location[]> {
const reqHeader = new HttpHeaders({ 'Content-Type': 'application/json', 'No-Auth': 'True' });
return this.http.get(`${LocationsService.locationsEndpoint}/dropoff`, { headers: reqHeader }).pipe(
map((data: ILocationResponse) => this.hydrateCollectionData(data, LocationsHydrator))
);
}
And Interceptors :
private static BASE_URL = environment.apiUrl;
readonly HEADER_AUTHORIZATION = 'Authorization';
readonly HEADER_ACCEPT = 'Accept';
readonly HEADER_CONTENT_TYPE = 'Content-Type';
readonly ACCEPT_LANGUAGE = 'Accept-Language';
constructor(
private authService: AuthService,
private localeService: LocaleService
) { }
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (req.headers.get('skip')) {
return next.handle(req);
}
if (req.url.startsWith('./assets')) {
return next.handle(req);
}
req = req.clone({
url: this._prefixUrl(req.url)
});
req = req.clone({
headers: req.headers.set(this.HEADER_ACCEPT, 'application/json')
});
req = req.clone({
headers: req.headers.set(this.HEADER_CONTENT_TYPE, 'application/json')
});
req = req.clone({
headers: req.headers.set(this.ACCEPT_LANGUAGE, this.localeService.getLocale())
});
// Set token if exists
const token = this.authService.getToken();
if (token) {
req = req.clone({
headers: req.headers.set(this.HEADER_AUTHORIZATION, `Bearer ${token}`)
});
}
return next.handle(req).pipe(
catchError((httpErrorResponse: HttpErrorResponse) => {
if(httpErrorResponse.error !== undefined){
const customError: ApiErrors = {
name: httpErrorResponse.error.name,
message: httpErrorResponse.error.message,
errors: httpErrorResponse.error.errors
};
return throwError(customError);
}
})
);
}
private _prefixUrl(path: string): string {
if (path.indexOf('/') === 0) {
path = path.substr(1, path.length - 1);
}
return `${Interceptor.BASE_URL}/${path}`;
}
I tried without these calls , tried to comment one of them.
When i disable those calls (comment them) , app works fine.
When i call them later, after i commented them, (onclick), app works fine.
When i disable interceptors , it works (SO the problem is in them, what to change ?? )
How to fix it ? And why it is happening ?
The trace seems to suggest the problem is related to CatchOperator.
I see only one place where catchError operator is used in your code
return next.handle(req).pipe(
catchError((httpErrorResponse: HttpErrorResponse) => {
if(httpErrorResponse.error !== undefined){
const customError: ApiErrors = {
name: httpErrorResponse.error.name,
message: httpErrorResponse.error.message,
errors: httpErrorResponse.error.errors
};
return throwError(customError);
}
})
);
In your code you seem to assume that the function passed to catchError will receive always an instance of HttpErrorResponse which is not the case. catchError will be used for any error and so the function passed to it can receive any type of error.
What happens if httpErrorResponse.error is null but you still have an error in the upstream Observables somewhere? According to the code above you do not return anything, so this may be the reason why the log says You provided 'undefined' where a stream was expected.
Rather than not doing anything, you can throw an error, so that you should get the details of the error causing the fact that you enter catchError operator, so something like this
catchError(err => {
if(error instanceof HttpErrorResponse && httpErrorResponse.error !== undefined){
const customError: ApiErrors = {
name: httpErrorResponse.error.name,
message: httpErrorResponse.error.message,
errors: httpErrorResponse.error.errors
};
return throwError(customError);
} else {
throw err
}
})
);
Info
I am creating an interceptor to use my refresh token to update my access token if I get a 401. The workflow looks like this now:
Sends request > gets 401 > sends refresh request > updates access token > sends new request
I am currently working with promises instead of observables.
Question
How do I logout if the last request fails?
Sends request > gets 401 > sends refresh request > updates access token > sends new request > fails > log out
I have a simple method for logging out, but I cannot find where to put it within the interceptor.
Code
export class RefreshInterceptor implements HttpInterceptor {
currentUser: User | null = null;
private isRefreshing = false;
private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(
null
);
constructor(private authenticationService: AuthenticationService) {
this.authenticationService.currentUser.subscribe(
user => (this.currentUser = user)
);
}
intercept(
request: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
return next.handle(request).pipe(
catchError(error => {
// check if user is signed in
if (!this.currentUser) {
return throwError(error);
}
// handle only 401 error
if (error instanceof HttpErrorResponse && error.status === 401) {
return from(this.handle401Error(request, next));
} else {
return throwError(error);
}
})
);
}
/**
* Adds the new access token as a bearer header to the request
* #param request - the request
* #param token - the new access token
*/
private async addToken(request: HttpRequest<any>, token: string) {
const currentUser = this.authenticationService.currentUserValue;
if (currentUser && currentUser.accessToken) {
return request.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
}
return request;
}
private async handle401Error(request: HttpRequest<any>, next: HttpHandler) {
// check if it is currently refreshing or not
if (!this.isRefreshing) {
this.isRefreshing = true;
this.refreshTokenSubject.next(null);
// send refresh request
const token = await this.authenticationService.getRefresh();
// update bearer token
const newRequest = await this.addToken(request, token);
// update values for next request
this.isRefreshing = false;
this.refreshTokenSubject.next(token);
return next.handle(newRequest).toPromise();
} else {
const token = this.refreshTokenSubject.value();
const newRequest = await this.addToken(request, token);
return next.handle(newRequest).toPromise();
}
}
}
I solved it with the following approach:
Modified the header of the outgoing, changed request (added a retry header so that I could identify it later).
Created a new interceptor for logout
Looked for a request with the retry header. Signed that request out.
Refresh token interceptor
if (currentUser && currentUser.accessToken) {
return request.clone({
setHeaders: {
Authorization: `Bearer ${token}`,
Retry: "true"
}
});
}
Logout interceptor
#Injectable()
export class LogoutInterceptor implements HttpInterceptor {
constructor(private authenticationService: AuthenticationService) {}
intercept(
request: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
return next.handle(request).pipe(
catchError(error => {
// handle only 401 error
if (error instanceof HttpErrorResponse && error.status === 401) {
from(this.handleRequest(request));
return throwError(error);
}
return next.handle(request);
})
);
}
private async handleRequest(request: HttpRequest<any>) {
const isRetriedRequest = request.headers.get("retry");
if (isRetriedRequest) {
await this.authenticationService.logout();
}
}
}
I need to refresh token in HttpInterceptor before the request is made, to do it I check the access token before the request and call refresh if it's expired.
Currently, my interceptor looks like this:
#Injectable()
export class TokenInterceptor implements HttpInterceptor {
private refreshTokenSubject = new BehaviorSubject(null);
private refreshTokenObservable = this.refreshTokenSubject.asObservable();
private isRefreshingToken = false;
constructor(private authService: AuthService) {
}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const tokenData = AuthService.getCurrentSessionData();
if (!this.isRefreshingToken) {
// if no token set - make request as-is
const tokenSet = tokenData && tokenData.token;
if (!tokenSet) {
return next.handle(request);
}
// proceed if token not expired
const tokenExpired = new Date(tokenData.expirationDate) < new Date();
if (!tokenExpired) {
return next.handle(this.setAuthHeader(request, tokenData.token));
}
// check if we can refresh the token and logout instantly if not
const tokenRefreshable = tokenData.refreshToken && new Date(tokenData.refreshTokenExpirationDate) > new Date();
if (!tokenRefreshable) {
this.authService.logout();
return Observable.throw('');
}
this.isRefreshingToken = true;
// make all subsequent requests wait for new token
this.refreshTokenSubject.next(null);
// make refresh request
return this.authService.refreshToken()
.switchMap((res: any) => {
AuthService.storeSessionData(res, Utils.getLocalStorageItem(STORAGE_KEYS.STAY_LOGGED_IN));
this.isRefreshingToken = false;
// let subsequent awaiting proceed
this.refreshTokenSubject.next(res.access_token);
return next.handle(this.setAuthHeader(request, res.access_token));
})
.catch((err) => {
this.authService.logout();
return Observable.throw('');
})
.finally(() => {
this.isRefreshingToken = false;
});
} else {
// if token refreshing in progress - wait for new token
return this.refreshTokenObservable
.filter(token => token !== null)
.take(1)
.switchMap((token) => {
return next.handle(this.setAuthHeader(request, token));
});
}
}
private setAuthHeader(request, token) {
return request.clone({
setHeaders: {
Authorization: `Bearer ${token}`,
},
});
}
}
The problem is that this.authService.refreshToken() never makes the request and subsequent requests never proceed. I guess it's because nothing subscribes to the observable returned from HttpClient, here is the refreshToken method code:
public refreshToken() {
const tokenData = AuthService.getCurrentSessionData();
return this.http.post(
`${environment.apiPath}/auth/refresh`,
{ refresh_token: tokenData.refreshToken },
);
}
How can I fix this code to make refreshToken request and let other requests proceed after it as intended?
#Injectable()
export class RequestInterceptorService implements HttpInterceptor {
#BlockUI() blockUI: NgBlockUI;
isRefreshingToken = false;
tokenSubject: BehaviorSubject<string> = new BehaviorSubject<string>(null);
oldToken = localStorage.getItem('access_token');
constructor(private authService: AuthService, private localStorageToken: AppLocalStorage,
private route: ActivatedRoute, private router: Router) {}
addToken(req: HttpRequest<any>, token: string): HttpRequest<any> {
return req.clone({ setHeaders: { Authorization: 'Bearer ' + token, 'Access-Control-Allow-Origin': '*' }});
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpSentEvent | HttpHeaderResponse
| HttpProgressEvent | HttpResponse<any> | HttpUserEvent<any>> {
return next.handle(this.addToken(req, this.authService.getAuthToken()))
.catch(error => {
if (error instanceof HttpErrorResponse) {
switch ((<HttpErrorResponse>error).status) {
case 400:
return this.handle400Error(error);
case 401: this.authService.refresh().subscribe((data) => { // A call has been made at an instant where network get 401 status code,It will prevent the asynchronous way of handling network calls.
this.localStorageToken.setRefreshTokens(data); //localstorageToken is used to store all the response, including access token, expiry time and refresh token to get stored in localstorage.
}
);
return this.handle401Error(req, next);
// default: this.logoutUser();
}
} else {
return Observable.throw(error);
}
});
}
handle400Error(error) {
if (error && error.status === 400 && error.error && error.error.error === 'invalid_grant') {
// If we get a 400 and the error message is 'invalid_grant', the token is no longer valid so logout.
// console.log('Bad Error');
return this.logoutUser();
}
return Observable.throw(error);
}
handle401Error(req: HttpRequest<any>, next: HttpHandler) {
if (!this.isRefreshingToken) {
this.isRefreshingToken = true;
// Reset here so that the following requests wait until the token
// comes back from the refreshToken call.
this.tokenSubject.next(null);
return this.authService.refreshToken()
.switchMap((newToken: string) => {
newToken = localStorage.getItem('access_token');
if (newToken) {
this.tokenSubject.next(newToken);
if (this.oldToken === newToken) {
return this.logoutUser();
} else {
return next.handle(this.addToken(req, newToken));
}
}
// If we don't get a new token, we are in trouble so logout.
return this.logoutUser();
})
.catch(error => {
// If there is an exception calling 'refreshToken', bad news so logout.
return this.logoutUser();
})
.finally(() => {
this.isRefreshingToken = false;
});
} else {
return this.tokenSubject
.filter(token => token != null)
.take(1)
.switchMap(token => {
return next.handle(this.addToken(req, token));
});
}
}
logoutUser() {
// Route to the login page (implementation up to you)
this.router.navigate(['login']).then(() => { this.blockUI.stop(); });
return Observable.throw('');
}
}
Now the AuthService are for getting the refresh token and also for login,
This service are called whenever refresh token are needed, I gave 2 sec gap for fetching the refresh token
export class AuthService {
private TokenApi = AppSettings.DEVELOPMENT_API;
private newToken = ' ';
private current_token: string;
private refresh_token: string = localStorage.getItem('refresh_token');
constructor(private http: HttpClient, private localStorageToken: AppLocalStorage) {
}
login(username: string, password: string): Observable<TokenParams> {
const headers = new HttpHeaders({'content-type': 'application/x-www-form-urlencoded'});
const loginApi = this.TokenApi + '/oauth/token?username=' + username + '&password=' + password + '&grant_' +
'type=password....';
return this.http.post<TokenParams>(loginApi, '', {headers: headers});
}
refresh(): Observable<TokenParams> {
this.refresh_token = localStorage.getItem('refresh_token');
const refreshToken = this.TokenApi + '/oauth/token?refresh_token=' + this.refresh_token + '&grant_' +
'type=refresh_token...';
return this.http.post<TokenParams>(refreshToken, '' );
}
logout() {
this.localStorageToken.emptyLocalStorage();
}
getAuthToken() {
this.current_token = localStorage.getItem('access_token');
return this.current_token;
}
refreshToken(): Observable<string> {
this.newToken = localStorage.getItem('access_token');
this.current_token = this.newToken;
return Observable.of(localStorage.getItem('access_token')).delay(2000);
}
}
Hi I am trying to figure out how implement the new angular interceptors and handle 401 unauthorized errors by refreshing the token and retrying the request. This is the guide I have been following: https://ryanchenkie.com/angular-authentication-using-the-http-client-and-http-interceptors
I am successfully caching the failed requests and can refresh the token but I cannot figure out how to resend the requests that previously failed. I also want to get this to work with the resolvers I am currently using.
token.interceptor.ts
return next.handle( request ).do(( event: HttpEvent<any> ) => {
if ( event instanceof HttpResponse ) {
// do stuff with response if you want
}
}, ( err: any ) => {
if ( err instanceof HttpErrorResponse ) {
if ( err.status === 401 ) {
console.log( err );
this.auth.collectFailedRequest( request );
this.auth.refreshToken().subscribe( resp => {
if ( !resp ) {
console.log( "Invalid" );
} else {
this.auth.retryFailedRequests();
}
} );
}
}
} );
authentication.service.ts
cachedRequests: Array<HttpRequest<any>> = [];
public collectFailedRequest ( request ): void {
this.cachedRequests.push( request );
}
public retryFailedRequests (): void {
// retry the requests. this method can
// be called after the token is refreshed
this.cachedRequests.forEach( request => {
request = request.clone( {
setHeaders: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${ this.getToken() }`
}
} );
//??What to do here
} );
}
The above retryFailedRequests() file is what I can't figure out. How do I resend the requests and make them available to the route through the resolver after retrying?
This is all the relevant code if that helps: https://gist.github.com/joshharms/00d8159900897dc5bed45757e30405f9
My final solution. Works with parallel requests.
UPDATE: The code updated with Angular 9 / RxJS 6, error handling and fix looping when refreshToken fails
import { HttpRequest, HttpHandler, HttpInterceptor, HTTP_INTERCEPTORS } from "#angular/common/http";
import { Injector } from "#angular/core";
import { Router } from "#angular/router";
import { Subject, Observable, throwError } from "rxjs";
import { catchError, switchMap, tap} from "rxjs/operators";
import { AuthService } from "./auth.service";
export class AuthInterceptor implements HttpInterceptor {
authService;
refreshTokenInProgress = false;
tokenRefreshedSource = new Subject();
tokenRefreshed$ = this.tokenRefreshedSource.asObservable();
constructor(private injector: Injector, private router: Router) {}
addAuthHeader(request) {
const authHeader = this.authService.getAuthorizationHeader();
if (authHeader) {
return request.clone({
setHeaders: {
"Authorization": authHeader
}
});
}
return request;
}
refreshToken(): Observable<any> {
if (this.refreshTokenInProgress) {
return new Observable(observer => {
this.tokenRefreshed$.subscribe(() => {
observer.next();
observer.complete();
});
});
} else {
this.refreshTokenInProgress = true;
return this.authService.refreshToken().pipe(
tap(() => {
this.refreshTokenInProgress = false;
this.tokenRefreshedSource.next();
}),
catchError(() => {
this.refreshTokenInProgress = false;
this.logout();
}));
}
}
logout() {
this.authService.logout();
this.router.navigate(["login"]);
}
handleResponseError(error, request?, next?) {
// Business error
if (error.status === 400) {
// Show message
}
// Invalid token error
else if (error.status === 401) {
return this.refreshToken().pipe(
switchMap(() => {
request = this.addAuthHeader(request);
return next.handle(request);
}),
catchError(e => {
if (e.status !== 401) {
return this.handleResponseError(e);
} else {
this.logout();
}
}));
}
// Access denied error
else if (error.status === 403) {
// Show message
// Logout
this.logout();
}
// Server error
else if (error.status === 500) {
// Show message
}
// Maintenance error
else if (error.status === 503) {
// Show message
// Redirect to the maintenance page
}
return throwError(error);
}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<any> {
this.authService = this.injector.get(AuthService);
// Handle request
request = this.addAuthHeader(request);
// Handle response
return next.handle(request).pipe(catchError(error => {
return this.handleResponseError(error, request, next);
}));
}
}
export const AuthInterceptorProvider = {
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
};
With the latest version of Angular (7.0.0) and rxjs (6.3.3), this is how I created a fully functional Auto Session recovery interceptor ensuring, if concurrent requests fail with 401, then also, it should only hit token refresh API once and pipe the failed requests to the response of that using switchMap and Subject. Below is how my interceptor code looks like. I have omitted the code for my auth service and store service as they are pretty standard service classes.
import {
HttpErrorResponse,
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpRequest
} from "#angular/common/http";
import { Injectable } from "#angular/core";
import { Observable, Subject, throwError } from "rxjs";
import { catchError, switchMap } from "rxjs/operators";
import { AuthService } from "../auth/auth.service";
import { STATUS_CODE } from "../error-code";
import { UserSessionStoreService as StoreService } from "../store/user-session-store.service";
#Injectable()
export class SessionRecoveryInterceptor implements HttpInterceptor {
constructor(
private readonly store: StoreService,
private readonly sessionService: AuthService
) {}
private _refreshSubject: Subject<any> = new Subject<any>();
private _ifTokenExpired() {
this._refreshSubject.subscribe({
complete: () => {
this._refreshSubject = new Subject<any>();
}
});
if (this._refreshSubject.observers.length === 1) {
this.sessionService.refreshToken().subscribe(this._refreshSubject);
}
return this._refreshSubject;
}
private _checkTokenExpiryErr(error: HttpErrorResponse): boolean {
return (
error.status &&
error.status === STATUS_CODE.UNAUTHORIZED &&
error.error.message === "TokenExpired"
);
}
intercept(
req: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
if (req.url.endsWith("/logout") || req.url.endsWith("/token-refresh")) {
return next.handle(req);
} else {
return next.handle(req).pipe(
catchError((error, caught) => {
if (error instanceof HttpErrorResponse) {
if (this._checkTokenExpiryErr(error)) {
return this._ifTokenExpired().pipe(
switchMap(() => {
return next.handle(this.updateHeader(req));
})
);
} else {
return throwError(error);
}
}
return caught;
})
);
}
}
updateHeader(req) {
const authToken = this.store.getAccessToken();
req = req.clone({
headers: req.headers.set("Authorization", `Bearer ${authToken}`)
});
return req;
}
}
As per #anton-toshik comment, I thought it's a good idea to explain the functioning of this code in a write-up. You can have a read at my article here for the explanation and understanding of this code (how and why it works?). Hope it helps.
I had to solve the following requirements:
✅ Refresh token only once for multiple requests
✅ Log out user if refreshToken failed
✅ Log out if user gets an error after first refreshing
✅ Queue all requests while token is being refreshed
As a result I've collected different options in order to refresh token in Angular:
Brute force solution with tokenRefreshed$ BehaviorSubject as a semaphore
Using caught parameter in catchError RxJS operator to retry request failed request
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
let retries = 0;
return this.authService.token$.pipe(
map(token => req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })),
concatMap(authReq => next.handle(authReq)),
// Catch the 401 and handle it by refreshing the token and restarting the chain
// (where a new subscription to this.auth.token will get the latest token).
catchError((err, restart) => {
// If the request is unauthorized, try refreshing the token before restarting.
if (err.status === 401 && retries === 0) {
retries++;
return concat(this.authService.refreshToken$, restart);
}
if (retries > 0) {
this.authService.logout();
}
return throwError(err);
})
);
}
Using retryWhen RxJS operator
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return this.authService.token$.pipe(
map(token => req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })),
concatMap(authReq => next.handle(authReq)),
retryWhen((errors: Observable<any>) => errors.pipe(
mergeMap((error, index) => {
// any other error than 401 with {error: 'invalid_grant'} should be ignored by this retryWhen
if (error.status !== 401) {
return throwError(error);
}
if (index === 0) {
// first time execute refresh token logic...
return this.authService.refreshToken$;
}
this.authService.logout();
return throwError(error);
}),
take(2)
// first request should refresh token and retry,
// if there's still an error the second time is the last time and should navigate to login
)),
);
}
All these options are horoughly tested and can be found in angular-refresh-token github repo
See also:
catchError - RxJS Reference
Andrei Ostrovski's final solution works really well, but does not work if the refresh token is also expired (assuming you're making an api call to refresh). After some digging, I realised that the refresh token API call was also intercepted by the interceptor. I've had to add an if statement to handle this.
intercept( request: HttpRequest<any>, next: HttpHandler ):Observable<any> {
this.authService = this.injector.get( AuthenticationService );
request = this.addAuthHeader(request);
return next.handle( request ).catch( error => {
if ( error.status === 401 ) {
// The refreshToken api failure is also caught so we need to handle it here
if (error.url === environment.api_url + '/refresh') {
this.refreshTokenHasFailed = true;
this.authService.logout();
return Observable.throw( error );
}
return this.refreshAccessToken()
.switchMap( () => {
request = this.addAuthHeader( request );
return next.handle( request );
})
.catch((err) => {
this.refreshTokenHasFailed = true;
this.authService.logout();
return Observable.throw( err );
});
}
return Observable.throw( error );
});
}
I ran into a similar problem as well and I think the collect/retry logic is overly complicated. Instead, we can just use the catch operator to check for the 401, then watch for the token refresh, and rerun the request:
return next.handle(this.applyCredentials(req))
.catch((error, caught) => {
if (!this.isAuthError(error)) {
throw error;
}
return this.auth.refreshToken().first().flatMap((resp) => {
if (!resp) {
throw error;
}
return next.handle(this.applyCredentials(req));
});
}) as any;
...
private isAuthError(error: any): boolean {
return error instanceof HttpErrorResponse && error.status === 401;
}
Based on this example, here's my piece
#Injectable({
providedIn: 'root'
})
export class AuthInterceptor implements HttpInterceptor {
constructor(private loginService: LoginService) { }
/**
* Intercept request to authorize request with oauth service.
* #param req original request
* #param next next
*/
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<any> {
const self = this;
if (self.checkUrl(req)) {
// Authorization handler observable
const authHandle = defer(() => {
// Add authorization to request
const authorizedReq = req.clone({
headers: req.headers.set('Authorization', self.loginService.getAccessToken()
});
// Execute
return next.handle(authorizedReq);
});
return authHandle.pipe(
catchError((requestError, retryRequest) => {
if (requestError instanceof HttpErrorResponse && requestError.status === 401) {
if (self.loginService.isRememberMe()) {
// Authrozation failed, retry if user have `refresh_token` (remember me).
return from(self.loginService.refreshToken()).pipe(
catchError((refreshTokenError) => {
// Refresh token failed, logout
self.loginService.invalidateSession();
// Emit UserSessionExpiredError
return throwError(new UserSessionExpiredError('refresh_token failed'));
}),
mergeMap(() => retryRequest)
);
} else {
// Access token failed, logout
self.loginService.invalidateSession();
// Emit UserSessionExpiredError
return throwError(new UserSessionExpiredError('refresh_token failed'));
}
} else {
// Re-throw response error
return throwError(requestError);
}
})
);
} else {
return next.handle(req);
}
}
/**
* Check if request is required authentication.
* #param req request
*/
private checkUrl(req: HttpRequest<any>) {
// Your logic to check if the request need authorization.
return true;
}
}
You may want to check if user enabled Remember Me to use refresh token for retrying or just redirect to logout page.
Fyi, the LoginService has the following methods:
- getAccessToken(): string - return the current access_token
- isRememberMe(): boolean - check if user have refresh_token
- refreshToken(): Observable / Promise - Request to oauth server for new access_token using refresh_token
- invalidateSession(): void - remove all user info and redirect to logout page
Ideally, you want to check isTokenExpired before request sent. And if expired refresh the token and add refreshed in the header.
Other than that retry operator may help with your logic of refreshing token on 401 response.
Use the RxJS retry operator in your service where you are making a request. It accepts a retryCount argument.
If not provided, it will retry the sequence indefinitely.
In your interceptor on response refresh the token and return the error. When your service gets back the error but now retry operator is being used so it will retry the request and this time with the refreshed token(Interceptor uses refreshed token to add in the header.)
import {HttpClient} from '#angular/common/http';
import { Injectable } from '#angular/core';
import { Observable } from 'rxjs/Rx';
#Injectable()
export class YourService {
constructor(private http: HttpClient) {}
search(params: any) {
let tryCount = 0;
return this.http.post('https://abcdYourApiUrl.com/search', params)
.retry(2);
}
}
On the most accepted answer by Andrei Ostrovski, people comment about memory leak when token refresh request fails for some reason. One could mitigate this by using RxJS timeout operator, like this:
//...
tokenRefreshTimeout = 60000;
//...
// Invalid token error
else if (error.status === 401) {
return this.refreshToken().pipe(
timeout(this.tokenRefreshTimeout), //added timeout here
switchMap(() => {
request = this.addAuthHeader(request);
return next.handle(request);
}),
//...
(sorry, I don't have enough rep to comment, also I cannot suggest an edit because the edit queue is always full)
To support ES6 syntax the solution needs to be bit modify and that is as following also included te loader handler on multiple request
private refreshTokenInProgress = false;
private activeRequests = 0;
private tokenRefreshedSource = new Subject();
private tokenRefreshed$ = this.tokenRefreshedSource.asObservable();
private subscribedObservable$: Subscription = new Subscription();
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (this.activeRequests === 0) {
this.loaderService.loadLoader.next(true);
}
this.activeRequests++;
// Handle request
request = this.addAuthHeader(request);
// NOTE: if the flag is true it will execute retry auth token mechanism ie. by using refresh token it will fetch new auth token and will retry failed api with new token
if (environment.retryAuthTokenMechanism) {
// Handle response
return next.handle(request).pipe(
catchError(error => {
if (this.authenticationService.refreshShouldHappen(error)) {
return this.refreshToken().pipe(
switchMap(() => {
request = this.addAuthHeader(request);
return next.handle(request);
}),
catchError(() => {
this.authenticationService.setInterruptedUrl(this.router.url);
this.logout();
return EMPTY;
})
);
}
return EMPTY;
}),
finalize(() => {
this.hideLoader();
})
);
} else {
return next.handle(request).pipe(
catchError(() => {
this.logout();
return EMPTY;
}),
finalize(() => {
this.hideLoader();
})
);
}
}
ngOnDestroy(): void {
this.subscribedObservable$.unsubscribe();
}
/**
* #description Hides loader when all request gets complete
*/
private hideLoader() {
this.activeRequests--;
if (this.activeRequests === 0) {
this.loaderService.loadLoader.next(false);
}
}
/**
* #description set new auth token by existing refresh token
*/
private refreshToken() {
if (this.refreshTokenInProgress) {
return new Observable(observer => {
this.subscribedObservable$.add(
this.tokenRefreshed$.subscribe(() => {
observer.next();
observer.complete();
})
);
});
} else {
this.refreshTokenInProgress = true;
return this.authenticationService.getNewAccessTokenByRefreshToken().pipe(tap(newAuthToken => {
this.authenticationService.updateAccessToken(newAuthToken.access_token);
this.refreshTokenInProgress = false;
this.tokenRefreshedSource.next();
}));
}
}
private addAuthHeader(request: HttpRequest<any>) {
const accessToken = this.authenticationService.getAccessTokenOnly();
return request.clone({
setHeaders: {
Authorization: `Bearer ${accessToken}`
}
});
}
/**
* #todo move in common service or auth service once tested
* logout and redirect to login
*/
private logout() {
this.authenticationService.removeSavedUserDetailsAndLogout();
}
My Answer
In this case just handler 401
#Injectable()
export class AuthHttpInterceptor implements HttpInterceptor {
logoutUser$ = defer(() => (this.authService.logout(), EMPTY));
refresh$ = defer(() => this.authService.refreshTokenFromServer()).pipe(catchError(() => this.logoutUser$), share());
constructor(private authService: AuthService) { }
private applyCredentials(request: HttpRequest<any>): HttpRequest<any> {
return request.clone({
setHeaders: { Authorization: 'Bearer ' + this.authService.accessToken }
});
}
public intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (InterceptorSkipHeader.checkHeader(request)) {
const req = InterceptorSkipHeader.deleteHeader(request);
return next.handle(req);
}
const nextHandle$ = defer(() => next.handle(this.applyCredentials(request)));
return iif(() => this.authService.tokenIsEmpty, this.logoutUser$, nextHandle$).pipe(this.httpErrorsHandler());
}
httpErrorsHandler() {
return (source$: Observable<any>) => source$.pipe(
catch401Error(() => this.handle401Error(source$)),
catch400Error((err) => EMPTY),
catch403Error((err) => EMPTY),
catch406Error((err) => EMPTY),
catch500Error((err) => EMPTY),
);
}
handle401Error(retry$: Observable<any>): Observable<any> {
return retry$.pipe(
startWhen(this.refresh$),
takeUntil(this.authService.logout$),
catch401Error(() => this.logoutUser$),
);
}
}
full code ( auth-http-interceptor.ts )
step 1, Create two Observable
logoutUser$ :
use defer() do your logout logic (like clear token from LocalStorage) and retun EMPTY
refresh$ :
use defer create refresh$ Observable, make it always take new refresh token to call refresh API
logout on catch error
share() this Observable(make all 401 wait same refresh API back)
logoutUser$ = defer(() => (this.authService.logout(), EMPTY));
refresh$ = defer(() => this.authService.refreshTokenFromServer()).pipe(catchError(() => this.logoutUser$), share());
step 2, Skip interceptor
just make api skip interceptor ( uitls.ts )
class Xheader {
static readonly interceptorSkipHeader = new Xheader('interceptorSkipHeader');
readonly headers = { [this.headerName]: this.headerName };
readonly options = { headers: this.headers };
private constructor(readonly headerName: string) { }
public checkHeader({ headers }: HttpRequest<any>) {
return headers.has(this.headerName);
}
public deleteHeader(request: HttpRequest<any>) {
return request.clone({ headers: request.headers.delete(this.headerName) });
}
}
export const InterceptorSkipHeader = Xheader.interceptorSkipHeader;
like this InterceptorSkipHeader.options ( auth.service.ts)
refreshTokenFromServer(): Observable<Token> {
return this.http.post<Token>(this.authApi + '/refreshToken', this.token, InterceptorSkipHeader.options).pipe(setTokenToLocalStorage());
}
step 3, Interceptor
Has skip header InterceptorSkipHeader.checkHeader(request)
delete and return without handler
Else, handler
create nextHandle$ with access token : applyCredentials(request) use defer() ( always take new access token )
use iif() check if token is empty will logoutUser$, else nextHandle$
add httpErrorsHandler() operator, handler this stream
public intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (InterceptorSkipHeader.checkHeader(request)) {
const req = InterceptorSkipHeader.deleteHeader(request);
return next.handle(req);
}
const nextHandle$ = defer(() => next.handle(this.applyCredentials(request)));
return iif(() => this.authService.tokenIsEmpty, this.logoutUser$, nextHandle$).pipe(this.httpErrorsHandler());
}
Add access token function
private applyCredentials(request: HttpRequest<any>): HttpRequest<any> {
return request.clone({
setHeaders: { Authorization: 'Bearer ' + this.authService.accessToken }
});
}
step 4, Custom operator
We should create some custom operator before error Handler
catchHttpError operator
In this case we just handler 401
catch401Error : catch http 401
catch400Error : catch http 400
catch403Error : catch http 403
catch406Error : catch http 406
catch500Error : catch http 500
function catchHttpError(...status: Array<number>) {
const statusMap = status.reduce((m, v) => m.set(v, v), new Map());
return (next: (err: HttpErrorResponse) => Observable<any>) => {
return catchError((err) => err instanceof HttpErrorResponse && statusMap.has(err.status) ? next(err) : throwError(err));
};
}
const catch401Error = catchHttpError(401);
const catch400Error = catchHttpError(400);
const catch403Error = catchHttpError(403);
const catch406Error = catchHttpError(406);
const catch500Error = catchHttpError(500);
startWhen operator (uitls.ts)
equal delayWhen() second parameter (subscriptionDelay)
export function startWhen<T>(subscriptionDelay: Observable<any>) {
return (source$: Observable<T>) => concat(subscriptionDelay.pipe(take(1), ignoreElements()), source$);
}
step 5, Http error handler
In this case we just handler 401
catch401Error must be the first (make sure other error handler will catch retry API error)
handle401Error(source$) will retry source$ (previous Observable)
httpErrorsHandler() {
return (source$: Observable<any>) => source$.pipe(
catch401Error(() => this.handle401Error(source$)),
catch400Error((err) => EMPTY),
catch403Error((err) => EMPTY),
catch406Error((err) => EMPTY),
catch500Error((err) => EMPTY),
);
}
handle401Error
startWhen() : retry$ will wait refresh$ complete than call retry API
In process, if authService.logout$ trigger will stop stream (unsubscribe)
If retry API still 401 error will logout user
handle401Error(retry$: Observable<any>): Observable<any> {
return retry$.pipe(
startWhen(this.refresh$),
takeUntil(this.authService.logout$),
catch401Error(() => this.logoutUser$),
);
}
https://medium.com/#eddylin1937/angular-interceptor-with-rxjs-refresh-token-176326c84a36
After api failed with HTTP Error 401 ,token-refresh api got called , all your failed and cached request can be retried using http interceptor.
if (this.isRefreshingToken && !req.url.endsWith(tokenURL)) {
// check if unique url to be added in cachedRequest
if (urlPresentIndex == -1) {
this.cachedRequests.push(req);
return this.tokenSubject.pipe(
switchMap(() => next.handle(req)),
tap((v) => {
// delete request from catchedRequest if api gets called
this.cachedRequests.splice(
this.cachedRequests.findIndex(
(httpRequest) => httpRequest.url == req.url
),
1
);
return EMPTY;
})
);
} else {
//already in cached request array
return EMPTY;
}
}
For more details you can read my medium article Token-Refresh-Interceptor-retry-failed-Requests
Check it out, how it works stackblitz
I got this creating a new request based on the url of the failed request and sending the same body of the failed request.
retryFailedRequests() {
this.auth.cachedRequests.forEach(request => {
// get failed request body
var payload = (request as any).payload;
if (request.method == "POST") {
this.service.post(request.url, payload).subscribe(
then => {
// request ok
},
error => {
// error
});
}
else if (request.method == "PUT") {
this.service.put(request.url, payload).subscribe(
then => {
// request ok
},
error => {
// error
});
}
else if (request.method == "DELETE")
this.service.delete(request.url, payload).subscribe(
then => {
// request ok
},
error => {
// error
});
});
this.auth.clearFailedRequests();
}
In your authentication.service.ts, you should have a HttpClient injected as a dependency
constructor(private http: HttpClient) { }
You can then re-submit the request (inside retryFailedRequests) as follow:
this.http.request(request).subscribe((response) => {
// You need to subscribe to observer in order to "retry" your request
});