HI WELCOME TO SIRIS

Angular 8 Role Based Authorization with Web API

Leave a Comment
This is the third article towards Angular 5 User Authentication and Authorization with Web API. In this part we’ll discuss Angular 5 Role Based Authorization with Web API. Here is the complete article list.

User Registration.
Login & Logout using Token.
Role Based Authorization.
GitHub link up to this article : https://goo.gl/3zHQB1.

So Far …
In previous articles, we have created user using registration form and implemented login and logout using token based authentication. So let’s check what we got for this part.

– Web API Role Based Authorization.
– Role Based Menu in Angular 5.
– Role Based Routing in Angular 5.

Add Some Roles
First of all I will add few roles manually inside the asp.net identity table – Role. Admin, Author and Reader.

Shows role table with inserted roles

we’ll be showing these roles as a Checkbox list in  Angular 5 user registration form. In-order to work with roles, let’s create a RoleController inside that create GetRoles function to fetch all roles from the roles table.

/Controllers/RoleController.cs
[HttpGet]
[Route("api/GetAllRoles")]
[AllowAnonymous]
public HttpResponseMessage GetRoles()
{
    var roleStore = new RoleStore<IdentityRole>(new ApplicationDbContext());
    var roleMngr = new RoleManager<IdentityRole>(roleStore);

    var roles = roleMngr.Roles
        .Select(x => new { x.Id, x.Name })
        .ToList();
    return this.Request.CreateResponse(HttpStatusCode.OK, roles);
}

Now we have to update registration POST web method – Register inside AccountController. Because we have an additional string array for selected roles.

So first update AccountModel class with Roles array.

/Models/AccountModel.cs
public class AccountModel
{
    public string UserName { get; set; }
    public string Email { get; set; }
    public string Password { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string LoggedOn { get; set; }
    public string[] Roles { get; set; }
}

Now update Register web method inside RegisterController.

/Controllers/AccountController.cs
[Route("api/User/Register")]
[HttpPost]
[AllowAnonymous]
public IdentityResult Register(AccountModel model)
{
    var userStore = new UserStore<ApplicationUser>(new ApplicationDbContext());
    var manager = new UserManager<ApplicationUser>(userStore);
    var user = new ApplicationUser() { UserName = model.UserName, Email = model.Email };
    user.FirstName = model.FirstName;
    user.LastName = model.LastName;
    manager.PasswordValidator = new PasswordValidator
    {
        RequiredLength = 3
    };
    IdentityResult result = manager.Create(user, model.Password);
    manager.AddToRoles(user.Id, model.Roles);
    return result;
}

The specific line from the above web method will insert selected roles for the user in UserRole table. Number of roles selected will be same as the number records inserted inside the table. ie, one record for each role.

Update Angular 5 App for User Registration Form
First add new method in UserService class from shared folder, to fetch all roles in client side.

/src/app/shared/user.service.ts
...
getAllRoles() {
  var reqHeader = new HttpHeaders({ 'No-Auth': 'True' });
  return this.http.get(this.rootUrl + '/api/GetAllRoles', { headers: reqHeader });
}
...

In previous article we discussed use of No-Auth header property, If it is true, we won’t add access token in Authorization header. we set No-Auth because GetAllRoles can be consumed anonymously.

Now we’ll call this function inside SignUpComponent  and save these roles inside roles array.

/src/app/user/sign-up/sign-up.component.ts
...
export class SignUpComponent implements OnInit {
...
roles : any[];

ngOnInit() {
  ...
  this.userService.getAllRoles().subscribe(
    (data : any)=>{
      data.forEach(obj => obj.selected = false);
      this.roles = data;
    }
  );
}
...

Now let’s update corresponding html file. to display checkbox list before submit button with this roles array and unordered list as follows.

/src/app/user/sign-up/sign-up.component.html
...
<div class="row" *ngIf="roles">
  <ul>
    <li *ngFor="let item of roles;let i = index">
      <input type="checkbox" [id]="'roles-'+i" value="{{item.Id}}"
      [checked]="item.selected"  (change)="updateSelectedRoles(i)">
      <label [for]="'roles-'+i">{{item.Name}}</label>
    </li>
  </ul>
</div>
...

there you can see change event for checkbox. it will set or unset role array properties accordingly.
/src/app/user/sign-up/sign-up.component.ts
...
updateSelectedRoles(index) {
  this.roles[index].selected = !this.roles[index].selected;
}
...

And update resetForm function for resetting roles array.
/src/app/user/sign-up/sign-up.component.ts
...
resetForm(form?: NgForm) {
  ...
  if (this.roles)
    this.roles.map(x => x.selected = false);
}
...

Since we have an additional string array, we have to make few changes in previous form submission procedure.

Update OnSubmit function inside UserComponent.

/src/app/user/sign-up/sign-up.component.ts
...
OnSubmit(form: NgForm) {
  var x = this.roles.filter(x => x.selected).map(y => y.Name);
  this.userService.registerUser(form.value,x)
    .subscribe((data: any) => {
    ...
    });
}
...

Now the registerUser method inside UserService class.

/src/app/shared/user.service.ts

registerUser(user: User,roles : string[]) {
  const body = {
    UserName: user.UserName,
    Password: user.Password,
    Email: user.Email,
    FirstName: user.FirstName,
    LastName: user.LastName,
    Roles : roles
  }
  var reqHeader = new HttpHeaders({'No-Auth':'True'});
  return this.http.post(this.rootUrl + '/api/User/Register', body,{headers : reqHeader});
}

Now we have a working updated user registration form.
Implement Role Based Authorization
Now let’s implement role based authorization in Web API and then in client side.

For that First of all, we have to store roles assigned to a user in Claims during authentication or login, Authentication is done inside token based authentication function GrantResourceOwnerCredentials in ApplicationOAuthProvider.cs file.

/ApplicationOAuthProvider.cs
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
    var userStore = new UserStore<ApplicationUser>(new ApplicationDbContext());
    var manager = new UserManager<ApplicationUser>(userStore);
    var user = await manager.FindAsync(context.UserName,context.Password);
    if (user != null) {
        var identity = new ClaimsIdentity(context.Options.AuthenticationType);
        identity.AddClaim(new Claim("Username", user.UserName));
        identity.AddClaim(new Claim("Email", user.Email));
        identity.AddClaim(new Claim("FirstName", user.FirstName));
        identity.AddClaim(new Claim("LastName", user.LastName));
        identity.AddClaim(new Claim("LoggedOn", DateTime.Now.ToString()));
        var userRoles = manager.GetRoles(user.Id);
        foreach (string roleName in userRoles)
        {
            identity.AddClaim(new Claim(ClaimTypes.Role, roleName));
        }
        //return data to client
        var additionalData = new AuthenticationProperties(new Dictionary<string, string>{
            {
                "role", Newtonsoft.Json.JsonConvert.SerializeObject(userRoles)
            }
        });
        var token = new AuthenticationTicket(identity, additionalData);
        context.Validated(token);
    }
    else
        return;
}

we have stored roles assigned to the user inside userRoles variable and with a for-each loop, each of those roles are added to ClaimTypes.Role.
As part of the token request response we have to return assigned roles also. for that we have created an object of AuthenticationProperties  with the roles in JSON format. In-order to combine this additional data we have used AuthenticationTicket. Finally we returns the token.

In-order to receive this token in client side with proper format we have to override one more function as follows

/ApplicationOAuthProvider.cs
public override Task TokenEndpoint(OAuthTokenEndpointContext context)
{
    foreach (KeyValuePair<string, string> property in context.Properties.Dictionary)
    {
        context.AdditionalResponseParameters.Add(property.Key, property.Value);
    }

    return Task.FromResult<object>(null);
}

Let’s Test the AuthorizationIn-order to test the implemented role based authorization 3 additional HttpGet Web Methods are added inside Account Controller.

/Controllers/AccountController.cs
...

[HttpGet]
[Authorize(Roles = "Admin")]
[Route("api/ForAdminRole")]
public string ForAdminRole()
{
    return "for admin role";
}

[HttpGet]
[Authorize(Roles = "Author")]
[Route("api/ForAuthorRole")]
public string ForAuthorRole()
{
    return "For author role";
}

[HttpGet]
[Authorize(Roles = "Author,Reader")]
[Route("api/ForAuthorOrReader")]
public string ForAuthorOrReader()
{
    return "For author/reader role";
}


...

First and second methods can only be accessed by users with Admin and Author roles respectively. But last method can be accessed with either Author or Reader role.
If users doesn’t have the sufficient role(s), Web API project must return 403- Forbidden Status Code, But by default – it returns 401-Unauthorized status code. To override this behavior, we can create new c# class file – AuthorizeAttribute.cs as follows.

/AuthorizeAttribute.cs
public class AuthorizeAttribute : System.Web.Http.AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(System.Web.Http.Controllers.HttpActionContext actionContext)
    {
        if (!HttpContext.Current.User.Identity.IsAuthenticated)
        {
            base.HandleUnauthorizedRequest(actionContext);
        }
        else
        {
            actionContext.Response = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.Forbidden);
        }
    }
}

Role Based Authorization in Client SideFirst of we have to store user role in local storage, for that I’ll update login form submit event.

/src/app/user/sign-in/sign-in.component.ts
...

OnSubmit(userName,password){
     this.userService.userAuthentication(userName,password).subscribe((data : any)=>{
      localStorage.setItem('userToken',data.access_token);
      localStorage.setItem('userRoles',data.role);
      this.router.navigate(['/home']);
    },
    (err : HttpErrorResponse)=>{
      this.isLoginError = true;
    });
  }

As part of the token request we will get roles assigned to a particular user. here we save the roles inside userRoles key in local Storage.
Role Based Menu
Let me create a new-empty component admin-panel. it can only be accessed for users with admin role. now let’s add a route for this component as follows.

/src/app/routes.ts
...

export const appRoutes: Routes = [
    { path: 'home', component: HomeComponent,canActivate:[AuthGuard] },
    { path: 'adminPanel', component: AdminPanelComponent, canActivate: [AuthGuard] , data: { roles: ['Admin'] }},
    ...
];

route path will be adminPanel,  here we have an additional  data property that we discuss later. Now let’s add a menu item for this component in home component html file.
/src/app/home/home.component.html
...

<ul id="nav-mobile" class="right hide-on-med-and-down">
  <li  *ngIf="userService.roleMatch(['Admin'])">
    <a routerLink="/adminPanel">Admin Panel</a>
  </li>
  <li>
    <a (click)="Logout()">Logout</a>
  </li>
</ul>

...

For the new li element we have ngIf directive. Which conditionally hide/show the li element. Now let’s define the UserService class function roleMatch.
/src/app/shared/user.service.ts
...
roleMatch(allowedRoles): boolean {
  var isMatch = false;
  var userRoles: string[] = JSON.parse(localStorage.getItem('userRoles'));
  allowedRoles.forEach(element => {
    if (userRoles.indexOf(element) > -1) {
      isMatch = true;
      return false;
    }
  });
  return isMatch;
}
...

inside this function we will match require array of roles with array of roles assigned to the user. if there is any match then it will return true otherwise false. Above ngIf directive passed an array containing admin role. Now we have role based menu in angular 5 project !.
Role Based Routing in Angular 5
As part of this role based routing we have already provided data  property in admin-panel component route with required role admin. Now we have to update existing AuthGuard for role based authorization in routing.

/src/app/auth/auth.gaurd.ts
...
canActivate(
  next: ActivatedRouteSnapshot,
  state: RouterStateSnapshot):  boolean {
    if (localStorage.getItem('userToken') != null)
    {
      let roles = next.data["roles"] as Array<string>;
      if (roles) {
        var match = this.userService.roleMatch(roles);
        if (match) return true;
        else {
          this.router.navigate(['/forbidden']);
          return false;
        }
      }
      else
        return true;
    }
    this.router.navigate(['/login']);
    return false;
}
...

First of all we retrieve roles array passed through routing parameter data. Compare it with user roles if no match found we will navigate to forbidden path.
we have to create this component forbidden. component html will be like this.

/src/app/forbidden/forbidden.component.html
<div class="row">
    <div class="col s6 offset-s3">
      <div class="card">
        <div class="card-image">
          <img src="/assets/img/403.png" style="height:300px;width:300px;">

        </div>
        <div class="card-content">
          <span class="card-title" style="color:#039be5">403 - ACCESS DENIED</span>
          <p>You don't have the permission to access this resource.</p>
        </div>
      </div>
    </div>
  </div>

for this component we have added an additional image inside /assets/img/403.png. Final component will look like this.


now let’s add a route for this component as follows.

 { path: 'forbidden', component: ForbiddenComponent, canActivate: [AuthGuard] }

Done.

0 comments:

Post a Comment

Note: only a member of this blog may post a comment.