Sunday, April 2, 2017

Storing Identity Claims in Session or Redis


ASP.NET Core Identity by default uses cookies to store claims. If your authorization is very granular then you will end up using many claims. With all these claims stored in cookie, the cookie size gets bigger and you can exceed the cookie limit.

We can achieve the authorization granularity without exceeding the cookie limit and impacting the way ASP.NET Identity authorizes users by storing the claims in session, Redis or any other memory storage.

Below are the steps I implemented to store the claims in session.

First let’s store the claims in session (assuming that the ASP.NET Core Session is already configured). During authentication, Identity system uses SignInManager for creating principal object of the logged in user. While creating the principal object, Identity populates the claims. We can override this SignInManager and store the claims as shown below:

public class ApplicationSignInManager : SignInManager<ApplicationUser>
{
    private IHttpContextAccessor contextAccessor;

    public ApplicationSignInManager(UserManager<ApplicationUser> userManager, IHttpContextAccessor contextAccessor, IUserClaimsPrincipalFactory<ApplicationUser> claimsFactory, IOptions<IdentityOptions> optionsAccessor, ILogger<SignInManager<ApplicationUser>> logger)
        : base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger)
    {
        this.contextAccessor = contextAccessor;
    }

    public override async Task<ClaimsPrincipal> CreateUserPrincipalAsync(ApplicationUser user)
    {
        var principal = await base.CreateUserPrincipalAsync(user);
        ClaimsIdentity identity = (ClaimsIdentity)principal.Identity;

        // storing claims in session and removing them. These claims will be added by Transformer
        List<ClaimModel> sessionClaims = new List<ClaimModel>();
        List<Claim> identityClaims = identity.Claims.ToList();
        foreach (var claim in identityClaims)
        {
            sessionClaims.Add(new ClaimModel() { ClaimType = claim.Type, ClaimValue = claim.Value });
            identity.RemoveClaim(claim);
        }

        this.contextAccessor.HttpContext.Session.SetString("IdentityClaims", JsonConvert.SerializeObject(sessionClaims));

        return principal;
    }
}

The Identity system should be configured to use our ApplicationSignInManager instead of the default one. For this we need to define the dependency injection in the Startup under ConfigureServices

services.AddScoped<SignInManager<ApplicationUser>, ApplicationSignInManager>();

As you see with above code claims are removed from the principal identity and stored in the session. These claims should to be added back to the principal identity for every request. This done through the claims transformer as shown below:

public class ClaimsTransformer : IClaimsTransformer
{
    public Task<ClaimsPrincipal> TransformAsync(ClaimsTransformationContext context)
    {
        ClaimsPrincipal principal = context.Principal;
        ClaimsIdentity identity = (ClaimsIdentity)principal.Identity;
        string claimString = NTContext.HttpContext.Session.GetString("IdentityClaims");
        if (claimString != null)
        {
            List<ClaimModel> sessionClaims = JsonConvert.DeserializeObject<List<ClaimModel>>(claimString);
            identity.AddClaims(sessionClaims.Select(sc => new Claim(sc.ClaimType, sc.ClaimValue)));
        }

        return Task.FromResult(principal);
    }
}

This ClaimsTransformer class is configured in the Startup under Configure section as shown below:

app.UseClaimsTransformation(new ClaimsTransformationOptions()
{
    Transformer = new ClaimsTransformer(),
});

app.UseIdentity();


With the above setup now the claims are stored in session when the user logs in. At every request from the user these claims will be copied from session to the identity object. Now we have overridden the default approach of using cookies with session J

Thursday, March 30, 2017

Inject AngularJS services into Angular 2


When you are running AngularJS and Angular side by side, then you may need to inject AngularJS default services such as $rootScope, $timeout, $state (UI-Router) etc.

In order to use the AngularJS default services, you need to define a provider. The upgrade module already defined a provider for $rootScope, hence you can use it directly as outlined in my other blog.

Let’s see how to inject the UI-Router $state into Angular. Upgrade Module internally stores the Angular JS injector. In our Angular 2 module, we can define a provider which utilizes the Angular JS injector to return the reference.

So first define a provider in Angular 2 as shown below

@NgModule({
    imports: [ ... ],
    declarations: [ ... ],
    providers: [
        { provide: '$state', useFactory: (i) => i.get('$state'), deps: ['$injector'] }
    ]
})

After defining the provider in the module, you can now inject the dependencies in your angular 2 components and services as shown below:

constructor(@Inject("$rootScope") private $rootScope: ng.IRootScopeService, @Inject("$state") private $state: ng.ui.IStateService) {
        // UI-Router change events

}


Inject $rootScope into Angular 2


If you have AngularJS and Angular running side by side in the same application by using Upgrade module, then you may need access $rootScope in Angular 2 part of the application.

The upgrade module already defined a provider for $rootScope, hence you directly inject $rootScope as below

@Inject("$rootScope") private $rootScope: ng.IRootScopeService

Here is the constructor DI code:

constructor(@Inject("$rootScope") private $rootScope: ng.IRootScopeService, @Inject("$state") private $state: ng.ui.IStateService) {
        // UI-Router change events

}

To inject $state and other AngularJS services please refer to my other blog

Sunday, January 29, 2017

Angular 2 UI-Grid



As you know currently (Jan 2017) UI-Grid is not supported in Angular 2 and there is no other data table that provides the functionality of what UI-Grid is providing. PrimeNG does provide a decent grid, ag-grid became commercial and Material 2 data table is still in specs. Considering the current state of other grids I opted to use UI-Grid in my next application. Yes, I need to take the overhead of loading Angular 1, but felt it is worth it. Also some of my applications are running on Angular 1 and Angular 2 side by side, hence I can use UI Grid without too much performance overhead.

To support UI-Grid in Angular 2, we need to use the Upgrade Module which enables us to run Angular 1 and Angular 2 side by side. Once the project is setup to have Angular 1, we need to write a wrapper that wraps the UI-Grid and provides a way to invoke this from Angular 2. Here is such wrapper, I am calling this as NT-Grid (NT stands for Nootus, the company I own)

export const ntGrid: ng.IComponentOptions = {
    bindings: {
        gridOptions: "<"
    },
    template: `<div ui-grid="$ctrl.gridOptions"></div>`
}

angular.module("ng2uigrid").component("ntGrid", ntGrid);

import { Directive, ElementRef, Injector, Input, Output, EventEmitter } from "@angular/core";
import { UpgradeComponent } from "@angular/upgrade/static";

@Directive({
    selector: "nt-grid"
})
export class NtGridDirective extends UpgradeComponent {
    @Input() gridOptions: any;

    constructor(elementRef: ElementRef, injector: Injector) {
        super("ntGrid", elementRef, injector);
    }
}

Here is the screenshot of UI grid in my Angular 2 POC.

This POC is on GitHub


Monday, January 23, 2017

Startup- Multi-Tenant Claim Based Identity for ASP.NET Core - Part 6 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.


As you know Startup is used for configure the request pipeline that handles all requests made to the application. I am using this Startup to configure the following:
  •  Configure Identity Framework
  • Dependency Injection of Identity classes and my classes
  • Automapper configuration (optional)

Here are the different sections of Startup. I have included only relevant code.

Startup Constructor
This is standard code. In this I am saving the connection string in a global variable so that I can later use it.
public Startup(IHostingEnvironment env)
{
    // Set up configuration sources.
    var builder = new ConfigurationBuilder()
        .SetBasePath(env.ContentRootPath)
        .AddJsonFile("appsettings.json")
        .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);

    builder.AddEnvironmentVariables();
    this.Configuration = builder.Build();

    // saving the site connection string
    SiteSettings.ConnectionString = this.Configuration.GetConnectionString("MegaMine");
}

ConfigureServices
In this method I am configuring the following services
  • Configuring Entity Framework
  •  Configure ASP.NET Identity system
  • Configure MVC
  • Configure our Authorization filter
  • Dependency injection configuration for classes related to security
  • Automapper configuration for classes related to security

Here is the relevant code in this method
public void ConfigureServices(IServiceCollection services)
{
    // configuring Entity Framework
    services.AddEntityFrameworkSqlServer()
    .AddDbContext<SecurityDbContext>(options =>
    {
        options.UseSqlServer(SiteSettings.ConnectionString);
    });
   
    // Configuring ASP.NET Identity
    services.AddIdentity<ApplicationUser, ApplicationRole>()
    .AddEntityFrameworkStores<SecurityDbContext>()
    .AddDefaultTokenProviders();

    services.AddMvc()
    .AddMvcOptions(options =>
    {
        options.Filters.Add(new NTAuthorizeFilter()); // Adding our Authorization filter
    })
    .AddJsonOptions(options =>
    {
        options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
        options.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Local;
        options.SerializerSettings.DateFormatHandling = DateFormatHandling.IsoDateFormat;
    });

    // dependency injection
    services.AddScoped<SignInManager<ApplicationUser>, ApplicationSignInManager>();
    services.AddTransient<AccountDomain>();
    services.AddTransient<SecurityRepository>();

    // Automapper configurations
    Mapper.Initialize(x =>
    {
        x.AddProfile<SecurityMappingProfile>();
    });
}

Configure

In this Configure method, apart from standard request processing configuration, I added the following configurations:
  • Enable ASP.NET Identity
  • Enable my own middleware which stores User information in a custom request context

Here is the code for this configure method

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    // Middleware for storing context for the request
    app.UseContextMiddleware();

    // To enable deliverying static content
    app.UseStaticFiles();

    // Enabling ASP.NET Identity
    app.UseIdentity();

    // Custom middleware to store user details
    app.UseProfileMiddleware();

    // Enabling MVC
    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "webapi",
            template: "api/{controller}/{action}/{id?}",
            defaults: new { controller = "Home", action = "Index" });

        routes.MapRoute(
            name: "error",
            template: "Error",
            defaults: new { controller = "Home", action = "Error" });

        routes.MapRoute(
            name: "default",
            template: "{controller}/{action}",
            defaults: new { controller = "Home", action = "Index" });
    });
}


With the above Configurations in the Startup class, the application is configure to utilize Multi-Tenant Claim Based Identity.

Thursday, January 5, 2017

Upgrading Angular 1 to Angular 2


I have an open source medium size application (about 100 pages) which is written in Angular 1.x. I used javascript for developing this application. In addition I am using UI Router, UI Grid and Material for developing pages in this application. Now I am upgrading this application to Angular 2. This blog is my experience in upgrading this application to Angular 2.
From the beginning of this application, I was following most of the patterns recommended in the style guide (when I started this application, there is no recommended style guide). As per Angular upgrade docs if we follow the recommended style guide then it should easy to upgrade. I hope my upgrade will be easy.
Here are the 7 steps I followed for this migration.
  1. Preparation
  2. Switching JavaScript to TypeScript
  3. Bootstrapping with UpgradeModule
  4. Wrap Angular 1 Modules
  5. Upgrading Components and Services
  6. Routing Configuration
  7. Goodbye Angular 1


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