Skip to content

Instantly share code, notes, and snippets.

@Toilal
Last active February 21, 2023 10:30
Show Gist options
  • Star 45 You must be signed in to star a gist
  • Fork 8 You must be signed in to fork a gist
  • Save Toilal/8849bd63d53bd2df2dd4df92d3b12f26 to your computer and use it in GitHub Desktop.
Save Toilal/8849bd63d53bd2df2dd4df92d3b12f26 to your computer and use it in GitHub Desktop.
@auth0/angular2-jwt Authorization Service and HttpInterceptor supporting JWT Refresh Token (Angular 4.3+ & 5+)
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { JWT_OPTIONS, JwtInterceptor, JwtModule } from '@auth0/angular-jwt';
import { AuthorizationService } from './authorization.service';
import { environment } from '../../environments/environment';
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { RefreshTokenInterceptor } from './refresh-token-interceptor';
function jwtOptionsFactory (authorizationService: AuthorizationService) {
return {
tokenGetter: () => {
return authorizationService.getAccessToken();
},
blacklistedRoutes: [`${environment.apiBaseUrl}/login-check`]
};
}
@NgModule({
imports: [
CommonModule,
HttpClientModule,
JwtModule.forRoot({
jwtOptionsProvider: {
provide: JWT_OPTIONS,
useFactory: jwtOptionsFactory,
deps: [AuthorizationService]
}
})
],
providers: [
AuthorizationService,
JwtInterceptor, // Providing JwtInterceptor allow to inject JwtInterceptor manually into RefreshTokenInterceptor
{
provide: HTTP_INTERCEPTORS,
useExisting: JwtInterceptor,
multi: true
},
{
provide: HTTP_INTERCEPTORS,
useClass: RefreshTokenInterceptor,
multi: true
}
],
declarations: []
})
export class ApiModule {
}
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { environment } from '../../environments/environment';
import { Observable, ReplaySubject } from 'rxjs';
import { LoginResponse } from './login-response';
@Injectable({
providedIn: 'root'
})
export class AuthorizationService {
constructor (private httpClient: HttpClient) {
}
loginCheckUrl = `${environment.apiBaseUrl}/login-check`;
refreshTokenUrl = `${environment.apiBaseUrl}/refresh-token`;
login (username: string, password: string): Observable<LoginResponse> {
const body = new HttpParams()
.set('_username', username)
.set('_password', password);
const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded');
const postObservable = this.httpClient.post<LoginResponse>(this.loginCheckUrl, body.toString(), { headers });
const subject = new ReplaySubject<LoginResponse>(1);
subject.subscribe((r: LoginResponse) => {
this.setAccessToken(r.token);
this.setRefreshToken(r.refresh_token);
}, (err) => {
this.handleAuthenticationError(err);
});
postObservable.subscribe(subject);
return subject;
}
refresh (): Observable<LoginResponse> {
const body = new HttpParams().set('refresh_token', this.getRefreshToken());
const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded');
const refreshObservable = this.httpClient.post<LoginResponse>(this.refreshTokenUrl, body.toString(), { headers });
const refreshSubject = new ReplaySubject<LoginResponse>(1);
refreshSubject.subscribe((r: LoginResponse) => {
this.setAccessToken(r.token);
this.setRefreshToken(r.refresh_token);
}, (err) => {
this.handleAuthenticationError(err);
});
refreshObservable.subscribe(refreshSubject);
return refreshSubject;
}
logout () {
this.setAccessToken(null);
this.setRefreshToken(null);
}
isAuthenticated (): boolean {
return !!this.getAccessToken();
}
private handleAuthenticationError (err: any) {
// TODO: Only for authentication error codes
this.setAccessToken(null);
this.setRefreshToken(null);
}
private setAccessToken (accessToken: string) {
if (!accessToken) {
localStorage.removeItem('access_token');
} else {
localStorage.setItem('access_token', accessToken);
}
}
private setRefreshToken (refreshToken: string) {
if (!refreshToken) {
localStorage.removeItem('refresh_token');
} else {
localStorage.setItem('refresh_token', refreshToken);
}
}
getAccessToken () {
return localStorage.getItem('access_token');
}
getRefreshToken () {
return localStorage.getItem('refresh_token');
}
}
import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, throwError } from 'rxjs';
import { catchError, mergeMap } from 'rxjs/operators';
import { AuthorizationService } from './authorization.service';
import { JwtInterceptor } from '@auth0/angular-jwt';
@Injectable()
export class RefreshTokenInterceptor implements HttpInterceptor {
constructor (private authorizationService: AuthorizationService, private jwtInterceptor: JwtInterceptor) {
}
intercept (req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (this.jwtInterceptor.isWhitelistedDomain(req) && !this.jwtInterceptor.isBlacklistedRoute(req)) {
return next.handle(req).pipe(
catchError((err) => {
const errorResponse = err as HttpErrorResponse;
if (errorResponse.status === 401 && errorResponse.error.message === 'Expired JWT Token') {
return this.authorizationService.refresh().pipe(mergeMap(() => {
return this.jwtInterceptor.intercept(req, next);
}));
}
return throwError(err);
}));
} else {
return next.handle(req);
}
}
}
@nerumo
Copy link

nerumo commented Jun 21, 2018

Great and simple solution, thank you for sharing. How will the refresh token flow behave if there are multiple requests at the same time where the token expired? As I understand the code, for every request it would concurrently request a refresh token for each request. You should probably add a state where you keep the refreshSubject on the service as member for as long as it hasn't completed and always return the same subject for subsequent calls. What do you think?

@SimonTod
Copy link

Thanks you. It helped me a lot. Gave you a star =)

@dakipro
Copy link

dakipro commented Sep 19, 2018

Great and simple solution, thank you for sharing. How will the refresh token flow behave if there are multiple requests at the same time where the token expired? As I understand the code, for every request it would concurrently request a refresh token for each request. You should probably add a state where you keep the refreshSubject on the service as member for as long as it hasn't completed and always return the same subject for subsequent calls. What do you think?

Also interested in this, thanks!

@henyana
Copy link

henyana commented Nov 12, 2018

Thank you for sharing. It help a lot!

@novaknole
Copy link

Great and simple solution, thank you for sharing. How will the refresh token flow behave if there are multiple requests at the same time where the token expired? As I understand the code, for every request it would concurrently request a refresh token for each request. You should probably add a state where you keep the refreshSubject on the service as member for as long as it hasn't completed and always return the same subject for subsequent calls. What do you think?

HAVE YOU FIGURED IT OUT? THANKS

@sr9yar
Copy link

sr9yar commented Feb 27, 2019

Great and simple solution, thank you for sharing. How will the refresh token flow behave if there are multiple requests at the same time where the token expired? As I understand the code, for every request it would concurrently request a refresh token for each request. You should probably add a state where you keep the refreshSubject on the service as member for as long as it hasn't completed and always return the same subject for subsequent calls. What do you think?

HAVE YOU FIGURED IT OUT? THANKS

This is a server side issue. You should adjust so called 'grace period' for your tokens. This way the tokens will still be accepted for some time after they are expired.

@vella-nicholas
Copy link

Why does the JWT Interceptor need to be called explicitly? Can't you just declare the refresh token interceptor before the JWT one and then do return next.handle(req)?

@AlexAegis
Copy link

AlexAegis commented Aug 15, 2019

Cool gist, I modified it a little bit, because I'm using NgRx, and added automatic logout on failed refresh.

(If someone knows a way to express that throw-catch-throw mess at the bottom without messing up the types, let me know)

	public intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
		if (this.jwtInterceptor.isWhitelistedDomain(req) && !this.jwtInterceptor.isBlacklistedRoute(req)) {
			return next.handle(req).pipe(
				catchError((err: HttpErrorResponse) => {
					if (err.status === 401) {
						this.authStoreFacade.refresh();
						return this.authStoreFacade.refreshFinished$.pipe(
							mergeMap(a =>
								iif(
									() => a.type === refreshSuccess.type,
									this.jwtInterceptor.intercept(req, next),
									throwError(a.payload).pipe(
										catchError((e: HttpErrorResponse) => {
											this.authStoreFacade.logout();
											return throwError(e);
										})
									)
								)
							)
						);
					}
					return throwError(err);
				})
			);
		} else return next.handle(req);
	}

@nullptrerror
Copy link

Great and simple solution, thank you for sharing. How will the refresh token flow behave if there are multiple requests at the same time where the token expired? As I understand the code, for every request it would concurrently request a refresh token for each request. You should probably add a state where you keep the refreshSubject on the service as member for as long as it hasn't completed and always return the same subject for subsequent calls. What do you think?

HAVE YOU FIGURED IT OUT? THANKS

This is a server side issue. You should adjust so called 'grace period' for your tokens. This way the tokens will still be accepted for some time after they are expired.

This is not a server side issue. If the token is expired any request until the first token response will request a new token request. We need a refreshSubject or something similar to let other consecutive request know a token is pending some how.

@hassanasad
Copy link

hassanasad commented Jan 14, 2023

Thanks for this very neat solution. I have been using it for a while. However lately i have noticed if the browser sits idle for a little while and then some API calls are sent - sometimes they are missing the authorization header (checked it from server logs). I opened an issue with the jwt token repo. @Toilal Would it be possible for you to check that thread out ( auth0/angular2-jwt#763 )?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment