Skip to content

Instantly share code, notes, and snippets.

@alxhub
Last active July 4, 2020 18:29
Show Gist options
  • Save alxhub/1fa85d0b8ef0286b617566e65a0011b4 to your computer and use it in GitHub Desktop.
Save alxhub/1fa85d0b8ef0286b617566e65a0011b4 to your computer and use it in GitHub Desktop.
abstract class AuthService {
// Subject tracks the current token, or is null if no token is currently
// available (e.g. refresh pending).
private subject = new BehaviorSubject<string|null>(null);
readonly refreshToken: Observable<any>;
readonly token: Observable<string>;
constructor() {
// refreshToken, when subscribed, gets the new token from the backend,
// and then completes without values.
this.refreshToken = Observable.defer(() => {
// Defer allows us to easily execute some action when the Observable
// is subscribed. Here, we set the current token to `null` until the
// refresh operation is complete. This ensures no requests will be
// sent with a known bad token.
this.subject.next(null);
return this
// Next, we refresh the token from the server.
.doRefreshToken()
// Set it as the active token.
.do(token => this.subject.next(token))
// Drop the value, ensuring this Observable only completes when
// done and doesn't emit.
.ignoreElements()
// Finally, share the Observable so we don't attempt multiple
// refreshes at once.
.shareReplay();
});
// token, when subscribed, returns the latest token.
this.token = this
// Read the subject (stream of tokens).
.subject
// Filter out the `null` ones. This part ensure we wait for the next
// good token.
.filter(token => token !== null)
// Take the next good token.
.take(1);
// There's no current token to start, so refresh to start with. Optionally,
// we could set token up to refresh on the first subscription.
this.refreshToken.subscribe();
}
// Actually refresh the token. Left up to the user.
abstract doRefreshToken(): Observable<string>;
}
class AuthInterceptor implements HttpInterceptor {
constructor(private auth: AuthService) { }
private addToken(req: HttpRequest<any>): HttpRequest<any> {
return req.clone({ headers: req.headers.set('Authorization', `Bearer ${this.auth.getToken()}`) });
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return this
.auth
// Get the latest token from the auth service.
.token
// Map the token to a request with the right header set.
.map(token => req.clone({ headers: req.headers.set('Authorization', `Bearer ${token}`) }))
// Execute the request on the server.
.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).
.catch((err, restart) => {
// If the request is unauthorized, try refreshing the token before restarting.
if (err instanceof HttpErrorResponse && err.status === 401) {
return Observable.concat(this.auth.refreshToken, restart);
}
throw err;
});
}
}
@patrickatkeylogic
Copy link

There seems to be a bug in Angular causing this. New instances of services are created as many times as they are imported. Have a read here: angular/angular#12889.

@parker-mike
Copy link

parker-mike commented Aug 1, 2018

@alxhub, thanks for sharing this with us, would you please help me to understand how it makes sense to return shareReplay from defer? Since the suggested use is to subscribe from within the interceptor to the deferred Observable which returns each time a new observable, the result won't be shared with any other request and we will attempt as many refreshes at once as the number of failed requests... I've asked on SO, but no one can tell the rationale behind it...

@devansvd
Copy link

Currently facing errors in angular 7 because of RXjs migrations (5.x to 6.X). Any possible changes you recommend ?

@jsertx
Copy link

jsertx commented Jan 30, 2019

Amazing contribution dude. Many thanks.

@jsertx
Copy link

jsertx commented Jan 30, 2019

Currently facing errors in angular 7 because of RXjs migrations (5.x to 6.X). Any possible changes you recommend ?

Now you have to introduce the operators in a list of arguments to the .pipe method.

return this.auth.token .pipe( map(token => req.clone({ headers: req.headers.set('Authorization', Bearer ${token}) })) concatMap(authReq => next.handle(authReq)) catchError((err, restart) => { if (err instanceof HttpErrorResponse && err.status === 401) { return Observable.concat(this.auth.refreshToken, restart); } throw err; }) );

@CptDev
Copy link

CptDev commented Mar 4, 2019

hello guys, were you able to fix that infinite loop or is there some workaround to get this approach working?

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