Angular-auth-oidc-client: Using Route Guards and Auto Login

Created on 9 Oct 2017  路  6Comments  路  Source: damienbod/angular-auth-oidc-client

Hi Damien,

thanks for your great library and all your work you put into it!

I'm trying to protect all routes via a route guard. The route guard should first check if the user is logged in and then if the user has the required role to access this path. This was my first approach:

  canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
    const expectedRoles = next.data['expectedRoles'] as Array<string>;
    if (this.isAuthorized) { // subscription to this.oidcSecurityService.getIsAuthorized()
      // check if user has required roles in access token
      if (this.hasRequiredRoles(expectedRoles)) {
        // user can access path
        return true;
      } else {
        // user doesn't have required role
        this.router.navigate(['/forbidden']);
        return false;
      }
    } else {
      // trigger login
      this.oidcSecurityService.authorize();
      return false;
    }
  }

At first I had similar issues like Brent described in #82. I ended up in a loop, because after the redirect from the IDP - when the user was successfully authenticated - the subscription to _oidcSecurityService.getIsAuthorized()_ is processed after the guard performs the check so that _this.isAuthorized_ is never true.

I followed your example (https://github.com/damienbod/angular-auth-oidc-sample-google-openid/blob/master/src/AngularClient/angularApp/app/app.component.ts) to resolve the problem.

So I split the check: I moved login/authentication to app.component.ts, created the auto-login component and left only the role check in the guard.
If I disable the guard everything works like expected: If the user is not logged in, he is redirected to the IDP and _after_ the login to the page originally requested page.

When enabling the guard, it is processed before the logic in _app.component.ts_ so that for an unauthenticated user the role check always fails and _canActivate()_ returns false. This always changes the path to 'forbidden', if I remove the _router.navigate(..)_ the path seems to be reset.

So I still need a way to reliably check in the guard if the user is authenticated, so that the role check can be disabled if he is not.
But the best solution would be to perform all authentication and role checks in one place. Preferably the guard, so that required roles for a path can be defined in _app.routes.ts_.

Any help is appreciated.

Best regards

Christian

enhancement documentation

Most helpful comment

@hannesrohde @thecepe Thanks for sharing your solution. I used a solution like this in a similar situation: https://github.com/robisim74/AngularSPAWebAPI/blob/master/AngularSPAWebAPI/app/services/auth.guard.ts
As you can see, you can also use the flatMap operator if you have to check other observable in cascade.

All 6 comments

I am working together with @thecepe on this project.

We have been able to solve the concurrency issue by returning an Observable from the route guard that in turn depends on the Observable from oidcSecurityService.getIsAuthorized().

Stripped down to the bare minimum, the guard now looks like this:

@Injectable()
export class AuthGuard implements CanActivate {

  constructor(private router: Router, private oidcSecurityService: OidcSecurityService) { }

  canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean {
    const expectedRoles = next.data['expectedRoles'] as Array<string>;
    return this.oidcSecurityService.getIsAuthorized().map(
      (authorized) => {
        if (!authorized) {
          this.router.navigate(['/forbidden']);
          return false;
        } else if (!this.checkTokenRoles(this.oidcSecurityService.getToken(), expectedRoles)) {
          this.router.navigate(['/forbidden']);
          return false;
        } else {
          return true;
        }
      }
    ).catch(
      (err) => {
        return Observable.of(false);
      }
    );
  }

  checkTokenRoles(token: any, expectedRoles: Array<string>): boolean {
    // check if token has expected roles...
  }

}

@hannesrohde @thecepe Thanks for sharing your solution. I used a solution like this in a similar situation: https://github.com/robisim74/AngularSPAWebAPI/blob/master/AngularSPAWebAPI/app/services/auth.guard.ts
As you can see, you can also use the flatMap operator if you have to check other observable in cascade.

@hannesrohde @thecepe @damienbod : So I was trying to do the same and I came across this. I am trying to have the authgaurd do an getIsAuthorized . if true 'redirect home' else 'redirect idp' ( auto login).

So I scatched everything and took the skeleton of the above auth gaurd that you provided. I am running into the issue where the "getIsAuthorized" is always returning a false.

So this is the auth gaurd I got

@Injectable()
export class AuthGuard implements CanActivate {

  constructor(private router: Router, private oidcSecurityService: OidcSecurityService) { }

  canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean {
    const expectedRoles = next.data['expectedRoles'] as Array<string>;
    return this.oidcSecurityService.getIsAuthorized().map(
      (authorized) => {
        if (!authorized) {
          this.oidcSecurityService.authorize();
          return false;
        }else {
          return true;
        }
      }
    ).catch(
      (err) => {
        return Observable.of(false);
      }
      );
  }
}

So with the above code ,
if the user is never logged in : it redirects them to the idp

Now when I login at IDP and it redirects back to the app, it hits the auth gaurd and when it does a "getIsAuthorized", its returning a false . Thus getting into a loop

Was hoping to see if there is a full sample code for the above scenario. Will be of great help. Thanks

@hannesrohde @thecepe @damienbod : So continuing ob the previous qn... Looking for an authgaurd that does both auto login and authentication. This is the modified version of ur above bare minimum authguard

import { Injectable, OnDestroy } from '@angular/core';
import { CanActivate } from '@angular/router';
import { Router, RouterStateSnapshot, ActivatedRouteSnapshot } from '@angular/router';
import { OidcSecurityService, AuthorizationResult } from 'angular-auth-oidc-client';
import { Observable } from 'rxjs/Observable';

import 'rxjs/add/observable/throw';

@Injectable()
export class AuthGuard implements CanActivate {

  private authorized: boolean = false;

  constructor(private router: Router, private _oidcSecurityService: OidcSecurityService) { }

  public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean {
    if (this._oidcSecurityService.moduleSetup) {
      this.onOidcModuleSetup();
    } else {
      this._oidcSecurityService.onModuleSetup.subscribe(() => {
        this.onOidcModuleSetup();
      });
    }

    this._oidcSecurityService.onAuthorizationResult.subscribe(
      (authorizationResult: AuthorizationResult) => {
        debugger;
        if (authorizationResult === AuthorizationResult.authorized) {
          this.authorized = true;
        } else {
          this.authorized = false;
        }
      });

    return this.authorized;
  }

  private onOidcModuleSetup() {
    if (window.location.hash) {
      debugger;
      this._oidcSecurityService.authorizedCallback();
      // TODO : need to find a way to authorize here instead of blindly returning true
      this.authorized = true;
    } else {
      this._oidcSecurityService.getIsAuthorized().take(1).subscribe((authorized: boolean) => {
        if (!authorized) {
          this._oidcSecurityService.authorize();
        }
        this.authorized = authorized;
      });
    }
  }

}

It redirects to idp when no user info found. Else it returns a true letting the router handle the redirection to the respective route .

I was hoping if any of you could provide suggestion on the above?.

Also trying to implement async awaits within the true hash condition to see if we check the authorizationResult result to authrnticate the user.

Thanks

@nathanvv you should not put the setup in your authGuard. You should store the current route and redirect automatically the user after login activating _trigger_authorization_result_event_ and using _onAuthorizationResult_ in your _app.component_. See also here: #104

Was this page helpful?
0 / 5 - 0 ratings