neroniaky / angular-token

:key: Token based authentication service for Angular with interceptor and multi-user support. Works best with devise token auth for Rails. Example:
https://stackblitz.com/github/neroniaky/angular-token
MIT License
370 stars 187 forks source link

Guarding routes with currentUserData Not working after Refresh or Force link. #501

Open TimSwerts opened 5 years ago

TimSwerts commented 5 years ago

I'm submitting a...

Current behavior

I want to reopen an issue. I have kind of the same problem as in the issue #253. I tried to reload the .currentUserData twice and it's not working for me. I think it has something to do with the way I am confronted with this problem.

I am making a AuthGuard or let's say I made it and it works all the time when I am logged in an when I am browsing trough my application. Now I was testing the basic security of my application and I found out that when I use the .currentUserData in my logic of canActivate in my guard and then type in a guarded link, the .currentUserData will be empty so my guard isn't functioning.

I've tried to validate the Token but that doesn't work iether. I think the problem is when I force the browser to go to a certain page the authGaurd works before the component gets loaded in that way the .currentUserData will not be available.

If there is any solution for this problem or if you have extra questions, feel free to respond.

Expected behavior

Getting the currentUserData before the logic of the authGaurd works.

Environment

Angular-Token version: X.Y.Z Angular version: X.Y.Z

Bundler

Browser:

Others: Guard: image

Router with guard implementation: image

rikkipitt commented 5 years ago

@TimSwerts did you find a solution to this? I'm experiencing the same. Even if I call validateToken, the currentUserData is still undefined...

raysuelzer commented 5 years ago

I think I had a similar problem. I did two things, not sure which solved it.

1) I put an ngIf on the root component when a user is logged in to wait for currentUserData to be available.

2) I have a much more complicated AuthGuard that does some throttling and returns Observable instead of boolean. currentUserData can be undefined in the case you described. Note, it is much more complicated, because I am wrapping things in a ReplaySubject so that I don't sent 50 requests to a server to validate a token since multiple components may have the same AuthGuard on them.

The important thing in the code below is 1) your guard should return an Observable 2) Check if current user data is undefined, if it is, validate the token before continuing:

 if (this.authService.currentUserData === undefined) {
        return this.authService.validateToken();
     }

Full code:

@Injectable({
    providedIn: 'root'
})
export class AuthGuard implements CanActivate {
    subject: ReplaySubject<boolean>;
    validationObservable: Observable<boolean>;

    // TODO: https://github.com/neroniaky/angular2-token/issues/253
    private authServiceValidation$ = this.authService.validateToken()
        .pipe(
            map<any, boolean>((res: any) => res.success as boolean),
            catchError(res => {
                const responseIs4xx = getSafe(() => res.status.toString(), '').match(/4\d\d/);
                // User probably lost their internet connection
                // we can assume that the token is valid until we get a 400 or
                // invalid token response
                if (navigator.onLine === false || !responseIs4xx) {
                    this.toastrService.warning('Please check your internet connection.');
                    // Assume the current user is logged in
                    return observableOf(true);
                } else {
                    return observableOf(res.success as boolean);
                }
            }),
            switchMap((loggedIn) => {
                return setAppStateForLoggedInUser(this.appStateService, this.apollo).pipe(
                    switchMap(() => observableOf(loggedIn)),
                    catchError(() => observableOf(loggedIn))
                );
            }),
            tap((loggedIn) => {
                if (!loggedIn) {
                    this.router.navigate(['/login']);
                }
            }
            )
        );

    constructor(private authService: AngularTokenService,
        private router: Router,
        private apollo: Apollo,
        private appStateService: AppStateService,
        private toastrService: ToastrService
    ) {
        this.subject = new ReplaySubject<any>();

        // resetValidationObservable method
        // sets / resets the validation observable.
        // It is called here to set the validation observable.

        // But, it also needs to be called on the login page
        // if a user has been logged out due to a bad token.
        // Issue is: if token expires or becomes invalid
        // then user is redirected to login page, (good)
        // but when they sign back in they are then brought
        // back to the login page
        // This does not happen when a user clicks "sign out"
        // only when token expires or is invalidated outside
        // of the angular session.
        // More investigation into why this is happening
        // is needed, but for now this solves the problem.
        this.resetValidationObservable();
    }

    /**
    * Used to set or reset the replay observable.
    * This is important that it is reset or set
    * on the initial login.
    *
    * If it is not set, then it is possible
    * that the user cannot not login without
    * refreshing the page because the previous
    * value of false will be returned before
    * the updated credentials are returned by the
    * observable.
    **/
    resetValidationObservable() {
        this.validationObservable = this.subject
            .pipe(
                throttleTime(3000),
                switchMap(() => {
                    return this.authServiceValidation$;
                }),
                shareReplay<boolean>()
            );
    }

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
        // Triggers `validationObservable` observable to be fired.
        // since validationObservable listens to `subject`
        // and provides a throttle which prevents multiple
        // requests to the server
        this.subject.next(null);

        // Logic
        return this.validationObservable
            .pipe(

                switchMap(tokenValid => {
                    // tokenValid is the previous result we had
                    // (in the last 3 seconds) for validating the token

                    // Regardless of if the token is valid or not,
                    // check if the auth service has been called.
                    // there are times when a refresh will happen
                    // and becuase the request is throttled and cached
                    // auth service might not be initialized
                    // https://github.com/neroniaky/angular2-token/issues/253
                    if (this.authService.currentUserData === undefined) {
                        return this.authService.validateToken();
                    }

                    return observableOf(tokenValid);
                }),
                switchMap((tokenValid) => {
                    if (tokenValid === false) {
                        // user needs to log back in
                        return observableOf(false);
                    } else {
                        const roleOk = this.checkRole(route); // user is logged in, check permissions

                        // User shouldn't be here
                        if (!roleOk) {
                            // so redirect to the landing page if we arne't already there
                            // prevents an infinite loop
                            if (state.url.toLowerCase() !== 'select_campaign') {
                                this.router.navigateByUrl('select_campaign');
                            }

                            return observableOf(false);
                        }
                        // Looks good user is signed in and has permissions.
                        return observableOf(true);
                    }
                })
            );
    }

    /**
     * Check if the current user has the correct role to view the route
     *
     * @param route
     */
    checkRole(route: ActivatedRouteSnapshot) {
        const currentUserData = this.authService.currentUserData;
        const canActivateRoles: Array<string> = route.data.canActivateRoles;
        if (isNil(route.data.canActivateRoles)) {
            return true;
        }

        // Roles of user is included in the array of roles
        // that can activate this route
        if (canActivateRoles.includes(currentUserData['role'])) {
            return true;
        }
        console.warn('User does not have permission to access component.');
        return false;
    }

}