1
votes

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

1
At a basic level, you are not actually returning anything from within the rxjs/operator map(). Therefore nothing will be returned the post to /connect/token and anything consuming it will receive null.Alexander Staroselsky

1 Answers

0
votes

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.