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);
}
}
}
}
}
This is really a good filter for authorization! I think I need to write in my website
ReplyDeleteMy husband is a programmer and I guess he will help me to get the things right here and get the result I need.
ReplyDeleteI need some time to understand this post. I don't know the coding, unfortunately.
ReplyDelete