Protect your routes with claims - a claim based route guard

The topic of Guards in Angular is not new and has already been described quite a few times
(i.e. see here).

In this post we will build the foundation of a Guard that can be handy if your backend is using Claim based Authorization.

The topic of securing routes is to be understood in the way that it delivers a better ui experience to the user based on his claims. It is in no way securing the Angular application as we all know the real security stuff happens in the backend (i.e. OIDC in combination with a JWT token).

The base class: ClaimGuardBase

This tiny abstract class is the starting point and implements the two interfaces CanActivate and CanActivateChild from the @angular/router module.

The abstract readonly property claim we need to implement once we implement a concrete ClaimGuard that extends from our base class.

import { Observable } from 'rxjs/Observable';
import { Router, CanActivate, CanActivateChild } from '@angular/router';
import { AuthService } from './auth.service';

@Injectable()
export abstract class ClaimGuardBase implements CanActivate, CanActivateChild {
  protected readonly abstract claim: string;

  public constructor(
    private _authService: AuthService,
    private _router: Router
  ) { }

  public canActivate(): Observable<boolean> | Promise<boolean> | boolean {
    const hasClaim = this._authService.hasClaim(this.claim);

    if (!hasClaim) {
      this._router.navigate(['401', this.claim]);
    }

    return hasClaim;
  }

  public canActivateChild(): Observable<boolean> | Promise<boolean> | boolean {
    return this.canActivate();
  }
}

Via Angular’s DI we inject the AuthService and in the above case the Router. The former one provides the hasClaim(claim: string) - method that checks whether our claim exists or doesn’t. For the latter you see an implementation of routing to a 401 component. We could also have injected the MessageBus service I have described here and send a message that would be handled by a toast component.

Testing the ClaimGuardBase class

If we now have a look at the unit tests we should get the idea I think.

The unit tests define a TestClaimGuard that extends from our ClaimGuardBase class. There we need to define the claim property and assign it with our claim name.

import {
  StorageService,
  AuthService,
  ClaimGuardBase
} from './index';

describe('ClaimGuardBase', () => {
  let router: Router;
  let authService: AuthService;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [
        RouterModule.forChild([
          { path: '401/:requiredClaim', redirectTo: 'home' }
        ]),
        RouterTestingModule
      ],
      providers: [
        StorageService,
        AuthService
      ]
    });

    router = TestBed.get(Router);
    authService = TestBed.get(AuthService);
  });

  it('should deny route activation', () => {
    // arrange

    // act
    const testClaimGuard = new TestClaimGuard(authService, router);

    // assert
    expect(testClaimGuard.canActivate()).toBe(false);
    expect(testClaimGuard.canActivateChild()).toBe(false);
  });

  it('should allow route activation', () => {
    // arrange
    authService.setClaims([TestClaimGuard.TESTCLAIM_KEY]);

    // act
    const testClaimGuard = new TestClaimGuard(authService, router);

    // assert
    expect(testClaimGuard.canActivate()).toBe(true);
    expect(testClaimGuard.canActivateChild()).toBe(true);
  });
});

class TestClaimGuard extends ClaimGuardBase {
  public static TESTCLAIM_KEY = 'TestClaim';
  protected readonly claim: string = TestClaimGuard.TESTCLAIM_KEY;
}

As we might noticed the above proposal uses the synchronous way of protecting a route. The CanActivate interfaces allow us to return:

  • an Observable
  • a Promise
  • a boolean (it is what the ClaimGuardBase and in particular the TestClaimGuard uses)

The abstract class could as well be used in a Promised or an Observable way. That depends on the use case we have to implement.

Summary

The above described approach shows one way to protect your Angular routes and components based on claims. Of course there are other ways to the solution for which I am very interested in.

So let me know… Thomas

Privacy & Cookies: This site uses cookies. By continuing to use this website, you agree to their use. To find out more, see here.