Angular 2+ Authorization

Sample Project

Download a sample project specific to this tutorial configured with your Auth0 API Keys.

System Requirements
  • Angular 2+
Show requirements

The previous step covers how to protect the resources served by your API such that only users who have authenticated in your application (and who are properly authorized) can access them. While this deals with protecting data resources from your server, it is likely that you will also need some way to control authorization in your client-side single page application.

Access Control in Single Page Apps

Distinguishing between different users and controlling what they can and cannot access is typically known as access control. The way that access control is implemented depends on the needs of your application, but with Auth0 the most common approach is to use the scopes granted to a user as a way to make decisions about what they can see in the application and which routes they have access to.

For single page applications which get their data from APIs, there are two major things that need to be considered when authorization and access control are introduced:

  1. The particular data being requested from the API should only be released if the user making the request is authorized to access it
  2. The user should only be able to access client-side routes and see certain UI elements if they have an appropriate access level to do so

The previous step used a scope of read:messages for access to API resources. Since this scope indicates that the user has read-only access to data, it might be considered that the user has some kind of "regular user" access level. If you wanted some users to have write access to the same resource, and therefore some kind of "administrator" access level, you might consider introducing a scope of write:messages.

Auth0 makes no assumptions about how the scopes for your API map to various access levels, nor is there any restriction on what you call your scopes and access levels. These details are at your discretion.

Determining a User's Scopes

When login transactions are initiated, you can specify which scopes you would like to request. Typically this is done when auth0.WebAuth is instantiated, but it can also be done in an options object passed to the authorize method.

If a requested scope is available for the user, the access_token that gets signed and sent back for them will have a scope claim in the payload. The value of this claim will be a string with any scopes that were granted.

Determining UI behavior in the client-side application can be done by using the scopes that were granted for a user, but there's a catch: the access_token must be treated as opaque in client-side applications and thus must not be decoded there. This means that you cannot decode and read the payload of the access_token itself to find out which scopes were granted for the user.

The solution is to use the value of the scope param that comes back after authentication. This will be a string value containing all of the scopes that were granted for the user, separated by spaces. However, this param will only be populated if the scopes that are granted for the user differ in any way from what was originally requested.

The resulting workflow for determining which scopes a user has is as such:

  1. Check for a value on authResult.scope. If one exists, use that value because these are the scopes that were granted for the user.
  2. If there is no value for authResult.scope, it means that all of the scopes requested for the user were granted. In this scenario, the scopes that were requested when the authentication transaction was initiated can be used directly to make UI decisions.

Handle Scopes in the AuthService

Adjust your AuthService to use a local member with any scopes you would like to request when users log in. Use this member in the auth0.WebAuth instance.

// src/app/auth/auth.service.ts

requestedScopes: string = 'openid profile read:messages write:messages';

auth0 = new auth0.WebAuth({
  // ...
  scope: this.requestedScopes
});

In the setSession method, save the scopes granted for the user into local storage. The first place to check for these granted scope values is the scope key from the authResult. If something exists there it's because the scopes which were granted for the user differ from those that were requested. If there is nothing on authResult.scope, it means that the granted scopes match those that were requested, so the requested values can be used directly. If there are no values for either of these, you can fall back to an empty string.

// src/app/auth/auth.service.ts

private setSession(authResult): void {

  const scopes = authResult.scope || this.requestedScopes || '';

  // ...
  localStorage.setItem('scopes', JSON.stringify(scopes));
}

Add a method called userHasScopes which will check for a particular scope in local storage. This method should take an array of strings and check whether the array of scopes saved in local storage contains those values. This method can be used to conditionally hide and show various UI elements and to limit route access.

// src/app/auth/auth.service.ts

public userHasScopes(scopes: Array<string>): boolean {
  const grantedScopes = JSON.parse(localStorage.getItem('scopes')).split(' ');
  return scopes.every(scope => grantedScopes.includes(scope));
}

Conditionally Dislay UI Elements

The userHasScopes method can now be used alongside isAuthenticated to conditionally show and hide certain UI elements based on those two conditions.

<!-- src/app/app.component.html -->

<button
  class="btn btn-primary btn-margin"
  *ngIf="auth.isAuthenticated() && auth.userHasScopes(['write:messages'])"
  routerLink="/admin">
    Admin Area
</button>

Protect Client-Side Routes

For some routes in your application, you may want to only allow access if the user is authenticated. This check can be made with the canActivate hook.

Create a new service called AuthGuardService.

// src/app/auth/auth-guard.service.ts

import { Injectable } from '@angular/core';
import { Router, CanActivate } from '@angular/router';
import { AuthService } from './auth.service';

@Injectable()
export class AuthGuardService implements CanActivate {

  constructor(public auth: AuthService, public router: Router) {}

  canActivate(): boolean {
    if (!this.auth.isAuthenticated()) {
      this.router.navigate(['']);
      return false;
    }
    return true;
  }

}

In your route configuration, apply the AuthGuardService to the canActivate hook for whichever routes you wish to protect.

// src/app/app.routes.ts

import { AuthGuardService as AuthGuard } from './auth/auth-guard.service';

export const ROUTES: Routes = [
  // ...
  { path: 'profile', component: ProfileComponent, canActivate: [AuthGuard] },
  { path: 'ping', component: PingComponent, canActivate: [AuthGuard] },
];

The guard implements the CanActivate interface which requires a method called canActivate in the service. This method returns true if the user is authenticated and false if not. It also navigates the user to the home route if they aren't authenticated.

Limit Route Access Based on scope

To prevent access to client-side routes based on a particular scope, create another service called ScopeGuard. This service should use ActivatedRouteSnapshot to check for a set of expectedScopes passed in the data key of the route configuration.

// src/app/auth/scope-guard.service.ts

import { Injectable } from '@angular/core';
import { Router, CanActivate, ActivatedRouteSnapshot } from '@angular/router';
import { AuthService } from './auth.service';

@Injectable()
export class ScopeGuardService implements CanActivate {

  constructor(public auth: AuthService, public router: Router) {}

  canActivate(route: ActivatedRouteSnapshot): boolean {

    const scopes = (route.data as any).expectedScopes;

    if (!this.auth.isAuthenticated() || !this.auth.userHasScopes(scopes)) {
      this.router.navigate(['']);
      return false;
    }
    return true;
  }

}
// src/app/app.routes.ts

import { ScopeGuardService as ScopeGuard } from './auth/scope-guard.service';

export const ROUTES: Routes = [
  // ...
  { path: 'admin', component: AdminComponent, canActivate: [ScopeGuard], data: { expectedScopes: ['write:messages']} },
];

The user will now be redirected to the main route unless they have a scope of write:messages.

Conditionally Assign Scopes to Users

The default behavior when registering scopes in you API settings is that all of those scopes become immediately available and can be requested by any user. To properly handle access control, you will need to create policies which stipulate the conditions under which users can be granted certain scopes. This can be done with Rules. See the documentation for how to use Rules to create scope policies.

Considerations for Client-Side Access Control

In the context of access control on the client-side, it's important to note that any scope values that end up in local storage are simply a clue that the user has that particular set of scopes. There is nothing stopping a user from manually adjusting the scopes in local storage in an attempt to give themselves a higher level of access. Doing so, a user could force their way to a client-side route that they shouldn't have access to.

So how are you supposed to create a secure application if client-side routes are so easily hackable? The answer is to remember that browsers are public clients and that they must be treated as such. Any and all sensitive data which powers your application needs to be kept on your server instead of being included directly in your client-side SPA because your server is the only place that can act as a secure gatekeeper for that data.

If a user wants to get access to the data on your server, they will require a valid access_token to do so. Any attempt to modify an access_token will invalidate it. This means that if a user tried to edit the payload of their access_token to include different scopes, the token would lose its integrity and would be rendered useless.

It would be easy for a user to modify the scopes array in local storage and thus be able to navigate to a client-side route that they shouldn't be at, but it wouldn't do them much good. If the data required for a given route is retrieved from your server (as it should be), the only thing the user would see on the page is an empty shell.

Previous Tutorial
3. Calling an API
Next Tutorial
5. Token Renewal
Use Auth0 for FREECreate free Account