'Use an observable inside of an interceptor
I want to write an interceptor to add an auth token to all the requests. My token comes from a lib, angularx-social-login, that only provides an Observable to get the token. So I wrote this but my request is never sent, as if the value was never outputed from the observable.
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from "@angular/common/http";
import { SocialAuthService } from "angularx-social-login";
import {Observable, switchMap} from "rxjs";
import {Injectable} from "@angular/core";
@Injectable()
export class AuthInterceptorService implements HttpInterceptor {
constructor(private authService: SocialAuthService) {}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return this.authService.authState.pipe(
switchMap((user) => {
const token = user.idToken
if (token) {
request = request.clone({
setHeaders: {Authorization: `Bearer ${token}`}
});
}
return next.handle(request)
})
);
}
}
Solution 1:[1]
I guess what happens here is that by the time the subscription to the observable occurs, the value of authState was already emitted in the login process, so the request hangs waiting for a new value to be emitted, which happened already in the login process.
In order to deal with this, I suggest that you implement a service (providedInRoot) to be injected into the Login component and retrieve the user data in the login process.
You could subscribe to the authState observable of SocialAuthService service in the OnInit of the Login component:
import { Component, OnInit, OnDestroy } from '@angular/core';
import { SocialAuthService } from "angularx-social-login";
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
// ... the rest of import
@Component({
// ... Component decorator props (selector, templateUrl, styleUrls)
})
export class LoginComponent implements OnInit, OnDestroy {
destroy = new Subject<boolean>();
constructor(private authService: SocialAuthService, myUserService: MyUserService) { }
ngOnInit(): void {
this.authService.authState.pipe(takeUntil(this.destroy)).subscribe((user) => {
this.myUserService.user = user;
});
}
// ... the rest of the component
ngOnDestroy(): void {
this.destroy.next(true);
this.destroy.complete();
}
}
Then you could use the value from myUserService.user
in your interceptor to retrieve the token.
I have used the takeUntil rxjs operator with a Subject for ngOnDestroy, but you could also store the subscription as a class variable and perform the unsubscribe.
import { Component, OnInit, OnDestroy } from '@angular/core';
import { SocialAuthService } from "angularx-social-login";
import { Subscription } from 'rxjs';
// ... the rest of import
@Component({
// ... Component decorator props (selector, templateUrl, styleUrls)
})
export class LoginComponent implements OnInit, OnDestroy {
authStateSubscription: Subscription;
constructor(private authService: SocialAuthService, myUserService: MyUserService) { }
ngOnInit(): void {
this.authStateSubscription = this.authService.authState.subscribe((user) => {
this.myUserService.user = user;
});
}
// ... the rest of the component
ngOnDestroy(): void {
this.authStateSubscription.unsubscribe();
}
}
In both ways should work.
Solution 2:[2]
I tend to agree with @cybering explanation, to add to his solution, you could define authState
to be a BehaviorSubject. this way no matter when some component subscribes to it, it'll get an emitted value as BehaviorSubject "saves" the last emitted value and emits it upon new subscriptions without being dependent on time.
Solution 3:[3]
I am not 100% sure on this, but I think using a switchMap
you are switching to a new observable so return next.handle(request)
never gets nexted. Seems like authState is an observable, so we could do pipe(take(1))
without the need to unsubscribe.
I would be more concise and split up getting the token and setting Bearer.
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { SocialAuthService } from 'angularx-social-login';
import { Observable } from 'rxjs';
import { Injectable } from '@angular/core';
@Injectable()
export class AuthInterceptorService implements HttpInterceptor {
constructor(private authService: SocialAuthService) {}
intercept( request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
let token;
this.authService.authState.pipe(take(1)).subscribe(data => token = data);
if (token) {
// If we have a token, we set it to the header
request = request.clone({
setHeaders: { Authorization: `Bearer ${token}` },
});
}
return next.handle(request).pipe(
catchError((err) => {
if (err instanceof HttpErrorResponse) {
if (err.status === 401) {
// redirect user to the logout page
}
}
return throwError(err);
})
);
}
}
Would also recommend refactoring the way token can be received. Like updating it to some storage and receiving it from storage via authService. Subscribing each time to an observable is a pain.
AuthService.ts
getAuthToken():string {
return localeStarage.getItem('token')
}
You could then just do:
const token = this.authService.getAuthToken();
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 | gil |
Solution 3 |