Aspnetcore: Future AuthZ improvements list

Created on 10 Aug 2017  ·  34Comments  ·  Source: dotnet/aspnetcore

This issue will track all the various Authorization improvements we are looking at for 2.1.

Some initial thoughts:

From @davidfowl

We've had a bunch of feedback about our authz system with respect to flowing context from the authorize attribute to the authorization handler:

Today the Authorize attribute supports has enough metadata to describe the policy but it doesn't let you specify the resource (via IAuthorizeData). I think if we solve that, it might solve a bulk of the issues since people will be able to write custom attributes that flow the relevant context to the authorization handler.

Today that's only possible when doing imperative authz. I was thinking something like:

public interface IAuthorizeResource
{
    object Resource { get; }
}

If the attribute implemented this, we would flow that as the resource to the handler. This way you implement permissions of whatever you please via resources.


From @rynowak

Look into providing overriding semantics in MVC, maybe a marker interface: IAuthorizeMetadata. Any attributes that implement that interface on the endpoint could be flowed. Then it’s up to developers to build whatever they want. (Also look into flowing single objects vs many objects + requirements VS resources)

We will also look into making it possible to specify requirements via Attributes similar to imperative AuthZ so you don't have to preconstruct policies for attributes.

Misc other improvements:

Design affected-medium area-security enhancement severity-major

Most helpful comment

I want to have ability to return custom json if specific policy failed. for example
403
{
"errorType": "PrivalcyPolicyAcceptRequired",
"errorMessage": "You token doesn't have PolicyAccepted claim"
}

All 34 comments

My additional thoughts on this. We're looking at making the concept of an endpoint available at a low level from the dispatcher. Endpoints will have arbitrary metadata in an ordered list. This is the equivalent to what MVC does today by passing the action descriptor.

We'll probably also have a concept of active metadata which are metadata items that have been hydrated with the request context.

These might be useful/necessary building blocks for these things.


Additionally in the case of MVC, the current [Authorize] has some surprising behaviors compared to other filters. All authorize filters will execute rather than supporting overriding. Additionally, [AllowAnonymous] cannot be overridden and turns off all authorize attributes. These might be consistent with the past, but they don't really fit with the zen of MVC filters.

Interesting, well if we have a new endpoint concept with a clean slate for metadata, we should probably just expose all of the basic building blocks

  1. Building the ClaimsPrincipal, need to end up with 0 or more authentication schemes. If none are specified, we continue to use the default user aka httpContext.User, otherwise we build a merged user via authenticating all of the schemes. We could manifest this with a series of [Authenticate(scheme)] attributes.

  2. Specifying the Authorization requirements, today the metadata only targets policies, but we could have [Authorize(typeof(MyRequirement), typeof(MyOtherRequirement)]

Some examples:

[Authorize]

Would behave the same as today (using some default policy which relies on context.User, and still only does RequireAuthenticatedUser by default

[Authenticate("Bearer")]
[Authorize]

Would use the Bearer scheme and only require that it exists.

[Authenticate("Bearer", "Cookie")]
[Authorize(typeof(RequireAdminClaim)]

Would use the merged Bearer Cookie schemes and require an admin claim.

Which challenge would you issue?

All of them, its not any different than today

Challenging both Bearer and Cookie is nonsensical.

Not disagreeing, but today if you have a policy that has Bearer and Cookies, that's what it does today as well

Unless we want to eliminate being able to specify multiple schemes entirely... that's an option that would simplify things...

Not disagreeing, but today if you have a policy that has Bearer and Cookies, that's what it does today as well

It does? What happens as a result? Don't they stomp on each other?

Yeah they do, last one wins, multiple auth schemes in policies has always been a bit weird

Some are compatible like Bearer, Basic, and Windows.

@HaoK please log new issues for any 2.1.0 proposals.

@HaoK Is it going to be a part of 2.1 release?

I want to have ability to return custom json if specific policy failed. for example
403
{
"errorType": "PrivalcyPolicyAcceptRequired",
"errorMessage": "You token doesn't have PolicyAccepted claim"
}

No the error authZ improvements didn't make it into 2.1, I'll try to get them in 2.2

I assume that is why the title changed to 2.2 😜⁉

Yup, I've got some cycles this week so I'll try to prototype something

So rough idea of some initial AuthZ improvements I've got at the top of my head for 2.2

  • For errors, maybe some kind of scoped IAuthorizationErrorService, that the authZ services will report errors to with appropriate context. This should enable apps to write an appropriate detailed authorization error.

  • UseAuthorize("policyName") - middleware equivalent of [Authorize("policyName")], would behave similarly, and also allow sideffect of setting httpContext.User if AuthenticationScheme is specified in the policy. TBD on what to do for failure, redirect to some access denied page which is able to display the appropriate context seems ideal.

I think we should close this issue and revisit this in 2.2 planning so we can scope it down.

Close or leave it open for planning?

Have you considered adding some way how we can access the list of policies which is run and which is failed. Also I thought it could be a helpful to have overload of method context.Fail(); with some object which can be accessed later.

Is this what currently prevents something such as the ability to show a specific page when a specific policy fails. For example, when a policy fails, redirect to a page that shows how to get that particular policy? (i.e. paying for a new membership level).

@natelaff I've seen that there is a IPolicyEvaluator interface you can implement (using a base class of PolicyEvaluator to make it easier). This will allowed me to redirect the user on the event of a forbidden policy.

In the example below, I have the same behaviour for all policies, but you could filter it out to trigger only on a specific policy based on the first parameter.

private class RedirectingPolicyEvaluator : PolicyEvaluator
{
    public RedirectingPolicyEvaluator(IAuthorizationService authorization) : base(authorization)
    {
    }

    public override async Task<PolicyAuthorizationResult> AuthorizeAsync(AuthorizationPolicy policy, AuthenticateResult authenticationResult, HttpContext context, object resource)
    {
        var result = await base.AuthorizeAsync(policy, authenticationResult, context, resource);
        if (result.Forbidden)
        {
            // If user is authenticated but not allowed, send them to a special error page
            if (context.User.Identity.IsAuthenticated)
            {
                var urlHelper = context.RequestServices.GetRequiredService<IUrlHelper>();
                var redirectTo = urlHelper.Action(nameof(HomeController.Unauthorized), "Home", new { ReturnUrl = context.Request.GetEncodedUrl() });

                // Redirect the user
                context.Response.Redirect(redirectTo);

                // Return success since we've handled it now
                return PolicyAuthorizationResult.Success();
            }

        }
        return result;
    }
}

And registered in my Startup.cs with:

        services
            .AddAuthorization()
            .AddScoped<IPolicyEvaluator, RedirectingPolicyEvaluator>()
            ;

@HaoK @rynowak Yo, I think we can close this now that we have AuthorizationMiddleware + endpoints + AuthorizeAttribute. Is there anything left to do?

This is currently also serving as tracking several other asks we haven't done yet for AuthZ:

  • Consider supporting OR logic for policies: aspnet/Security#1356
  • Consider enabling policies to be defined in configuration
  • Make it easier to be able to display authZ failure reasons in an error page etc aspnet/Security#1530
  • Make it easier to pass parameters to policies, i.e. aspnet/Security#1689

We moved this into backlog in triage last week so I think its safe to just leave this parked there for now

Is there any progress on custom failure message? Or any official workaround?

I too wonder about how to get a custom json body when ForbidResult is called.

Is there any way to get at the AuthorizationResult(s) later in the pipeline? As far as I can tell it gets swallowed as part of the PolicyEvaluator (line 84 where it calls authorizationService). The following works inside the controller action but it completely bypasses the filters and I really don't want to go this route.

c# //ControllerAction public async Task<IActionResult> GetSomeThing() { var a = await this._authorizationService.AuthorizeAsync(User, "MyPolicy"); if (!a.Succeeded) { return this.StatusCode(403, a.Failure.FailedRequirements); } }

Just putting this out there as part of the conversation, I came across the following issue and being able to map the AuthorizationResult (and FailedRequirements) to a ProblemDetail would be amazing.
https://github.com/aspnet/AspNetCore/issues/10120

Ping! Any progress on these issues in this ticket?

I am looking for a solution to obtain the reason an AuthorizationPolicy failed, as in, what Claim or scope was missing or whatever the reason might be for the HTTP 403 when access is denied. I would like to provide a JSON body in the 403 reply with said information.

@bugnuker As far as I can tell, there's still no support for specific "reasons" why an authorization is marked as Forbidden. However, if your system is relatively simple (e.g. mine is mostly just role based), then you could write a PolicyEvaluator (as mentioned above) and look up the existing policy.Requirements to attach detail the redirection. For example:

        private class RedirectingPolicyEvaluator : PolicyEvaluator
        {
            internal static object ItemsKey = new object();

            public RedirectingPolicyEvaluator(IAuthorizationService authorization) : base(authorization)
            {
            }

            public override async Task<PolicyAuthorizationResult> AuthorizeAsync(AuthorizationPolicy policy, AuthenticateResult authenticationResult, HttpContext context, object resource)
            {
                var result = await base.AuthorizeAsync(policy, authenticationResult, context, resource);
                if (result.Forbidden && policy.Requirements.OfType<RolesAuthorizationRequirement>().Any())
                {
                    // If user is authenticated but not allowed, send them to a special error page
                    if (authenticationResult.Succeeded)
                    {
                        // Find all the role name that are required
                        var rolesRequired = policy.Requirements.OfType<RolesAuthorizationRequirement>().SelectMany(r => r.AllowedRoles).Where(role => !authenticationResult.Principal.IsInRole(role));

                        // Get the URL to the help page, with a return url that's appropriate to the request type
                        var urlHelper = context.RequestServices.GetRequiredService<IUrlHelper>();
                        var returnUrl = context.Request.GetEncodedUrl();
                        if (context.Request.IsAjaxRequest())
                        {
                            returnUrl = FindAjaxRequestReturnUrl(context, urlHelper);
                        }
                        var redirectTo = urlHelper.AbsoluteAction(nameof(HomeController.Unauthorized), "Home", new { ReturnUrl = returnUrl, RolesRequired = rolesRequired.ToArray() });

                        // Store an item in the context
                        context.Items[ItemsKey] = new ForbiddenRequestDetails { RedirectTo = redirectTo, RolesRequired = rolesRequired.ToArray() };
                    }
                }
                return result;
            }
        }

Then I wrapped/adapted the IAuthenticationService interface to override what happens for ForbidAsync in particular by looking for that ForbiddenRequestDetails in the context items:

        private class RedirectingAuthenticationService : IAuthenticationService
        {
            private readonly IAuthenticationService _adaptee;

            public RedirectingAuthenticationService(IAuthenticationService adaptee)
            {
                _adaptee = adaptee;
            }

            .... Other methods removed ...

            public Task ForbidAsync(HttpContext context, string scheme, AuthenticationProperties properties)
            {
                if (context.Items.ContainsKey(RedirectingPolicyEvaluator.ItemsKey))
                {
                    var options = context.Items[RedirectingPolicyEvaluator.ItemsKey] as ForbiddenRequestDetails;
                    var isApi = context.Request.IsAjaxRequest();
                    if (!isApi)
                    {
                        // Redirect the user
                        context.Response.Redirect(options.RedirectTo);
                        return Task.CompletedTask;
                    }
                    else
                    {
                        // Just write some extra stuff to the content body to make it easier
                        var routeData = context.GetRouteData();
                        var actionDescriptor = new ActionDescriptor();
                        var actionContext = new ActionContext(context, routeData, actionDescriptor);
                        var actionResult = new JsonResult(options) { StatusCode = (int)HttpStatusCode.Forbidden };
                        return actionResult.ExecuteResultAsync(actionContext);
                    }
                }
                return _adaptee.ForbidAsync(context, scheme, properties);
            }
     }

We are also building a Asp.Net Web application with api controllers with Attribute based authorization and need a way to returm some additional information about the reason why a request was forbidden by the authorization handler The possibility to add a custom header or json body would do a lot for us.

Also interested in returning a reason as part failing a policy.

For anyone else, there is a very dodgy workaround you can use here
https://github.com/aspnet/Security/issues/1560#issue-278186113

Returning an auth failure reason now seems to be supported thanks to https://github.com/dotnet/aspnetcore/pull/21117 ?

@HaoK @blowdart is there any design progress on allowing the construction of AuthorizationAttributes that make use of resources? Instead of having to fall back on imperative resource-based authorization.
There's solutions like this article, but the potential disconnect between the self-parsed values out of ActionArguments and what MVC etc ends up binding to makes it way too easy to end up with security issues.
Thanks!

No there is not. We can't have model binding before authz, because people do weird things in model binding which can have side effects, which in turn cause security issues. It's a non-starter.

Regarding this specifically - it's possible to access routing's info in AuthZ with the current layering. So you could figure out the id of the item being accessed (in a typical scenario) but without having run any model binding code to turn arbitrary data into arbitrary objects.

Not sure if this helps 😆

Was this page helpful?
0 / 5 - 0 ratings