diff --git a/.gitignore b/.gitignore index 708c4155fa..d15bf87a35 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ packages/ artifacts/ PublishProfiles/ .vs/ +.vscode/ *.user *.suo *.cache diff --git a/AuthSamples.sln b/AuthSamples.sln index b4df19b013..063f6b6aa4 100644 --- a/AuthSamples.sln +++ b/AuthSamples.sln @@ -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 diff --git a/samples/CustomPolicyProvider/Authorization/MinimumAgeAuthorizationHandler.cs b/samples/CustomPolicyProvider/Authorization/MinimumAgeAuthorizationHandler.cs new file mode 100644 index 0000000000..f853ed50cd --- /dev/null +++ b/samples/CustomPolicyProvider/Authorization/MinimumAgeAuthorizationHandler.cs @@ -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 + { + private readonly ILogger _logger; + + public MinimumAgeAuthorizationHandler(ILogger 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; + } + } +} \ No newline at end of file diff --git a/samples/CustomPolicyProvider/Authorization/MinimumAgeAuthorizeAttribute.cs b/samples/CustomPolicyProvider/Authorization/MinimumAgeAuthorizeAttribute.cs new file mode 100644 index 0000000000..bbdebbd149 --- /dev/null +++ b/samples/CustomPolicyProvider/Authorization/MinimumAgeAuthorizeAttribute.cs @@ -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()}"; + } + } + } +} \ No newline at end of file diff --git a/samples/CustomPolicyProvider/Authorization/MinimumAgePolicyProvider.cs b/samples/CustomPolicyProvider/Authorization/MinimumAgePolicyProvider.cs new file mode 100644 index 0000000000..8fc11098a1 --- /dev/null +++ b/samples/CustomPolicyProvider/Authorization/MinimumAgePolicyProvider.cs @@ -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 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 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 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(null) instead. + return FallbackPolicyProvider.GetPolicyAsync(policyName); + } + } +} \ No newline at end of file diff --git a/samples/CustomPolicyProvider/Authorization/MinimumAgeRequirement.cs b/samples/CustomPolicyProvider/Authorization/MinimumAgeRequirement.cs new file mode 100644 index 0000000000..03d6a61ccd --- /dev/null +++ b/samples/CustomPolicyProvider/Authorization/MinimumAgeRequirement.cs @@ -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; } + } +} \ No newline at end of file diff --git a/samples/CustomPolicyProvider/Controllers/AccountController.cs b/samples/CustomPolicyProvider/Controllers/AccountController.cs new file mode 100644 index 0000000000..2a859ca22e --- /dev/null +++ b/samples/CustomPolicyProvider/Controllers/AccountController.cs @@ -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 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(); + // 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 Signout() + { + await HttpContext.SignOutAsync(); + return Redirect("/"); + } + + public IActionResult Denied() + { + return View(); + } + } +} diff --git a/samples/CustomPolicyProvider/Controllers/HomeController.cs b/samples/CustomPolicyProvider/Controllers/HomeController.cs new file mode 100644 index 0000000000..750b1ffc77 --- /dev/null +++ b/samples/CustomPolicyProvider/Controllers/HomeController.cs @@ -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); + } + } +} diff --git a/samples/CustomPolicyProvider/CustomPolicyProvider.csproj b/samples/CustomPolicyProvider/CustomPolicyProvider.csproj new file mode 100644 index 0000000000..44d2b45db0 --- /dev/null +++ b/samples/CustomPolicyProvider/CustomPolicyProvider.csproj @@ -0,0 +1,13 @@ + + + + netcoreapp2.1;netcoreapp2.0 + + + + + + + + + diff --git a/samples/CustomPolicyProvider/Program.cs b/samples/CustomPolicyProvider/Program.cs new file mode 100644 index 0000000000..6f4cba22c9 --- /dev/null +++ b/samples/CustomPolicyProvider/Program.cs @@ -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(); + } +} diff --git a/samples/CustomPolicyProvider/Startup.cs b/samples/CustomPolicyProvider/Startup.cs new file mode 100644 index 0000000000..0aeade97aa --- /dev/null +++ b/samples/CustomPolicyProvider/Startup.cs @@ -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(); + + // As always, handlers must be provided for the requirements of the authorization policies + services.AddSingleton(); + + 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}"); + }); + } + } +} diff --git a/samples/CustomPolicyProvider/Views/Account/Denied.cshtml b/samples/CustomPolicyProvider/Views/Account/Denied.cshtml new file mode 100644 index 0000000000..42f736f375 --- /dev/null +++ b/samples/CustomPolicyProvider/Views/Account/Denied.cshtml @@ -0,0 +1,15 @@ +@using System.Security.Claims + + +

Access Denied: @(User?.Identity?.Name ?? "User") is not authorized to view this page.

+ + +@{ + var dateOfBirth = User?.FindFirst(c => c.Type == ClaimTypes.DateOfBirth)?.Value; +} +

Date of birth: @(dateOfBirth ?? "")

+ +
    +
  • @Html.ActionLink("Home", "Index", "Home")
  • +
  • @Html.ActionLink("Sign Out", "Signout", "Account")
  • +
\ No newline at end of file diff --git a/samples/CustomPolicyProvider/Views/Account/Signin.cshtml b/samples/CustomPolicyProvider/Views/Account/Signin.cshtml new file mode 100644 index 0000000000..a245e76195 --- /dev/null +++ b/samples/CustomPolicyProvider/Views/Account/Signin.cshtml @@ -0,0 +1,18 @@ +

Sign In

+
+
+
+ + +
+ +
+ + +
+ +
+ +
+
+
\ No newline at end of file diff --git a/samples/CustomPolicyProvider/Views/Home/Index.cshtml b/samples/CustomPolicyProvider/Views/Home/Index.cshtml new file mode 100644 index 0000000000..43837678f5 --- /dev/null +++ b/samples/CustomPolicyProvider/Views/Home/Index.cshtml @@ -0,0 +1,19 @@ +

Custom Authorization Policy Provider Sample

+ +

+ 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). +

+

+ Use the links below to sign in, sign out, or to try accessing pages + requiring different minimum ages. +

+ +
    +
  • @Html.ActionLink("Sign In", "Signin", "Account")
  • +
  • @Html.ActionLink("Sign Out", "Signout", "Account")
  • +
  • @Html.ActionLink("Minimum Age 10", "MinimumAge10", "Home")
  • +
  • @Html.ActionLink("Minimum Age 50", "MinimumAge50", "Home")
  • +
\ No newline at end of file diff --git a/samples/CustomPolicyProvider/Views/Home/MinimumAge.cshtml b/samples/CustomPolicyProvider/Views/Home/MinimumAge.cshtml new file mode 100644 index 0000000000..f40345dab0 --- /dev/null +++ b/samples/CustomPolicyProvider/Views/Home/MinimumAge.cshtml @@ -0,0 +1,21 @@ +@using System.Security.Claims + +@model int + +@{ + var dateOfBirth = User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth)?.Value; +} + + +

Welcome, @User.Identity.Name

+ +

Welcome to a page restricted to users @Model or older

+ +

+ You can access this page since you were born on @dateOfBirth. +

+ +
    +
  • @Html.ActionLink("Home", "Index", "Home")
  • +
  • @Html.ActionLink("Sign Out", "Signout", "Account")
  • +
diff --git a/samples/CustomPolicyProvider/appsettings.Development.json b/samples/CustomPolicyProvider/appsettings.Development.json new file mode 100644 index 0000000000..5ec73ff5b1 --- /dev/null +++ b/samples/CustomPolicyProvider/appsettings.Development.json @@ -0,0 +1,7 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information" + } + } +} diff --git a/samples/CustomPolicyProvider/appsettings.json b/samples/CustomPolicyProvider/appsettings.json new file mode 100644 index 0000000000..09cf536c1e --- /dev/null +++ b/samples/CustomPolicyProvider/appsettings.json @@ -0,0 +1,7 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning" + } + } +} diff --git a/samples/CustomPolicyProvider/readme.md b/samples/CustomPolicyProvider/readme.md new file mode 100644 index 0000000000..c88f1f8efe --- /dev/null +++ b/samples/CustomPolicyProvider/readme.md @@ -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`. \ No newline at end of file