Monday, December 26, 2016

Filters - Multi-Tenant Claim Based Identity for ASP.NET Core - Part 5 of 10



This part 5 of 10 part series which outlines my implementation of Multi-Tenant Claim Based Identity. For more details please see my index post.


I am using MVC Authorization Filter to authorize user access to the controller action. If the user is not allowed then I am returning 403 error response and the action is not invoked. Here are the sequence of checks to authorize the authenticated user. If any of these checks then I am returning 403 response.
  • Validate that the controller and action exists
  • Certain actions can be marked for Anonymous access, in that case let the user access the page
  • Check whether the user has access to the company he is requesting
  • Check if the user is an admin so that he will have unrestricted access to the all the claims in that module
  • Check if user has any denial claims. If the current claim is denied then return 403
  • Finally check if the user has the current claim

Here are the code snippets for each of the above check/validations.

Checking that the controller and action exists in our database scheme:
    var page = PageService.Pages.Where(c => string.Compare(c.Controller, controller, true) == 0 && string.Compare(c.ActionMethod, action, true) == 0).FirstOrDefault();
    if (page == null)
    {
        context.Result = new StatusCodeResult(403);
        return;
    }

Verify whether anonymous action is allowed for this page. If yes, then bypass authorization
    // checking for annonymous claim
    if (page.PageClaims.Any(p => p.ClaimType == SecuritySettings.AnonymouseClaimType && p.ClaimValue == SecuritySettings.AnonymousClaim))
    {
        return;
    }

Get all the claims for the current user
    var userClaims = context.HttpContext.User.Claims;

Check whether the user has permissions for the company (tenant) he is trying to access:
    // checking the companyid passed in headers
    string companies = userClaims.Where(c => c.Type == NTClaimTypes.Companies).Select(c => c.Value).FirstOrDefault();
    string companyId = context.HttpContext.Request.Headers[SecurityConstants.HeaderCompanyId];
    companyId = companyId ?? userClaims.Where(c => c.Type == NTClaimTypes.CompanyId).Select(c => c.Value).FirstOrDefault();

    if (companies == null || companyId == null || !companies.Split(',').Contains(companyId))
    {
        context.Result = new StatusCodeResult(403);
        return;
    }

Checking whether user is an admin user using his roles
    // getting current roles and then get all the child roles
    string[] roles = userClaims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value).ToArray();
    roles = PageService.AdminRoles.Where(r => roles.Contains(r.Key)).Select(r => r.Item).ToArray();

    // checking whether user is an admin
    if (!roles.Any(r => page.PageClaims.Any(p => r == p.ClaimType + SecuritySettings.AdminSuffix)))
    {
        // additional checks
    }

Checking for denial claims
    // checking for deny claim
    if (userClaims.Any(c => page.PageClaims.Any(p => c.Type == p.ClaimType + SecuritySettings.DenySuffix && c.Value == p.ClaimValue)))
    {
        context.Result = new StatusCodeResult(403);  // new HttpUnauthorizedResult();
    }

Finally checking whether user has claims for the current page:
    // checking for current claim
    if (!userClaims.Any(c => page.PageClaims.Any(p => c.Type == p.ClaimType && c.Value == p.ClaimValue)))
    {
        context.Result = new StatusCodeResult(403);
    }

With the above checks we can ensure that user is authorized to access the current page.
Here is the full code for this filter
//-------------------------------------------------------------------------------------------------
// <copyright file="NTAuthorizeFilter.cs" company="Nootus">
//  Copyright (c) Nootus. All rights reserved.
// </copyright>
// <description>
//  MVC filter to authorize user for a page using claims
// </description>
//-------------------------------------------------------------------------------------------------
namespace MegaMine.Services.Security.Filters
{
    using System;
    using System.Linq;
    using System.Security.Claims;
    using MegaMine.Core.Context;
    using MegaMine.Services.Security.Common;
    using MegaMine.Services.Security.Identity;
    using MegaMine.Services.Security.Middleware;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.Mvc.Filters;

    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
    public class NTAuthorizeFilter : Attribute, IAuthorizationFilter
    {
        public void OnAuthorization(AuthorizationFilterContext context)
        {
            // getting the current module and claim
            string action = context.RouteData.Values["action"].ToString().ToLower();
            string controller = context.RouteData.Values["controller"].ToString().ToLower() + "controller";

            var page = PageService.Pages.Where(c => string.Compare(c.Controller, controller, true) == 0 && string.Compare(c.ActionMethod, action, true) == 0).FirstOrDefault();

            if (page == null)
            {
                context.Result = new StatusCodeResult(403);
                return;
            }


            // checking for annonymous claim
            if (page.PageClaims.Any(p => p.ClaimType == SecuritySettings.AnonymouseClaimType && p.ClaimValue == SecuritySettings.AnonymousClaim))
            {
                return;
            }

            var userClaims = context.HttpContext.User.Claims;

            // checking the companyid passed in headers
            string companies = userClaims.Where(c => c.Type == NTClaimTypes.Companies).Select(c => c.Value).FirstOrDefault();
            string companyId = context.HttpContext.Request.Headers[SecurityConstants.HeaderCompanyId];
            companyId = companyId ?? userClaims.Where(c => c.Type == NTClaimTypes.CompanyId).Select(c => c.Value).FirstOrDefault();

            if (companies == null || companyId == null || !companies.Split(',').Contains(companyId))
            {
                context.Result = new StatusCodeResult(403);
                return;
            }

            // checking for annonymous claim for each module
            if (page.PageClaims.Any(p => p.ClaimValue == SecuritySettings.AnonymousClaim))
            {
                return;
            }

            // getting current roles and then get all the child roles
            string[] roles = userClaims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value).ToArray();
            roles = PageService.AdminRoles.Where(r => roles.Contains(r.Key)).Select(r => r.Item).ToArray();

            // checking whether user is an admin
            if (!roles.Any(r => page.PageClaims.Any(p => r == p.ClaimType + SecuritySettings.AdminSuffix)))
            {
                // checking for deny claim
                if (userClaims.Any(c => page.PageClaims.Any(p => c.Type == p.ClaimType + SecuritySettings.DenySuffix && c.Value == p.ClaimValue)))
                {
                    context.Result = new StatusCodeResult(403);  // new HttpUnauthorizedResult();
                }

                // checking for current claim
                else if (!userClaims.Any(c => page.PageClaims.Any(p => c.Type == p.ClaimType && c.Value == p.ClaimValue)))
                {
                    context.Result = new StatusCodeResult(403);
                }
            }
        }
    }
}


2 comments:

  1. This is really a good filter for authorization! I think I need to write in my website

    ReplyDelete
  2. My husband is a programmer and I guess he will help me to get the things right here and get the result I need.

    ReplyDelete