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);
                }
            }
        }
    }
}


3 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
  3. The free roblox hack is used for the free resources and sources hack for the free roblox free robux game to play free then use my link for the free roblox robux game to play free.

    ReplyDelete