3

I am using angular 8 to make a SPA.

Firebase is used to authenticate the user both in the client as well as in the backend, so I need to send the jwt token in http.get request to the backend to authenticate the user.

Backend is an API made with django 2.2 and django rest framework which sends the api to be consumed in client application.

auth.service.ts

@Injectable({
  providedIn: 'root'
})

export class AuthService {
  userData: any; // Save logged in user data
  public userToken: string;

  constructor(
    public afs: AngularFirestore,   // Inject Firestore service
    public afAuth: AngularFireAuth, // Inject Firebase auth service
    public router: Router,
    public ngZone: NgZone // NgZone service to remove outside scope warning
  ) {
    /* Saving user data in localstorage when 
    logged in and setting up null when logged out */
    this.afAuth.authState.subscribe(user => {
      if (user) {
        this.userData = user;
        localStorage.setItem('user', JSON.stringify(this.userData));
        JSON.parse(localStorage.getItem('user'));
      } else {
        localStorage.setItem('user', null);
        JSON.parse(localStorage.getItem('user'));
      }
    });

  }


  GetToken(): string {
    this.afAuth.auth.onAuthStateChanged( user => {
      if (user) {
        user.getIdToken().then(idToken => {
          this.userToken = idToken;

            // this shows the userToken
          console.log('token inside getToken method ' + this.userToken);
        });
      }
    });

    // this shows userToken as undefined
    console.log('before return ' + this.userToken);
    return this.userToken;
  }

}

api.service.ts

@Injectable({
  providedIn: 'root'
})
export class ApiService {

  private url = environment.baseUrl;
  token: any;
  data: any;

  constructor(
    private http: HttpClient,
    private authService: AuthService,
    ) {}



    // old method to get emloyees data
  // public getEmployees(): Observable<Employee[]> {
  //   return this.http.get<Employee[]>(`${this.url}/employee/`);
  // }


  httpOptions = {
    headers: new HttpHeaders({
      'Content-Type': 'application/json',
      'Authorization': 'JWT ' + this.authService.GetToken()
    }),
  };


  public getEmployees(): Observable<Employee[]> {

      // token is undefined here
    console.log('token inside getEmployees method ' + this.token);

    return this.http.get<Employee[]>(`${this.url}/employee/`, this.httpOptions);
  }

}

The backend is working perfectly which I verified by adding the token in the httpOptions, like so:

httpOptions = {
    headers: new HttpHeaders({
      'Content-Type': 'application/json',
      'Authorization': 'JWT ' + 'ey.....'
    }),
  };

But when I try doing the same as given in code it doesn't work. The user token remains undefined.

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
isAif
  • 2,126
  • 5
  • 22
  • 34

2 Answers2

7

Peter's answer has the crux of it: getIdToken() is asynchronous, so by the time your return this.userToken; runs, the this.userToken = idToken; hasn't run yet. You should be able to see this from the output of your console.log statements.

For more on this see How to return value from an asynchronous callback function? I highly recommend studying this answer for a while, as this asynchronous behavior is incredibly common when dealing with web APIs.

The fix for your code is to return a Promise, instead of trying to return the value:

GetToken(): Promise<string> {
  return new Promise((resolve, reject) => {
    this.afAuth.auth.onAuthStateChanged( user => {
      if (user) {
        user.getIdToken().then(idToken => {
          this.userToken = idToken;
          resolve(idToken);    
        });
      }
    });
  })
}

In words: GetToken returns a promise that resolves once an ID token is available. If you know the user is already signed in when you call this function, you can simplify it to:

GetToken(): string {
    const user = firebase.authentication().currentUser;
    return user.getIdToken()
}

The difference is that the second function does not wait for the user to be signed in, so will fail if there is no signed in user.

You then use either of the above functions like this in getEmployees:

public getEmployees(): Observable<Employee[]> {
  return new Promise((resolve, reject) => 
    this.authService.GetToken().then((idToken) => {
      httpOptions = {
        headers: new HttpHeaders({
          'Content-Type': 'application/json',
          'Authorization': 'JWT ' + idToken
        }),
      };

      this.http.get<Employee[]>(`${this.url}/employee/`, this.httpOptions)
          .then(resolve).catch(reject);

    })
  })
}
zwessels
  • 617
  • 1
  • 10
  • 25
Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
  • I need to return Observable of type Employee[] from getEmployee() method but now it returns a promise. – isAif Oct 26 '19 at 07:01
  • My answer focuses on why your http options don't work properly, and shows how to fix that. It returns a promise that resolves into an `Employee[]`. It can probably also return an observable, as shown in these results: https://www.google.com/search?q=turn+promise+into+observable – Frank van Puffelen Oct 26 '19 at 14:01
  • I now understand how promise thanks to the links you provided. As for the conversion of promise to observable, I did it using 'from' in rxjs. I am now using http interceptors to add tokens to all the http request. – isAif Oct 26 '19 at 14:37
3

It is undefined here console.log('before return ' + this.userToken); because getIdToken() returns a Promise which means it is asynchronous, therefore the only way to access the userToken is inside the then() method.

Peter Haddad
  • 78,874
  • 25
  • 140
  • 134