diff --git a/security/authorization/BlazorWebAppAuthorization/Components/Pages/Home.razor b/security/authorization/BlazorWebAppAuthorization/Components/Pages/Home.razor index 55f347f4..18461e68 100644 --- a/security/authorization/BlazorWebAppAuthorization/Components/Pages/Home.razor +++ b/security/authorization/BlazorWebAppAuthorization/Components/Pages/Home.razor @@ -2,20 +2,20 @@ Home -

Roles Sample App

+

Authorization Sample App

Policy-based authorization example

Role-based authorization examples

+

Policy provider authorization example

+ + + + + + +

+ You can't see this link to the 'PassMinimumAge21PolicyWithAttribute' component because you don't satisfy + the 'MinimumAge21' policy. If you navigate to the page at '/pass-minimumage21-policy-with-attribute' in + the browser's address bar, the Access Denied page is loaded. +

+
+

Sign into the app with one of the seeded accounts. See the README file for accounts and credentials.

diff --git a/security/authorization/BlazorWebAppAuthorization/Components/Pages/PassMinimumAge21Policy.razor b/security/authorization/BlazorWebAppAuthorization/Components/Pages/PassMinimumAge21Policy.razor new file mode 100644 index 00000000..8f39f26b --- /dev/null +++ b/security/authorization/BlazorWebAppAuthorization/Components/Pages/PassMinimumAge21Policy.razor @@ -0,0 +1,18 @@ +@page "/pass-minimumage21-policy" + +

Pass 'MinimumAge21' policy (weakly-typed approach)

+ +

+ Uses an AuthorizeView component to apply the policy using the policy's name. + This approach is shown for demonstration purposes and isn't recommended for + production code. +

+ + + +

You satisfy the 'MinimumAge21' policy.

+
+ +

You don't satisfy the 'MinimumAge21' policy.

+
+
diff --git a/security/authorization/BlazorWebAppAuthorization/Components/Pages/PassMinimumAge21PolicyWithAttribute.razor b/security/authorization/BlazorWebAppAuthorization/Components/Pages/PassMinimumAge21PolicyWithAttribute.razor new file mode 100644 index 00000000..5999afda --- /dev/null +++ b/security/authorization/BlazorWebAppAuthorization/Components/Pages/PassMinimumAge21PolicyWithAttribute.razor @@ -0,0 +1,14 @@ +@page "/pass-minimumage21-policy-with-attribute" +@using BlazorWebAppAuthorization.Policies.Attributes +@attribute [MinimumAgeAuthorize(21)] + +

Pass 'MinimumAge21' policy (strongly-typed approach)

+ +

+ Applies the policy to the Razor component with a custom + [MinimumAgeAuthorize] attribute (derived from AuthorizeAttribute). + This approach is preferred for production code, as it's strongly-typed + and avoids the use of a string to set the policy and minimum age. +

+ +

You satisfy the 'MinimumAge21' policy.

diff --git a/security/authorization/BlazorWebAppAuthorization/Policies/Attributes/MinimumAgeAuthorizeAttribute.cs b/security/authorization/BlazorWebAppAuthorization/Policies/Attributes/MinimumAgeAuthorizeAttribute.cs new file mode 100644 index 00000000..3ae273d2 --- /dev/null +++ b/security/authorization/BlazorWebAppAuthorization/Policies/Attributes/MinimumAgeAuthorizeAttribute.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Authorization; + +namespace BlazorWebAppAuthorization.Policies.Attributes; + +public class MinimumAgeAuthorizeAttribute : AuthorizeAttribute +{ + private const string PolicyPrefix = "MinimumAge"; + + public MinimumAgeAuthorizeAttribute(int age) => Age = age; + + public int Age + { + get + { + if (!string.IsNullOrEmpty(Policy) && + Policy.StartsWith(PolicyPrefix, + StringComparison.OrdinalIgnoreCase) && + int.TryParse(Policy.AsSpan(PolicyPrefix.Length), out var age)) + { + return age; + } + + return default; + } + set + { + ArgumentOutOfRangeException.ThrowIfNegative(value); + Policy = $"{PolicyPrefix}{value}"; + } + } +} diff --git a/security/authorization/BlazorWebAppAuthorization/Policies/Providers/MinimumAgePolicyProvider.cs b/security/authorization/BlazorWebAppAuthorization/Policies/Providers/MinimumAgePolicyProvider.cs new file mode 100644 index 00000000..fb225a70 --- /dev/null +++ b/security/authorization/BlazorWebAppAuthorization/Policies/Providers/MinimumAgePolicyProvider.cs @@ -0,0 +1,38 @@ +using BlazorWebAppAuthorization.Policies.Requirements; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; + +namespace BlazorWebAppAuthorization.Policies.Providers; + +public class MinimumAgePolicyProvider(IOptions options) + : IAuthorizationPolicyProvider +{ + private const string PolicyPrefix = "MinimumAge"; + + private DefaultAuthorizationPolicyProvider DefaultPolicyProvider { get; } = + new(options); + + public Task GetPolicyAsync(string policyName) + { + if (policyName.StartsWith( + PolicyPrefix, StringComparison.OrdinalIgnoreCase) && + int.TryParse(policyName.AsSpan(PolicyPrefix.Length), out var age) && + age >= 0) + { + var policy = new AuthorizationPolicyBuilder( + IdentityConstants.ApplicationScheme); + policy.AddRequirements(new MinimumAgeRequirement(age)); + + return Task.FromResult(policy.Build()); + } + + return DefaultPolicyProvider.GetPolicyAsync(policyName); + } + + public Task GetDefaultPolicyAsync() => + DefaultPolicyProvider.GetDefaultPolicyAsync(); + + public Task GetFallbackPolicyAsync() => + DefaultPolicyProvider.GetFallbackPolicyAsync(); +} diff --git a/security/authorization/BlazorWebAppAuthorization/Program.cs b/security/authorization/BlazorWebAppAuthorization/Program.cs index 6873e065..278e1bf5 100644 --- a/security/authorization/BlazorWebAppAuthorization/Program.cs +++ b/security/authorization/BlazorWebAppAuthorization/Program.cs @@ -7,6 +7,7 @@ using BlazorWebAppAuthorization.Data; using BlazorWebAppAuthorization.Identity; using BlazorWebAppAuthorization.Policies.Handlers; +using BlazorWebAppAuthorization.Policies.Providers; using BlazorWebAppAuthorization.Policies.Requirements; var builder = WebApplication.CreateBuilder(args); @@ -84,6 +85,7 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddScoped(); +builder.Services.AddSingleton(); var app = builder.Build(); diff --git a/security/authorization/BlazorWebAppAuthorization/README.md b/security/authorization/BlazorWebAppAuthorization/README.md index 5410de4b..4db8cf6a 100644 --- a/security/authorization/BlazorWebAppAuthorization/README.md +++ b/security/authorization/BlazorWebAppAuthorization/README.md @@ -33,17 +33,17 @@ For more information, see the following resources: * A `Department` claim with a `Human Resources` value. * The author of a test resource with an `Id` (Guid) of `aaaabbbb-0000-cccc-1111-dddd2222eeee` (Test Document 1). * Can perform full CRUD operations on resources in the `AccessDocumentCrud` page. - * Satisfies the 'AtLeast21' policy with a `DateOfBirth` claim with a value of `2000-01-01`. + * Has a `DateOfBirth` claim with a value of `2000-01-01`, which satisfies the 'AtLeast21' and 'MinimumAge21' policies. * `harry@contoso.com` (Password: `Passw0rd!`) * The `Admin` role. * An `EmployeeNumber` claim with a value of `10`. * A `Department` claim with a `Customer Service` value. * The author of a test resource with an `Id` (Guid) of `00001111-aaaa-2222-bbbb-3333cccc4444` (Test Document 2). * Can create, read, and update resources in the `AccessDocumentCrud` page. - * Doesn't satisfy the 'AtLeast21' policy with a `DateOfBirth` claim with a value of `2008-01-01`. + * Has a `DateOfBirth` claim with a value of `2008-01-01`, which doesn't satisfy the 'AtLeast21' and 'MinimumAge21' policies. * `sarah@contoso.com` (Password: `Passw0rd!`) * The `SuperUser` role. * Doesn't have claims as an employee (`EmployeeNumber`) or for a department (`Department`). * The author of a test resource with an `Id` (Guid) of `11112222-bbbb-3333-cccc-4444dddd5555` (Test Document 3). * Can delete and read resources in the `AccessDocumentCrud` page. - * Doesn't satisfy the 'AtLeast21' policy with a `DateOfBirth` claim with a value of `2008-01-01`. + * Has a `DateOfBirth` claim with a value of `2008-01-01`, which doesn't satisfy the 'AtLeast21' and 'MinimumAge21' policies.