24

I have many controllers with many actions. Each action has it's own Role ( Role name = ControllerName.actionName ).

In previous versions I could test if the current user can acces an action or not using a "generic" AuthorizeAttribute :

public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext actionContext)
{
    string currentAction = actionContext.ActionDescriptor.ActionName;
    string currentController = actionContext.ActionDescriptor.ControllerDescriptor.ControllerName;
    Roles = (currentController + "." + currentAction).ToLower();
    base.OnAuthorization(actionContext);
}

With the version asp.net 5, I've found that I need to use requirements ( How do you create a custom AuthorizeAttribute in ASP.NET Core? ). The problem is that the AuthorizationContext does not give us the information about the action that the user is trying to get to.

I don't want to put an Authorize attribute on each action, is there any way to achieve my requirement with the new framework ? ( I prefer to avoid using HttpContext.Current, it doesn't fit well in a pipeline architecture )

Community
  • 1
  • 1
yeska
  • 569
  • 1
  • 6
  • 20

1 Answers1

54

Here is the general process for enforcing custom authentication. Your situation may be able to solved completely in step one, since you could add a Claim for the Role that decorates your

1. authenticate by creating an identity for the user

Writing middleware and inserting it into the pipeline via IApplicationBuilder.UseMiddleware<> is how custom authentication is done. This is where we extract whatever info may be later needed for authorization, and put it into an ClaimsIdentity. We have an HttpContext here so we can grab info from the header, cookies, requested path, etc. Here is an example:

public class MyAuthHandler : AuthenticationHandler<MyAuthOptions>
{
   protected override Task<AuthenticationTicket> HandleAuthenticateAsync()
   {
      // grab stuff from the HttpContext
      string authHeader = Request.Headers["Authorization"] ?? "";
      string path = Request.Path.ToString() ?? "";

      // make a MyAuth identity with claims specifying what we'll validate against
      var identity = new ClaimsIdentity(new[] {
         new Claim(ClaimTypes.Authentication, authHeader),
         new Claim(ClaimTypes.Uri, path)
      }, Options.AuthenticationScheme);

      var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), 
         new AuthenticationProperties(), Options.AuthenticationScheme);
      return Task.FromResult(ticket);
   }
}

public class MyAuthOptions : AuthenticationOptions
{
   public const string Scheme = "MyAuth";
   public MyAuthOptions()
   {
      AuthenticationScheme = Scheme;
      AutomaticAuthentication = true;
   }
}

public class MyAuthMiddleware : AuthenticationMiddleware<MyAuthOptions>
{
   public MyAuthMiddleware(
               RequestDelegate next,
               IDataProtectionProvider dataProtectionProvider,
               ILoggerFactory loggerFactory,
               IUrlEncoder urlEncoder,
               IOptions<MyAuthOptions> options,
               ConfigureOptions<MyAuthOptions> configureOptions)
         : base(next, options, loggerFactory, urlEncoder, configureOptions)
   {
   }

   protected override AuthenticationHandler<MyAuthOptions> CreateHandler()
   {
      return new MyAuthHandler();
   }
}

public static class MyAuthMiddlewareAppBuilderExtensions
{
   public static IApplicationBuilder UseMyAuthAuthentication(this IApplicationBuilder app, string optionsName = "")
   {
      return app.UseMiddleware<MyAuthMiddleware>(
         new ConfigureOptions<MyAuthOptions>(o => new MyAuthOptions()) { Name = optionsName });
   }
}

To use this middleware insert this in Startup.Configure prior to the routing: app.UseMyAuthAuthentication();

2. authorize by enforcing requirements on the identity

We've created an identity for the user but we still need to enforce it. To do this we need to write an AuthorizationHandler like this:

  public class MyAuthRequirement : AuthorizationHandler<MyAuthRequirement>, IAuthorizationRequirement
  {
     public override void Handle(AuthorizationContext context, MyAuthRequirement requirement)
     {
        // grab the identity for the MyAuth authentication
        var myAuthIdentities = context.User.Identities
           .Where(x => x.AuthenticationType == MyAuthOptions.Scheme).FirstOrDefault();
        if (myAuthIdentities == null)
        {
           context.Fail();
           return;
        }

        // grab the authentication header and uri types for our identity
        var authHeaderClaim = myAuthIdentities.Claims.Where(x => x.Type == ClaimTypes.Authentication).FirstOrDefault();
        var uriClaim = context.User.Claims.Where(x => x.Type == ClaimTypes.Uri).FirstOrDefault();
        if (uriClaim == null || authHeaderClaim == null)
        {
           context.Fail();
           return;
        }

        // enforce our requirement (evaluate values from the identity/claims)
        if ( /* passes our enforcement test */ )
        {
           context.Succeed(requirement);
        }
        else
        {
           context.Fail();
        }
     }
  }

3. add the requirement handler as an authorization policy

Our authentication requirement still needs to be added to the Startup.ConfigureServices so that it can be used:

// add our policy to the authorization configuration
services.ConfigureAuthorization(auth =>
{
   auth.AddPolicy(MyAuthOptions.Scheme, 
      policy => policy.Requirements.Add(new MyAuthRequirement()));
});

4. use the authorization policy

The final step is to enforce this requirement for specific actions by decorating our action or controller with [Authorize("MyAuth")]. If we have many controllers, each with many action which require enforcement, then we may want to make a base class and just decorate that single controller.

Your simpler situation:

Each action has it's own Role ( Role name = ControllerName.actionName> )

If you already have all your actions fine-tuned with [Authorize(Roles = "controllername.actionname")] then you probably only need part #1 above. Just add a new Claim(ClaimTypes.Role, "controllername.actionname") that is valid for the particular request.

jltrem
  • 10,475
  • 4
  • 37
  • 46
  • Thanks for your answer, but I want to keep the authorization early in the pipeline, actionFilter comes very late and would be costy for just authorization. – yeska Jul 29 '15 at 17:38
  • does (or can) your HTTP header or cookie contain authorization info that can be used to identify the Role? If you are relying only on the action specified, then you'll need to complete the MVC routing (due to default values) before you know what action will be hit – jltrem Jul 29 '15 at 18:52
  • 1
    my header will contain just a token, so if authorization comes before routing, I prefer to parse the request url myself, it wouldn't be complicated – yeska Aug 01 '15 at 18:39
  • @yeska I did a complete rewrite to use middleware and authorization pipeline – jltrem Aug 01 '15 at 21:49
  • 1
    @pinpoint mentioned to me that part 1 is the generic way to write middleware, but there are authorization-specific classes that provide other benefits. Unfortunately there are significant API differences in this between the different beta versions. Take a look at `AuthenticationMiddleware` and `AuthenticationHandler` in the [asp.net security repo](https://github.com/aspnet/Security/). – jltrem Aug 01 '15 at 22:52
  • @yeska I updated this for beta6 and to use AuthenticationHandler & AuthenticationMiddleware – jltrem Aug 02 '15 at 05:06
  • @jiltrem Thank you very much for this very complete answer ! – yeska Aug 02 '15 at 18:07
  • @yeska no problem! thanks for the question. What is here works but I'm going to refine MyAuthHandler a bit more. Will post an update when I do. – jltrem Aug 02 '15 at 18:36
  • @yeska all done now. Just changed HandleAuthenticateAsync to not add directly to `Context.User` since [`BaseInitializeAsync` merges the principal we provide](https://github.com/aspnet/Security/blob/ab4ba794e546074a573fbf76d9150ffcf9752c89/src/Microsoft.AspNet.Authentication/AuthenticationHandler.cs#L81). Also got rid of the async/await since there was no benefit here. – jltrem Aug 03 '15 at 03:50
  • 4
    Looks like AuthenticationTicket is old see http://stackoverflow.com/a/37415902/632495 – Jon49 Jan 27 '17 at 15:43
  • This method now expects a return type of `Task`, not `Task`. So for failed auth you could return `Task.FromResult(AuthenticateResult.Skip())`, and for success you could return `Task.FromResult(AuthenticateResult.Success(ticket))`. – Tobias J Feb 04 '17 at 23:58
  • There is one issue with `HandleAuthenticateAsync` - you can't access currently called controller name because MVC (seemingly) hasn't resolved it. Ability to know the name at this point would allow for more efficient claims retrieval from the database when creating `ClaimsIdentity` - if we know the requested resource by some key, we can request the database for the specific resource permissions only and not all possible permissions for this user. The optimization could be useful if your web app doesn't have state (session) to cache permissions and loads them on every request. – JustAMartin Sep 10 '19 at 13:31