Search code examples
angularauth-guard

Angular 8 .next doesn't seem to work as expected


I am trying to create a service that authenticates a user and stores the token in the localStorage. I want other services (specifically the auth guard) to be able to access the current user, so I have set up the constructor of the authentication service like this:

currentUserSubject: BehaviorSubject<Token>;

constructor(private http: HttpClient) {
    this.currentUserSubject = new BehaviorSubject<Token>(JSON.parse(localStorage.getItem('currentUser')));
}

When a user logs in using a http POST request, if it's successful call the next method on the currentSubject like this:

return this.http.post<Token>(`${environment.identityServerUrl}/connect/token`, params, httpOptions)
    .pipe(map(user => {
        localStorage.setItem('currentUser', JSON.stringify(user));
        console.log(user);
        this.currentUserSubject.next(user);
    }));

The problem is, it doesn't seem to work. If I check my auth guard, it just sees the user as null. Only when I refresh is it populated (because of the authentication service constructor).

My auth guard looks like this:

import { Injectable } from '@angular/core';
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { mapTo, take, filter } from 'rxjs/operators';

import { AuthenticationService } from './authentication.service';

@Injectable({ providedIn: 'root' })
export class AuthGuardService implements CanActivate {
    constructor(
        private router: Router,
        private authenticationService: AuthenticationService
    ) { }

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
        return this.authenticationService.currentUserSubject.pipe(
            filter(user => !!user), 
            mapTo(true),
            take(1)
        );
    }
}

Does anyone know what I am doing wrong?


If I change my canActivate method to this:

canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean|UrlTree {
    var user = this.authenticationService.currentUserSubject.value;
    console.log(user);

    if (user) return true;

    this.router.navigate(["login"],{ queryParams: { retUrl: route.url} });
    return false;
}

It should work, but it just the console log shows null and so it just stays on the login page.

I saw that you can get .value from this article: https://medium.com/@luukgruijs/understanding-rxjs-behaviorsubject-replaysubject-and-asyncsubject-8cc061f1cfc0


Solution

  • The subject next() method is used to send messages to an observable which are then sent to your angular components that are subscribed to that observable.

    You have only created a subject but you have not created an observable that your guard can subscribe to.

    currentUserSubject: BehaviorSubject<Token>;
    currentUser$ = this.currentUserSubject.asObservable();
    
    constructor(private http: HttpClient) {
        this.currentUserSubject = new BehaviorSubject<Token>(JSON.parse(localStorage.getItem('currentUser')));
    }
    

    By convention, observables have a $ sign at the end. Now that we have a created an observable which receives your subject's messages, we can subscribe to it in the guard.

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
        return this.authenticationService.currentUser$.subscribe(user => {
            console.log(user);
        });
    }
    

    I'm also not sure why you are using the filter, using map should be sufficient.