How can I implement Claims-Based Authorization with ASP.NET WebAPI without using Roles?
Asked Answered
P

1

28

I have an ASP.Net WebAPI 2 Application that uses Claims. The claims are stored as two additional columns in a standard Identity2 AspNetUsers table:

CREATE TABLE [dbo].[AspNetUsers] (
    [Id]                   INT            IDENTITY (1, 1) NOT NULL,
    ....
    [SubjectId]            INT            DEFAULT ((0)) NOT NULL,
    [LocationId]           INT            DEFAULT ((0)) NOT NULL,
    CONSTRAINT [PK_dbo.AspNetUsers] PRIMARY KEY CLUSTERED ([Id] ASC)
);

I have modified the ApplicationUser class like this:

public class ApplicationUser : IdentityUser<int, CustomUserLogin, CustomUserRole, CustomUserClaim>
    {
        public async Task<ClaimsIdentity> GenerateUserIdentityAsync(ApplicationUserManager manager, string authenticationType)
        {
            // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
            ClaimsIdentity userIdentity = await manager.CreateIdentityAsync(this, authenticationType);
            // Add custom user claims here
            userIdentity.AddClaim(new Claim("SubjectId", this.SubjectId.ToString()));
            userIdentity.AddClaim(new Claim("LocationId", this.LocationId.ToString()));
            return userIdentity;
        }

        public int SubjectId { get; set; }
        public int LocationId { get; set; }

    }

In my register method I add in new data for the SubjectId:

    var user = new ApplicationUser() { 
        UserName = model.UserName, 
        SubjectId = 25,
        LocationId = 4
    };

    IdentityResult result = await UserManager.CreateAsync(user, model.Password);

Can someone help tell me how I can now go about restricting access to a controller based on this SubjectId at the controller level and also at the method level with something similar to this:

[Authorize(SubjectId = "1,25,26")]
[RoutePrefix("api/Content")]
public class ContentController : BaseController
{

    [Authorize(LocationId = "4")]
    [Route("Get")]
    public IQueryable<Content> Get()
    {
        return db.Contents;
    }

    [Authorize(SubjectId = "25")]
    [Route("Get/{id:int}")]
    public async Task<IHttpActionResult> Get(int id)
    {
        Content content = await db.Contents.FindAsync(id);
        if (content == null)
        {
            return NotFound();
        }
        return Ok(content);
    }

For months now I have been looking for an example but other than some reference to ThinkTexture product and the following link I have found nothing

Update:

#region Assembly System.Web.Http.dll, v5.2.2.0
// C:\Users\Richard\GitHub\abilitest-server\packages\Microsoft.AspNet.WebApi.Core.5.2.2\lib\net45\System.Web.Http.dll
#endregion

using System;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;

namespace System.Web.Http
{
    // Summary:
    //     Specifies the authorization filter that verifies the request's System.Security.Principal.IPrincipal.
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
    public class AuthorizeAttribute : AuthorizationFilterAttribute
    {
        // Summary:
        //     Initializes a new instance of the System.Web.Http.AuthorizeAttribute class.
        public AuthorizeAttribute();

        // Summary:
        //     Gets or sets the authorized roles.
        //
        // Returns:
        //     The roles string.
        public string Roles { get; set; }
        //
        // Summary:
        //     Gets a unique identifier for this attribute.
        //
        // Returns:
        //     A unique identifier for this attribute.
        public override object TypeId { get; }
        //
        // Summary:
        //     Gets or sets the authorized users.
        //
        // Returns:
        //     The users string.
        public string Users { get; set; }

        // Summary:
        //     Processes requests that fail authorization.
        //
        // Parameters:
        //   actionContext:
        //     The context.
        protected virtual void HandleUnauthorizedRequest(HttpActionContext actionContext);
        //
        // Summary:
        //     Indicates whether the specified control is authorized.
        //
        // Parameters:
        //   actionContext:
        //     The context.
        //
        // Returns:
        //     true if the control is authorized; otherwise, false.
        protected virtual bool IsAuthorized(HttpActionContext actionContext);
        //
        // Summary:
        //     Calls when an action is being authorized.
        //
        // Parameters:
        //   actionContext:
        //     The context.
        //
        // Exceptions:
        //   System.ArgumentNullException:
        //     The context parameter is null.
        public override void OnAuthorization(HttpActionContext actionContext);
    }
}
Penult answered 7/11, 2014 at 16:47 Comment(0)
W
38

You can achieve that if you override the Authorize attribute. In your case it should be something like this:

public class ClaimsAuthorize : AuthorizeAttribute
{
    public string SubjectID { get; set; }
    public string LocationID { get; set; }

    protected override bool IsAuthorized(HttpActionContext actionContext)
    {
        ClaimsIdentity claimsIdentity;
        var httpContext = HttpContext.Current;
        if (!(httpContext.User.Identity is ClaimsIdentity))
        {
            return false;
        }      

        claimsIdentity = httpContext.User.Identity as ClaimsIdentity;
        var subIdClaims = claimsIdentity.FindFirst("SubjectId");
        var locIdClaims = claimsIdentity.FindFirst("LocationId");
        if (subIdClaims == null || locIdClaims == null)
        {
            // just extra defense
            return false;
        }

        var userSubId = subIdClaims.Value;
        var userLocId = subIdClaims.Value;

        // use your desired logic on 'userSubId' and `userLocId', maybe Contains if I get your example right?
        if (!this.SubjectID.Contains(userSubId) || !this.LocationID.Contains(userLocId))
        {
            return false;
        }

        //Continue with the regular Authorize check
        return base.IsAuthorized(actionContext);
    } 
}

In your controller that you wish to restrict access to, use the ClaimsAuthorize attribute instead of the normal Authorize one:

[ClaimsAuthorize(
    SubjectID = "1,2",
    LocationID = "5,6,7")]
[RoutePrefix("api/Content")]
public class ContentController : BaseController
{
     ....
}
Worley answered 13/12, 2014 at 16:35 Comment(16)
Thanks very much for your answer. Can you give me some advice as to how I could manage to have the alternate and two levels of authorization. In my question I mention about two types of claim. SubjectId and also LocationId claims. I see how you suggest overriding but I am not sure how I could do this if I had two different types of ClaimsAuthorize.Penult
@marifemac You can still do it with one attribute, give me a couple of minutes I'll edit the answer.Worley
@marifemac Edited, does it answer your question better?Worley
I am having a problem as there's no AuthorizeCore in the AuthorizeAttribute.Penult
Are you overriding System.Web.Mvc.AuthorizeAttribute ?Worley
No it's a WebAPI application. I updated the question to show the AuthorizeAttribute classPenult
@marifemac My bad, don't know why I used the MVC one when it's obviously Web API. Updated to the correct one.Worley
I noticed on the web some others using OnAuthorization. Do you know if that is different from IsAuthorized?Penult
@marifemac Yes, but look at the first answer (12 votes and not the accepted one) which will explain it better than me in one comment #12630030Worley
@OmriAharon I suggest you edit your answer not to take ClaimsPrincipal from the HttpContext.Current. It is not always guarantied to be present as it depends on the way your web api is hosted (WebHost/self host). The safest bet is actionContext.RequestContext.Principal or ClaimsPrincipal.Current.Alburga
@Alburga You suggest to replace HttpContext.Current.User.Identity with ClaimsPrincipal.Current.Identity then ?Worley
@OmriAharon both will always work but I prefer var user = actionContext.RequestContext.Principal as ClaimsPrincipal then if not null just get the identity user.IdentityAlburga
@Alburga Can't seem to find RequestContext property under actionContext.Worley
@OmriAharon should be there according to msdnAlburga
@OmriAharon I get these errors in VS: "ClaimsAuthorize.IsAuthorized(HttpActionContext): no suitable method found to override" and "HttpContext does not contain a definition for Current"Hypogynous
@JohnPankowicz Make sure you're overriding System.Web.Http.AuthorizeAttribute as there's also System.Web.Mvc.AuthorizeAttribute.Worley

© 2022 - 2024 — McMap. All rights reserved.