Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sobhardwaj/f76c3dde52c8796de65b6b66bf54f868 to your computer and use it in GitHub Desktop.
Save sobhardwaj/f76c3dde52c8796de65b6b66bf54f868 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;
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment