Angular Authentication and Authorization - A Resource Based Approach


Authentication and Authorization is very crucial to all applications. However, defining authentication and authorization mechanisms for application access doesn't guarantee end-to-end security of an application whether it resides within a local system or made accessible over a network. Authentication and authorization mechanisms just contribute to verifying the user's identity. These mechanisms confirm the user is who he or she claims to be and define what actions the user is allowed to perform after granting access to the system. In addition, you would need to ensure the confidentiality and integrity of the communication and data exchanged during the application access and also to ensure non-repudiation and accountability make sure all actions are logged and available for audit.

The project structure

The application will be in two part:


  • Ther Server (ASP.Net WebAPI) - NodeJS, PHP etc will also work fine.
  • The Client (Angular App)


ASP.Net WebAPI

This is a fairly simple API that handles user login and token generation. It also fetches the logged in users roles.

Angular App

We will be implementing 2 layers to handle our application's Authentication and Authorization:

  • UI manipulation (Showing or hiding part of the screens based on the user permission)
  • Routing (When the user access a route that he doesn’t have permission to will redirect him or her to login page)


How we want it to work

We want all anonymous users to be redirected to login page when they navigate to our application.
We want to get all the user roles before loading our app (more like a resolve but for every route)
We want to save information about the user to a local storage for subsequent checks.

A walk through of the process

Let's start by looking at the ASP.Net WebAPI. Its just a simple web api that uses Microsoft.Owin.Security.OAuth for token generation.

First, I installed the following packages:

  • Microsoft.Owin.Host.System.Web
  • Microsoft.Owin.Security.OAuth
  • Microsoft.Owin.Cors


I then created a class named AuthTokenValidatorService that implements OAuthAuthorizationServerProvider like so:
public class AuthTokenValidatorService : OAuthAuthorizationServerProvider
    {
        public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
        {
            context.Validated();
        }

        public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
        {
            var identity = new ClaimsIdentity(context.Options.AuthenticationType);

            var mockUsers = new Models.MockUsers();

            var user = mockUsers.GetUsers().Where(x => x.username == context.UserName && x.password == context.Password);

            if (user.Any())
            {
                var loggedInUser = user.First();

                
                identity.AddClaim(new Claim("username", loggedInUser.username));
                identity.AddClaim(new Claim(ClaimTypes.Name, loggedInUser.name));
                if(loggedInUser.Roles.Count > 0)
                {
                    foreach (var item in loggedInUser.Roles)
                    {
                        identity.AddClaim(new Claim(ClaimTypes.Role, item));
                    }
                }
                context.Validated(identity);
            }
            else
            {
                context.SetError("invalid_grant", "Wrong username or password");
                return;
            }
         
        }
    }


I created a class that mimic getting data from a data store.

public class User
    {
        public string username { get; set; }
        public string password { get; set; }
        public List Roles { get; set; }
        public string name { get;set; }
    }


public class MockUsers
    {
        public List GetUsers()
        {
            var users = new List();

            users.Add(new User
            {
                name= "John Doe",
                username = "admin",
                password = "admin",
                Roles = new List(new string[] { "admin", "superuser" })
            });

            users.Add(new User
            {
                name = "Jane Doe",
                username = "user",
                password = "user",
                Roles = new List(new string[] { "hr", "user" })
            });

            return users;
        }
    }
 


Add a startup class to the project root and add the following

  public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);

            var authProvider = new AuthTokenValidatorService();

            OAuthAuthorizationServerOptions options = new OAuthAuthorizationServerOptions
            {
                AllowInsecureHttp = true,
                TokenEndpointPath = new PathString("/api/auth/token"),
                AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
                Provider = authProvider
            };

            app.UseOAuthAuthorizationServer(options);
            app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
            HttpConfiguration config = new HttpConfiguration();
            WebApiConfig.Register(config);
        }
    }

As you can see in the GetUsers class, I have added two users - admin and user with two different roles for each user. I have specified the route that will handle token generation in the startup class - "/api/auth/token"

We need a way to send back logged in user roles back to our angular app for storage, so create a method in the homecontroller of the web api like so:

   
        [AllowAnonymous]
        [HttpGet]
        [Route("user/roles")]
        public IHttpActionResult GetRoles()
        {
            var identity =(ClaimsIdentity) User.Identity;
            var roles = identity.Claims.Where(x => x.Type == ClaimTypes.Role).Select(x => x.Value).ToList();

            return Ok(new { roles = roles,name = identity.Claims.Where(x => x.Type == ClaimTypes.Name).Select(x => x.Value).First() });
        }

In the getroles actionresult, we are just fetching the logged in user details.

Angular Application

In the angular app where we have most of the work, I have created to component  - loginc omponent and dashboard component. The first thing we need to do is to protect the dashboard component from been accessed by an anonymous user. We will be making use of route guard to achieve this.

There are four different guard types we can use to protect our routes:

  1. CanActivate - Decides if a route can be activated
  2. CanActivateChild - Decides if children routes of a route can be activated
  3. CanDeactivate - Decides if a route can be deactivated
  4. CanLoad - Decides if a module can be loaded lazily


In this case we will be using CanActivate to protect our route.

Let take a look at how the login process in implemented. I created an authservice that host all the login and role fetching like so:

import { Injectable } from '@angular/core';
import { Http, Response, RequestOptions,Headers } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';

@Injectable()
export class AuthService {
     constructor(private http: Http) { }


     signIn(userParams) {
        let bodyObj = `username=${userParams.username}&password=${userParams.password}&grant_type=${userParams.grant_type}`;
         let headers = new Headers({ 'Content-Type': 'application/x-www-form-urlencoded' });
         let options = new RequestOptions({ headers: headers });
  
          return this.http.post("http://localhost:54977/api/auth/token",bodyObj,options)
              .map((res: Response) => res.json());
      }

      getUserRoles(token){
        
        let headers = new Headers({ 'Content-Type': 'application/json' });
        headers.append('Authorization', `Bearer ${token}`);
        let options = new RequestOptions({ headers: headers });

        return this.http.get("http://localhost:54977/api/user/roles", options)
            .map((res: Response) => res.json())
            .catch((error: any) => Observable
                .throw(error.json().error || 'Server error'));
      }
     
}

There nothing complex going on here. There two methods in the service - SignIn and GetUserRoles
The signin method is responsible for logging in and token generation while the getuserroles fetches all roles for the user.

Then I added another service that is responsible for user details storage like so:

import { Injectable } from '@angular/core';

@Injectable()
export class SessionStorageService {

     constructor() { }

     setLoggedInUser(user) {

          window.localStorage.setItem("loggedInUser", JSON.stringify({
               role: user.roles,
               name: user.name
          }));
     }

     getLoggedInUser() {
          if (window.localStorage.getItem("loggedInUser")) {
               return JSON.parse(window.localStorage.getItem("loggedInUser"));
          } else {
               return null;
          }
     }

     logOut(){
          window.localStorage.removeItem("loggedInUser");
     }
}

Now in the login.component.ts here is how signin was done:

 loginUser() {
    let body = {
      username: this.data.username,
      password: this.data.password,
      grant_type:'password'
    };

    this.authSrv.signIn(body).subscribe((res) => {
      
      this.authSrv.getUserRoles(res.access_token).subscribe((user)=>{      
        this.sessionStorage.setLoggedInUser(user);
        this.router.navigate(['/dashboard']);
      });
      
    }, (err) => {
      swal(
        'Oops...',
        err.message + '!',
        'error'
      )
    })


The authguard is now used to protect the dashboard route from been accessed by anonymous user.

const routes: Routes = [
  {
    path: '',
    component: DashboardComponent,
    canActivate: [AuthGuard],
    data: {
      title: 'Dashboard'
    }
  }
];

In order to show or hide menu based on user role, we need to find a way to check logged in user's role against all roles that can access a particular route.

In our FullLayoutComponent component which is our application master page, add the following code:

checkRoles(acceptedArr, incomingArr): boolean {    
    return incomingArr.some(v => acceptedArr.includes(v));
  }

  userInRole(roles: any[]):boolean {
    var userRoles = this.sessionStorage.getLoggedInUser();    
    return this.checkRoles(roles, userRoles.role);
  }


The userInRole method can now be used to show or hide elements in our FullLayoutComponent.html like so:



The *ngIf="userInRole(['admin','superuser','user'])" will check the logged in user roles against those roles that were specified in the array and show or hide menu. This can also be used to hide or show any component in the application.

The full source for both the server and the client can be downloaded here.

Thanks for reading.


No comments:

Powered by Blogger.