13

What is the best way to handle the implicit flow Callback in Angular 4? I want the Guard to wait until the user is redirected back with the token and it is stored before the Guard returns true or false, I'm getting the Access Denied route for a few seconds before I'm redirected back to check the token. Is there a better way to handle the AuthGuard than what I'm doing so I don't get the Access Denied before authentication completes?

How do I make the router guard wait for the redirect?

AppComponent

    ngOnInit() {    

         //if there is a hash then the user is being redirected from the AuthServer with url params
         if (window.location.hash && !this.authService.isUserLoggedIn()) {     

          //check the url hash
          this.authService.authorizeCallback();

        }
        else if (!this.authService.isUserLoggedIn()) {         

          //try to authorize user if they aren't login
          this.authService.tryAuthorize();       

  }    
}

AuthSerivce

tryAuthorize() {
       //redirect to open id connect /authorize endpoint
        window.location.href = this.authConfigService.getSignInEndpoint();
    }

    authorizeCallback() {       

        let hash = window.location.hash.substr(1);

        let result: any = hash.split('&').reduce(function (result: any, item: string) {
            let parts = item.split('=');
            result[parts[0]] = parts[1];
            return result;
        }, {});


        if (result.error && result.error == 'access_denied') {
            this.navigationService.AccessDenied();
        }
        else {

            this.validateToken(result);
        }
    }


    isUserLoggedIn(): boolean {       
        let token = this.getAccessToken();

        //check if there is a token      
        if(token === undefined || token === null || token.trim() === '' )
        {
            //no token or token is expired;
            return false;
        }

        return true;
    }


    getAccessToken(): string {              

        let token = <string>this.storageService.get(this.accessTokenKey);


        if(token === undefined || token === null || token.trim() === '' )
        {
            return '';
        }

        return token;
    }

    resetAuthToken() {
        this.storageService.store(this.accessTokenKey, '');
    }

    validateToken(tokenResults: any) {        

        //TODO: add other validations         

        //reset the token
        this.resetAuthToken();

        if (tokenResults && tokenResults.access_token) {

            //store the token
            this.storageService.store(this.accessTokenKey, tokenResults.access_token);

            //navigate to clear the query string parameters
            this.navigationService.Home();

        }
        else {
            //navigate to Access Denied
            this.navigationService.AccessDenied();
        }

    }
}

AuthGuard

 canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot){

    var hasAccess = this.authService.isUserLoggedIn();        

    if(!hasAccess)
    {
        this.naviationService.AccessDenied();
        return false;
    }
     return true;    
  }
Derek Brown
  • 4,232
  • 4
  • 27
  • 44
Fab
  • 904
  • 2
  • 14
  • 38

2 Answers2

6

If you want your guard to wait for asynchronous task, you need to change your AuthService to return an observable and emit value that you need inside of asynchronous task that you want to wait, in your case its reduce(). After that make a subscription in the guard. In this way you can make your guard wait for any asynchronous task.

AuthGuard

canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot){

  this.authService.isUserLoggedIn().map(logged => {
        if (logged) {
            return true;
        } else {
            this.naviationService.AccessDenied();
            return false;
        }
    }).catch(() => {
        this.naviationService.AccessDenied();
        return Observable.of(false);
    });
  }
}

part of AuthService

 isUserLoggedIn(): Observable<boolean> {
    return new Observable((observer) => {
      let hash = window.location.hash.substr(1);

      let result: any = hash.split('&').reduce(function (result: any, item: string) {
        let parts = item.split('=');
        result[parts[0]] = parts[1];

        if (result.error && result.error == 'access_denied') {
          observer.next(false);
          observer.complete();
        }
        else {
          this.validateToken(result);
        }
        let token = this.getAccessToken();

        //check if there is a token      
        if(token === undefined || token === null || token.trim() === '' )
        {
          //no token or token is expired;
          observer.next(false);
          observer.complete();
        }

        observer.next(true);
        observer.complete();
      }, {});
    });
} 
Yevgen
  • 4,519
  • 3
  • 24
  • 34
  • That seems to be working good but I have another issue I guess maybe related to using Observables. When I try to use an injected service inside the Obserable such as this.logger.log inside the isUserLoggedIn method it throws error in observer.js on this method: Observable.prototype._trySubscribe = function (sink) { try { return this._subscribe(sink); } catch (err) { sink.syncErrorThrown = true; sink.syncErrorValue = err; sink.error(err); } }; – Fab May 30 '17 at 15:21
  • It's registered in the constructor and works outside the observable but not inside – Fab May 30 '17 at 15:23
  • I'm actually only getting errors inside the .reduce if I try to use an injected service. – Fab May 30 '17 at 15:33
  • it works a lot better with the logic outside of reduce function. I guess I need to learn more about creating observable but I'm catching on to observer.next to return a value and observer.complete to complete which I haven't used yet but I think I get it now. – Fab May 30 '17 at 15:40
1

The CanActivate method can return an Observable, Promise, or Boolean, and Angular will know to unwrap it and handle everything async. You can alter your code to check for the necessary data before returning either a completed/failed Observable or a resolved/rejected Promise to the Angular router, and calling this.naviationService.AccessDenied() as a result of that asynchronous function.

joh04667
  • 7,159
  • 27
  • 34
  • 1
    How does that work with a redirect from another / authorization server? I understand the Observables with a data api but not so much with a redirect? can you provide a code example perhaps? Thanks. – Fab May 15 '17 at 19:32
  • Basically what is happening is that your `AuthGuard` gets called before Angular loads the route and component. In the current implementation, your `CanActivate` *immediately* returns false and navigates to `AccessDenied`. It looks like something set up with that route or an external redirect then reloads the page where the `AuthGuard` gets fired again and returns true. Depending on the implementation of the auth server, I can see where this could be difficult to weed out. You might want to look into having the window wait for a redirect, or check the `resolve` router method – joh04667 May 15 '17 at 19:49
  • ...in order to get data for a component before it actually loads. – joh04667 May 15 '17 at 19:49
  • yeah in app component ngOnInit() either redirects to the auth server or calls tryAuthorize which will valid the token then redirect to the Home route which I have has the AuthGuard which all the Guard does is check if they are logged in which just returns true or false if the token exists in storage. – Fab May 15 '17 at 20:09