211 lines
9.5 KiB
ReStructuredText
211 lines
9.5 KiB
ReStructuredText
.. _security-authorization-policies-based:
|
|
|
|
Custom Policy-Based Authorization
|
|
=================================
|
|
|
|
Underneath the covers the :ref:`role authorization <security-authorization-role-based>` and :ref:`claims authorization <security-authorization-claims-based>` make use of a requirement, a handler for the requirement and a pre-configured policy. These building blocks allow you to express authorization evaluations in code, allowing for a richer, reusable, and easily testable authorization structure.
|
|
|
|
An authorization policy is made up of one or more requirements and registered at application startup as part of the Authorization service configuration, in ``ConfigureServices`` in the *Startup.cs* file.
|
|
|
|
.. code-block:: c#
|
|
|
|
public void ConfigureServices(IServiceCollection services)
|
|
{
|
|
services.AddMvc();
|
|
|
|
services.AddAuthorization(options =>
|
|
{
|
|
options.AddPolicy("Over21",
|
|
policy => policy.Requirements.Add(new MinimumAgeRequirement(21)));
|
|
}
|
|
});
|
|
|
|
Here you can see an "Over21" policy is created with a single requirement, that of a minimum age, which is passed as a parameter to the requirement.
|
|
|
|
Policies are applied using the ``Authorize`` attribute by specifying the policy name, for example;
|
|
|
|
.. code-block:: c#
|
|
|
|
[Authorize(Policy="Over21")]
|
|
public class AlcoholPurchaseRequirementsController : Controller
|
|
{
|
|
public ActionResult Login()
|
|
{
|
|
}
|
|
|
|
public ActionResult Logout()
|
|
{
|
|
}
|
|
}
|
|
|
|
Requirements
|
|
------------
|
|
An authorization requirement is a collection of data parameters that a policy can use to evaluate the current user principal. In our Minimum Age policy the requirement we have a single parameter, the minimum age. A requirement must implement ``IAuthorizationRequirement``. This is an empty, marker interface. A parameterized minimum age requirement might be implemented as follows;
|
|
|
|
.. code-block:: c#
|
|
|
|
public class MinimumAgeRequirement : IAuthorizationRequirement
|
|
{
|
|
public MinimumAgeRequirement(int age)
|
|
{
|
|
MinimumAge = age;
|
|
}
|
|
|
|
protected int MinimumAge { get; set; }
|
|
}
|
|
|
|
A requirement doesn't need to have data or properties.
|
|
|
|
.. _security-authorization-policies-based-authorization-handler:
|
|
|
|
Authorization Handlers
|
|
----------------------
|
|
|
|
An authorization handler is responsible for the evaluation of any properties of a requirement. The authorization handler must evaluate them against a provided ``AuthorizationContext`` to decide if authorization is allowed. A requirement can have :ref:`multiple handlers <security-authorization-policies-based-multiple-handlers>`. Handlers must inherit ``AuthorizationHandler<T>`` where T is the requirement it handles.
|
|
|
|
.. _security-authorization-handler-example:
|
|
|
|
The minimum age handler might look like this:
|
|
|
|
.. code-block:: c#
|
|
|
|
public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
|
|
{
|
|
protected override void Handle(AuthorizationContext context, MinimumAgeRequirement requirement)
|
|
{
|
|
if (!context.User.HasClaim(c => c.Type == ClaimTypes.DateOfBirth &&
|
|
c.Issuer == "http://contoso.com"))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var dateOfBirth = Convert.ToDateTime(context.User.FindFirst(
|
|
c => c.Type == ClaimTypes.DateOfBirth && c.Issuer == "http://contoso.com").Value);
|
|
|
|
int calculatedAge = DateTime.Today.Year - dateOfBirth.Year;
|
|
if (dateOfBirth > DateTime.Today.AddYears(-calculatedAge))
|
|
{
|
|
calculatedAge--;
|
|
}
|
|
|
|
if (calculatedAge >= requirement.MinimumAge)
|
|
{
|
|
context.Succeed(requirement);
|
|
}
|
|
}
|
|
}
|
|
|
|
In the code above we first look to see if the current user principal has a date of birth claim which has been issued by an Issuer we know and trust. If the claim is missing we can't authorize so we return. If we have a claim, we figure out how old the user is, and if they meet the minimum age passed in by the requirement then authorization has been successful. Once authorization is successful we call ``context.Succeed()`` passing in the requirement that has been successful as a parameter.
|
|
|
|
.. _security-authorization-policies-based-handler-registration:
|
|
|
|
Handlers must be registered in the services collection during configuration, for example;
|
|
|
|
.. code-block:: c#
|
|
|
|
public void ConfigureServices(IServiceCollection services)
|
|
{
|
|
services.AddMvc();
|
|
|
|
services.AddAuthorization(options =>
|
|
{
|
|
options.AddPolicy("Over21",
|
|
policy => policy.Requirements.Add(new MinimumAgeRequirement(21)));
|
|
});
|
|
|
|
services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
|
|
}
|
|
|
|
Each handler is added to the services collection by using ``services.AddSingleton<IAuthorizationHandler, YourHandlerClass>();`` passing in your handler class.
|
|
|
|
What should a handler return?
|
|
-----------------------------
|
|
|
|
You can see in our :ref:`handler example <security-authorization-handler-example>` that the ``Handle()`` method has no return value, so how do we indicate success or failure?
|
|
|
|
* A handler indicates success by calling ``context.Succeed(IAuthorizationRequirement requirement)``, passing the requirement that has been successfully validated.
|
|
* A handler does not need to handle failures generally, as other handlers for the same requirement may succeed.
|
|
* To guarantee failure even if other handlers for a requirement succeed, call ``context.Fail``.
|
|
|
|
Regardless of what you call inside your handler all handlers for a requirement will be called when a policy requires the requirement. This allows requirements to have side effects, such as logging, which will always take place even if ``context.Fail()`` has been called in another handler.
|
|
|
|
.. _security-authorization-policies-based-multiple-handlers:
|
|
|
|
Why would I want multiple handlers for a requirement?
|
|
-----------------------------------------------------
|
|
|
|
In cases where you want evaluation to be on an **OR** basis you implement multiple handlers for a single requirement. For example, Microsoft has doors which only open with key cards. If you leave your key card at home the receptionist prints a temporary sticker and opens the door for you. In this scenario you'd have a single requirement, *EnterBuilding*, but multiple handlers, each one examining a single requirement.
|
|
|
|
.. code-block:: c#
|
|
|
|
public class EnterBuildingRequirement : IAuthorizationRequirement
|
|
{
|
|
}
|
|
|
|
public class BadgeEntryHandler : AuthorizationHandler<EnterBuildingRequirement>
|
|
{
|
|
protected override void Handle(AuthorizationContext context, EnterBuildingRequirement requirement)
|
|
{
|
|
if (context.User.HasClaim(c => c.Type == ClaimTypes.BadgeId &&
|
|
c.Issuer == "http://microsoftsecurity"))
|
|
{
|
|
context.Succeed(requirement);
|
|
}
|
|
}
|
|
}
|
|
|
|
public class HasTemporaryStickerHandler : AuthorizationHandler<EnterBuildingRequirement>
|
|
{
|
|
protected override void Handle(AuthorizationContext context, EnterBuildingRequirement requirement)
|
|
{
|
|
if (context.User.HasClaim(c => c.Type == ClaimTypes.TemporaryBadgeId &&
|
|
c.Issuer == "https://microsoftsecurity"))
|
|
{
|
|
// We'd also check the expiration date on the sticker.
|
|
context.Succeed(requirement);
|
|
}
|
|
}
|
|
}
|
|
|
|
Now, assuming both handlers are :ref:`registered <security-authorization-policies-based-handler-registration>` when a policy evaluates the ``EnterBuildingRequirement`` if either handler succeeds the policy evaluation will succeed.
|
|
|
|
Using a func to fufill a policy
|
|
-------------------------------
|
|
|
|
There may be occasions where fufilling a policy is simple to express in code. It is possible to simply supply a ``Func<AuthorizationHandlerContext, bool>`` when configuring your policy with the ``RequireAssertion`` policy builder.
|
|
|
|
For example the previous ``BadgeEntryHandler`` could be rewritten as follows;
|
|
|
|
.. code-block:: c#
|
|
|
|
services.AddAuthorization(options =>
|
|
{
|
|
options.AddPolicy("BadgeEntry",
|
|
policy => policy.RequireAssertion(context =>
|
|
context.User.HasClaim(c =>
|
|
(c.Type == ClaimTypes.BadgeId ||
|
|
c.Type == ClaimTypes.TemporaryBadgeId)
|
|
&& c.Issuer == "https://microsoftsecurity"));
|
|
}));
|
|
}
|
|
}
|
|
|
|
Accessing MVC Request Context In Handlers
|
|
-----------------------------------------
|
|
|
|
The ``Handle`` method you must implement in an authorization handler has two parameters, an ``AuthorizationContext`` and the ``Requirement`` you are handling. Frameworks such as MVC or Jabbr are free to add any object to the ``Resource`` property on the ``AuthorizationContext`` to pass through extra information.
|
|
|
|
For example MVC passes an instance of ``Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext`` in the resource property which is used to access HttpContext, RouteData and everything else MVC provides.
|
|
|
|
The use of the ``Resource`` property is framework specific. Using information in the ``Resource`` property will limit your authorization policies to particular frameworks. You should cast the ``Resource`` property using the ``as`` keyword, and then check the cast has succeed to ensure your code doesn't crash with ``InvalidCastExceptions`` when run on other frameworks;
|
|
|
|
.. code-block:: c#
|
|
|
|
var mvcContext = context.Resource as Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext;
|
|
|
|
if (mvcContext != null)
|
|
{
|
|
// Examine MVC specific things like routing data.
|
|
}
|
|
|