Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
9 tasks
TimSwerts opened this issue Apr 4, 2019 · 2 comments
Open
9 tasks

Comments

@TimSwerts
Copy link

TimSwerts commented Apr 4, 2019

I'm submitting a...

  • Regression (a behavior that used to work and stopped working in a new release)
  • [x ] Bug report
  • Performance issue
  • [x ] Feature request
  • [x ] Documentation issue or request
  • [x ] Other... Please describe:

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

  • [x ] Angular CLI (Webpack)
  • Webpack
  • SystemJS

Browser:

  • [x ] Chrome (desktop) version XX
  • Chrome (Android) version XX
  • Chrome (iOS) version XX
  • [x ] Firefox version XX
  • Safari (desktop) version XX
  • Safari (iOS) version XX
  • IE version XX
  • [x ] Edge version XX

Others:
Guard:
image

Router with guard implementation:
image

@rikkipitt
Copy link

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

@raysuelzer
Copy link

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

}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants