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


Angular 2 Quickstart with Visual Studio 2017 and ASP.NET Core


Here is the quickstart for “Angular 2 quickstart” using Visual Studio 2017. With this you can setup and run Angular 2 quickstart in Visual Studio 2017. Please refer to the official Angular 2 Quickstart. This Visual Studio adaption of the Angular 2 Quickstart can be found on my GitHub site.


Here are the step by step details on how to setup Angular 2 project in Visual Studio.
·         Let’s start by creating a new ASP.NET Core project in Visual Studio

·         Use Empty template to create a blank solution

·         Using Nuget install MVC

·         After installing MVC, install static files middleware. This middleware is needed to serve javascript and html pages

·         Build the project to ensure all the dependencies are downloaded

·         Add MVC services in ConfigureServices method in Startup.cs
    services.AddMvc();

·         After that, in Configure method configure Angular 2 startup and MVC (replace the app.Run code)
// to serve index.html as the default page
app.UseDefaultFiles();
// to serve all the javascript, css and other static pages
app.UseStaticFiles();

// configure route for webapi
app.UseMvc(routes =>
{
    routes.MapRoute(
        name: "default",
        template: "{controller}/{action}/{id?}");
});

·         To download all the Angular dependencies using npm add package.json under wwwroot

·         From Angular 2 Quickstart GitHub copy the package.json. In this blog unit testing is not covered. Hence there is no need to include devDependenies. Here is the complete code for package.json

{
  "name": "angular-quickstart",
  "version": "1.0.0",
  "description": "QuickStart package.json from the documentation, supplemented with testing support",
  "keywords": [],
  "author": "Prasanna Kumar Pattam",
  "license": "MIT",
  "dependencies": {
    "@angular/common": "~2.4.0",
    "@angular/compiler": "~2.4.0",
    "@angular/core": "~2.4.0",
    "@angular/forms": "~2.4.0",
    "@angular/http": "~2.4.0",
    "@angular/platform-browser": "~2.4.0",
    "@angular/platform-browser-dynamic": "~2.4.0",
    "@angular/router": "~3.4.0",

    "angular-in-memory-web-api": "~0.2.2",
    "systemjs": "0.19.40",
    "core-js": "^2.4.1",
    "reflect-metadata": "^0.1.8",
    "rxjs": "5.0.1",
    "zone.js": "^0.7.4"
  },
  "repository": {}
}

·        Wait for the packages to be downloaded
·         From Angular 2 Quickstart GitHub copy the following files/folder to the wwwroot folder
o   index.html
o   systemjs.config.js
o   styles.css
o   favicon.ico
o   tsconfig.json
o   app\main.ts
o   app\app.module.ts
o   app\app.component.ts
·         Once you copy the above files, the solution explorer for wwwroot should look like this

·         Build the solution so that javascript files are generated for the typescript files
·         If you get compile error, delete tsconfig.json files under node_modules (not the one we copied). We don’t need to compile the node_modules
·         Once you successfully compiled, run the application.



Voila our first Angular 2 app running in Visual Studio 2017.