Angular 4 Implicit Flow Callback and Router Guard
Asked Answered
M

2

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;    
  }
Minestrone answered 15/5, 2017 at 19:1 Comment(0)
R
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();
      }, {});
    });
} 
Rattlepate answered 28/5, 2017 at 18:46 Comment(4)
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); } };Minestrone
It's registered in the constructor and works outside the observable but not insideMinestrone
I'm actually only getting errors inside the .reduce if I try to use an injected service.Minestrone
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.Minestrone
B
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.

Bilberry answered 15/5, 2017 at 19:22 Comment(4)
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.Minestrone
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 methodBilberry
...in order to get data for a component before it actually loads.Bilberry
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.Minestrone

© 2022 - 2024 — McMap. All rights reserved.