Add IAuthorizationPolicyProvider sample (#31)

* Add IAuthorizationPolicyProvider sample

* Update IAuthorizationPolicyProvider sample to show falling back to a default provider

* Make project, directory, and namespace names consistent for custom policy provider sample

* Make policy provider sample an MVC app and replace Values controller with a home controller (with views)

* Add cookie authentication and account controller/views

* Update readme and comments to reflect new authorization policy provider scenario

* Add some more information to the access denied page for debugging purposes

* Add some additional logging

* Minor updates based on PR feedback
This commit is contained in:
Mike Rousos 2018-04-26 16:25:07 -04:00 committed by Hao Kung
parent 2abf5de0e3
commit 8d0f84ea46
18 changed files with 448 additions and 0 deletions

1
.gitignore vendored
View File

@ -9,6 +9,7 @@ packages/
artifacts/
PublishProfiles/
.vs/
.vscode/
*.user
*.suo
*.cache

View File

@ -24,6 +24,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Identity.ExternalClaims", "
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DynamicSchemes", "samples\DynamicSchemes\DynamicSchemes.csproj", "{F2F7A64C-870C-40C9-B5FC-F8952F1572B3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CustomPolicyProvider", "samples\CustomPolicyProvider\CustomPolicyProvider.csproj", "{70299871-8FF5-4521-AD56-48BB6E07BA13}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StaticFilesAuth", "samples\StaticFilesAuth\StaticFilesAuth.csproj", "{0F013930-E66F-4F8B-95BE-CDFB417ACE3E}"
EndProject
Global
@ -100,6 +102,18 @@ Global
{F2F7A64C-870C-40C9-B5FC-F8952F1572B3}.Release|x64.Build.0 = Release|Any CPU
{F2F7A64C-870C-40C9-B5FC-F8952F1572B3}.Release|x86.ActiveCfg = Release|Any CPU
{F2F7A64C-870C-40C9-B5FC-F8952F1572B3}.Release|x86.Build.0 = Release|Any CPU
{70299871-8FF5-4521-AD56-48BB6E07BA13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{70299871-8FF5-4521-AD56-48BB6E07BA13}.Debug|Any CPU.Build.0 = Debug|Any CPU
{70299871-8FF5-4521-AD56-48BB6E07BA13}.Debug|x64.ActiveCfg = Debug|Any CPU
{70299871-8FF5-4521-AD56-48BB6E07BA13}.Debug|x64.Build.0 = Debug|Any CPU
{70299871-8FF5-4521-AD56-48BB6E07BA13}.Debug|x86.ActiveCfg = Debug|Any CPU
{70299871-8FF5-4521-AD56-48BB6E07BA13}.Debug|x86.Build.0 = Debug|Any CPU
{70299871-8FF5-4521-AD56-48BB6E07BA13}.Release|Any CPU.ActiveCfg = Release|Any CPU
{70299871-8FF5-4521-AD56-48BB6E07BA13}.Release|Any CPU.Build.0 = Release|Any CPU
{70299871-8FF5-4521-AD56-48BB6E07BA13}.Release|x64.ActiveCfg = Release|Any CPU
{70299871-8FF5-4521-AD56-48BB6E07BA13}.Release|x64.Build.0 = Release|Any CPU
{70299871-8FF5-4521-AD56-48BB6E07BA13}.Release|x86.ActiveCfg = Release|Any CPU
{70299871-8FF5-4521-AD56-48BB6E07BA13}.Release|x86.Build.0 = Release|Any CPU
{0F013930-E66F-4F8B-95BE-CDFB417ACE3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0F013930-E66F-4F8B-95BE-CDFB417ACE3E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0F013930-E66F-4F8B-95BE-CDFB417ACE3E}.Debug|x64.ActiveCfg = Debug|Any CPU
@ -123,6 +137,7 @@ Global
{4E91BD2A-616F-45EE-9647-2F1608D17FB9} = {CA4538F5-9DA8-4139-B891-A13279889F79}
{D8804E7A-BD7A-4E4B-ACA7-822A37A81B28} = {CA4538F5-9DA8-4139-B891-A13279889F79}
{F2F7A64C-870C-40C9-B5FC-F8952F1572B3} = {CA4538F5-9DA8-4139-B891-A13279889F79}
{70299871-8FF5-4521-AD56-48BB6E07BA13} = {CA4538F5-9DA8-4139-B891-A13279889F79}
{0F013930-E66F-4F8B-95BE-CDFB417ACE3E} = {CA4538F5-9DA8-4139-B891-A13279889F79}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution

View File

@ -0,0 +1,61 @@
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Logging;
namespace CustomPolicyProvider
{
// This class contains logic for determining whether MinimumAgeRequirements in authorizaiton
// policies are satisfied or not
internal class MinimumAgeAuthorizationHandler : AuthorizationHandler<MinimumAgeRequirement>
{
private readonly ILogger<MinimumAgeAuthorizationHandler> _logger;
public MinimumAgeAuthorizationHandler(ILogger<MinimumAgeAuthorizationHandler> logger)
{
_logger = logger;
}
// Check whether a given MinimumAgeRequirement is satisfied or not for a particular context
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MinimumAgeRequirement requirement)
{
// Log as a warning so that it's very clear in sample output which authorization policies
// (and requirements/handlers) are in use
_logger.LogWarning("Evaluating authorization requirement for age >= {age}", requirement.Age);
// Check the user's age
var dateOfBirthClaim = context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth);
if (dateOfBirthClaim != null)
{
// If the user has a date of birth claim, check their age
var dateOfBirth = Convert.ToDateTime(dateOfBirthClaim.Value);
var age = DateTime.Now.Year - dateOfBirth.Year;
if (dateOfBirth > DateTime.Now.AddYears(-age))
{
// Adjust age if the user hasn't had a birthday yet this year
age--;
}
// If the user meets the age criterion, mark the authorization requirement succeeded
if (age >= requirement.Age)
{
_logger.LogInformation("Minimum age authorization requirement {age} satisfied", requirement.Age);
context.Succeed(requirement);
}
else
{
_logger.LogInformation("Current user's DateOfBirth claim ({dateOfBirth}) does not satisfy the minimum age authorization requirement {age}",
dateOfBirthClaim.Value,
requirement.Age);
}
}
else
{
_logger.LogInformation("No DateOfBirth claim present");
}
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,35 @@
using Microsoft.AspNetCore.Authorization;
namespace CustomPolicyProvider
{
// This attribute derives from the [Authorize] attribute, adding
// the ability for a user to specify an 'age' paratmer. Since authorization
// policies are looked up from the policy provider only by string, this
// authorization attribute creates is policy name based on a constant prefix
// and the user-supplied age parameter. A custom authorization policy provider
// (`MinimumAgePolicyProvider`) can then produce an authorization policy with
// the necessary requirements based on this policy name.
internal class MinimumAgeAuthorizeAttribute : AuthorizeAttribute
{
const string POLICY_PREFIX = "MinimumAge";
public MinimumAgeAuthorizeAttribute(int age) => Age = age;
// Get or set the Age property by manipulating the underlying Policy property
public int Age
{
get
{
if (int.TryParse(Policy.Substring(POLICY_PREFIX.Length), out var age))
{
return age;
}
return default(int);
}
set
{
Policy = $"{POLICY_PREFIX}{value.ToString()}";
}
}
}
}

View File

@ -0,0 +1,50 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
namespace CustomPolicyProvider
{
internal class MinimumAgePolicyProvider : IAuthorizationPolicyProvider
{
const string POLICY_PREFIX = "MinimumAge";
public DefaultAuthorizationPolicyProvider FallbackPolicyProvider { get; }
public MinimumAgePolicyProvider(IOptions<AuthorizationOptions> options)
{
// ASP.NET Core only uses one authorization policy provider, so if the custom implementation
// doesn't handle all policies (including default policies, etc.) it should fall back to an
// alternate provider.
//
// In this sample, a default authorization policy provider (constructed with options from the
// dependency injection container) is used if this custom provider isn't able to handle a given
// policy name.
//
// If a custom policy provider is able to handle all expected policy names then, of course, this
// fallback pattern is unnecessary.
FallbackPolicyProvider = new DefaultAuthorizationPolicyProvider(options);
}
public Task<AuthorizationPolicy> GetDefaultPolicyAsync() => FallbackPolicyProvider.GetDefaultPolicyAsync();
// Policies are looked up by string name, so expect 'parameters' (like age)
// to be embedded in the policy names. This is abstracted away from developers
// by the more strongly-typed attributes derived from AuthorizeAttribute
// (like [MinimumAgeAuthorize] in this sample)
public Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
{
if (policyName.StartsWith(POLICY_PREFIX, StringComparison.OrdinalIgnoreCase) &&
int.TryParse(policyName.Substring(POLICY_PREFIX.Length), out var age))
{
var policy = new AuthorizationPolicyBuilder();
policy.AddRequirements(new MinimumAgeRequirement(age));
return Task.FromResult(policy.Build());
}
// If the policy name doesn't match the format expected by this policy provider,
// try the fallback provider. If no fallback provider is used, this would return
// Task.FromResult<AuthorizationPolicy>(null) instead.
return FallbackPolicyProvider.GetPolicyAsync(policyName);
}
}
}

View File

@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Authorization;
namespace CustomPolicyProvider
{
internal class MinimumAgeRequirement : IAuthorizationRequirement
{
public int Age { get; private set; }
public MinimumAgeRequirement(int age) { Age = age; }
}
}

View File

@ -0,0 +1,51 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
namespace CustomPolicyProvider.Controllers
{
public class AccountController: Controller
{
[HttpGet]
public IActionResult Signin(string returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
return View();
}
[HttpPost]
public async Task<IActionResult> Signin(string userName, DateTime? birthDate, string returnUrl = null)
{
if (string.IsNullOrEmpty(userName)) return BadRequest("A user name is required");
// In a real-world application, user credentials would need validated before signing in
var claims = new List<Claim>();
// Add a Name claim and, if birth date was provided, a DateOfBirth claim
claims.Add(new Claim(ClaimTypes.Name, userName));
if (birthDate.HasValue)
{
claims.Add(new Claim(ClaimTypes.DateOfBirth, birthDate.Value.ToShortDateString()));
}
// Create user's identity and sign them in
var identity = new ClaimsIdentity(claims, "UserSpecified");
await HttpContext.SignInAsync(new ClaimsPrincipal(identity));
return Redirect(returnUrl ?? "/");
}
public async Task<IActionResult> Signout()
{
await HttpContext.SignOutAsync();
return Redirect("/");
}
public IActionResult Denied()
{
return View();
}
}
}

View File

@ -0,0 +1,28 @@
using Microsoft.AspNetCore.Mvc;
namespace CustomPolicyProvider.Controllers
{
// Sample actions to demonstrate the use of the [MinimumAgeAuthorize] attribute
[Controller]
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
// View protected with custom parameterized authorization policy
[MinimumAgeAuthorize(10)]
public IActionResult MinimumAge10()
{
return View("MinimumAge", 10);
}
// View protected with custom parameterized authorization policy
[MinimumAgeAuthorize(50)]
public IActionResult MinimumAge50()
{
return View("MinimumAge", 50);
}
}
}

View File

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.1;netcoreapp2.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore" Version="$(MicrosoftAspNetCorePackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="$(MicrosoftAspNetCoreAuthenticationCookiesPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="$(MicrosoftAspNetCoreAuthorizationPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="$(MicrosoftAspNetCoreMvcPackageVersion)" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,17 @@
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
namespace CustomPolicyProvider
{
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
}

View File

@ -0,0 +1,45 @@
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
namespace CustomPolicyProvider
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Replace the default authorization policy provider with our own
// custom provider which can return authorization policies for given
// policy names (instead of using the default policy provider)
services.AddSingleton<IAuthorizationPolicyProvider, MinimumAgePolicyProvider>();
// As always, handlers must be provided for the requirements of the authorization policies
services.AddSingleton<IAuthorizationHandler, MinimumAgeAuthorizationHandler>();
services.AddMvc();
// Add cookie authentication so that it's possible to sign-in to test the
// custom authorization policy behavior of the sample
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.AccessDeniedPath = "/account/denied";
options.LoginPath = "/account/signin";
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}");
});
}
}
}

View File

@ -0,0 +1,15 @@
@using System.Security.Claims
<!-- Simple view used when a user doesn't have authorization to access a resource -->
<h2>Access Denied: @(User?.Identity?.Name ?? "User") is not authorized to view this page.</h2>
<!-- Show the current user's date of birth (if such a claim exists) for debugging purposes -->
@{
var dateOfBirth = User?.FindFirst(c => c.Type == ClaimTypes.DateOfBirth)?.Value;
}
<h3>Date of birth: @(dateOfBirth ?? "<Undefined>")</h3>
<ul>
<li>@Html.ActionLink("Home", "Index", "Home")</li>
<li>@Html.ActionLink("Sign Out", "Signout", "Account")</li>
</ul>

View File

@ -0,0 +1,18 @@
<h1>Sign In</h1>
<div>
<form asp-controller="Account" asp-action="Signin" asp-route-returnurl="@ViewData["ReturnUrl"]" method="post">
<div>
<label>User Name</label>
<input type="text" name="userName" />
</div>
<div>
<label>Date of Birth (MM/DD/YYYY)</label>
<input type="text" name="birthDate" />
</div>
<div>
<button type="submit">Sign In</button>
</div>
</form>
</div>

View File

@ -0,0 +1,19 @@
<h1>Custom Authorization Policy Provider Sample</h1>
<p>
This sample demonstrates a custom IAuthorizationPolicyProvider which
dynamically generates authorization policies based on arguments
(in this case, an integer indicating the minimum age required to
satisfy the policies requirements).
</p>
<p>
Use the links below to sign in, sign out, or to try accessing pages
requiring different minimum ages.
</p>
<ul>
<li>@Html.ActionLink("Sign In", "Signin", "Account")</li>
<li>@Html.ActionLink("Sign Out", "Signout", "Account")</li>
<li>@Html.ActionLink("Minimum Age 10", "MinimumAge10", "Home")</li>
<li>@Html.ActionLink("Minimum Age 50", "MinimumAge50", "Home")</li>
</ul>

View File

@ -0,0 +1,21 @@
@using System.Security.Claims
@model int
@{
var dateOfBirth = User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth)?.Value;
}
<!-- Welcome user and display DateOfBirth claim value -->
<h1>Welcome, @User.Identity.Name</h1>
<h2>Welcome to a page restricted to users @Model or older</h2>
<p>
You can access this page since you were born on @dateOfBirth.
</p>
<ul>
<li>@Html.ActionLink("Home", "Index", "Home")</li>
<li>@Html.ActionLink("Sign Out", "Signout", "Account")</li>
</ul>

View File

@ -0,0 +1,7 @@
{
"Logging": {
"LogLevel": {
"Default": "Information"
}
}
}

View File

@ -0,0 +1,7 @@
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
}
}

View File

@ -0,0 +1,34 @@
IAuthorizationPolicyProvider Sample
===================================
This small sample demonstrates how to use `IAuthorizationPolicyProvider` to
dynamically produce authorization policies.
In the simple example, a `MinimumAgePolicyProvider` will produce minimum age
policies for any integer age (based on the policy's
name). This demonstrates a slightly round-about way to allow 'parameterized'
authorization policies. Since authorization policies are identified by
name strings, the custom `MinimumAgeAuthorizeAttribute` in the sample
allows users to specify an age parameter and then embeds it into its
underlying `AuthorizationAttribute`'s policy name string. The
`MinimumAgePolicyProvider` dynamically generates the policies needed for use
with these attributes by pulling the age from the policy name and creating
necessary authorization requirements.
Other uses of `IAuthorizationPolicyProvider` might be loading policy
information from some external data source (like a database, for example).
Notice that ASP.NET Core only uses one authorization policy provider, so
if not all policies will be generated by the custom policy provider, it
should fall back to other policy providers (like `DefaultAuthorizationPolicyProvider`).
To use the sample:
1. Run the app
2. Navigate to http://localhost:11606/
3. Attempt to follow one of the 'Minimum Age' links
4. Sign in by providing a user name and birth date
5. Notice that depending on the birth date entered, pages guarded by minimum age authorization policies will either be accessible or forbidden
The interesting classes for this sample are in the Authorization folder,
particularly `MinimumAgePolicyProvider`.