Use an observable inside of an interceptor

1.3k Views Asked by At

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)
      })
    );
  }
}
2

There are 2 best solutions below

2
cybering On BEST ANSWER

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.

0
gil On

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.