Filters in ASP.NET Core

WIP

What are Filters?

Filters in ASP.NET Core allow code to be run before or after specific stages in the request processing pipeline.

Filter types include Authorization, Resource, Action, Exception and Result. A pretty crude Authorization could be to check the HTTP Headers have have some expected value like an API Key.

Example filter pipeline from docs.microsoft.com

Custom Authorization Policy

The following sample code adds a simple API Key check to all requests. The complete solution is here https://github.com/carlpaton/AuthorizationDemo.

We can apply custom authorization policys using IAuthorizationPolicyProvider from Microsoft.AspNetCore.Authorization; with the following flow.

  1. Create /Authorization/Requirements/ApiKeyRequirement.cs where ApiKey is the business use case. This could be anything that is sensible for your use case. EG: AdminUser

IAuthorizationRequirement is a marker interface with no methods and the mechanism for tracing whether authorization is successful.

1
2
3
4
5
6
7
8
using Microsoft.AspNetCore.Authorization;

namespace SweetApp.Authorization.Requirements
{
public class ApiKeyRequirement : IAuthorizationRequirement
{
}
}

You can however include state in the requirements construction, the ApiKeyRequirement instance will be passed into the handler so you would then access it as requirement.Permission.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
using Microsoft.AspNetCore.Authorization;

namespace SweetApp.Authorization.Requirements
{
public string Permission { get; }

public class ApiKeyRequirement : IAuthorizationRequirement
{
public ApiKeyRequirement(string permission)
{
Permission = permission;
}
}
}
  1. Create /Authorization/Handlers/ApiKeyRequirementHandler.cs with constructor injection resources such as repository, contexts, services ect.

The flows could then be:

  • Extract data from CTR injected resources
  • Based on the result apply logic for failure; context.Fail(); and success; context.Succeed(requirement);

My flow was real simple and just checks for the ApiKey in the given RequestContext.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
namespace SweetApp.Authorization.Handlers
{
public class ApiKeyRequirementHandler : AuthorizationHandler<ApiKeyRequirement>
{
private static IGeneralRequestContext _generalRequestContext;

public ApiKeyRequirementHandler(IGeneralRequestContext generalRequestContext)
{
_generalRequestContext = generalRequestContext;
}

protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ApiKeyRequirement requirement)
{
var expectedGuid = Guid.Parse("cdef007a-5d8e-496d-b123-c9055d157d5f");

if (_generalRequestContext.ApiKey.Equals(expectedGuid))
{
context.Succeed(requirement);
return Task.CompletedTask;
}

context.Fail();
return Task.CompletedTask;
}
}
}
  1. Create /Authorization/AuthorizationRequirementMapper.cs. This will hold a collection of key/values being string and IAuthorizationRequirement and methods to access these. Policys is just a static class with static string members.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
namespace SweetApp.Authorization
{
public class AuthorizationRequirementMapper : IAuthorizationRequirementMapper
{
///<inheritdoc/>
public IDictionary<string, IAuthorizationRequirement> GetAuthorizationRequirementMappings()
{
return new Dictionary<string, IAuthorizationRequirement>
{
{ Policys.FallbackRequirementPolicy, new FallbackRequirement() },
{ Policys.RequireHeaderKeyPolicy, new ApiKeyRequirement() },
{ Policys.DefaultPolicy, new DefaultPolicyRequirement() },
};
}

public IAuthorizationRequirement GetDefaultPolicy()
{
return GetAuthorizationRequirementMappings()[Policys.DefaultPolicy];
}

///<inheritdoc/>
public IAuthorizationRequirement GetFallbackPolicy()
{
return GetAuthorizationRequirementMappings()[Policys.FallbackRequirementPolicy];
}
}
}
  1. Create /Authorization/AuthorizationPolicyProvider.cs then inherit and implement IAuthorizationPolicyProvider. This will give you GetDefaultPolicyAsync(), GetFallbackPolicyAsync() and GetPolicyAsync(policyName).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
namespace SweetApp.Authorization
{
public class AuthorizationPolicyProvider : IAuthorizationPolicyProvider
{
readonly IAuthorizationRequirementMapper _authorizationRequirementMapper;

public AuthorizationPolicyProvider(
IAuthorizationRequirementMapper authorizationRequirementMapper)
{
_authorizationRequirementMapper = authorizationRequirementMapper;
}

/// <summary>
/// GetDefaultPolicyAsync returns the default authorization policy (the policy used for [Authorize] attributes without a policy specified).
/// </summary>
/// <returns></returns>
public Task<AuthorizationPolicy> GetDefaultPolicyAsync()
{
var requirement = _authorizationRequirementMapper
.GetDefaultPolicy();

return GetPolicy(requirement);
}

/// <summary>
/// GetFallbackPolicyAsync returns the fallback authorization policy (the policy used by the Authorization Middleware when no policy is specified).
/// </summary>
/// <returns></returns>
public Task<AuthorizationPolicy> GetFallbackPolicyAsync()
{
var requirement = _authorizationRequirementMapper
.GetFallbackPolicy();

return GetPolicy(requirement);
}

/// <summary>
/// GetPolicyAsync returns an authorization policy for a given name. Example `[Authorize(Policy = "RequireHeaderKeyPolicy")]`
/// </summary>
/// <param name="policyName"></param>
/// <returns></returns>
public Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
{
if (_authorizationRequirementMapper
.GetAuthorizationRequirementMappings()
.TryGetValue(policyName, out IAuthorizationRequirement requirement))
{
return GetPolicy(requirement);
}

return GetDefaultPolicyAsync();
}

private Task<AuthorizationPolicy> GetPolicy(IAuthorizationRequirement requirement)
{
var policy = new AuthorizationPolicyBuilder()
.AddRequirements(requirement)
.Build();

return Task.FromResult(policy);
}
}
}
  1. Add the authorization mapper, policy provider and handlers to the application pipeline in Startup.cs
1
2
3
4
5
6
services.AddSingleton<IAuthorizationRequirementMapper, AuthorizationRequirementMapper>();
services.AddSingleton<IAuthorizationPolicyProvider, AuthorizationPolicyProvider>();

services.AddScoped<IAuthorizationHandler, FallbackRequirementHandler>();
services.AddScoped<IAuthorizationHandler, ApiKeyRequirementHandler>();
services.AddScoped<IAuthorizationHandler, DefaultPolicyRequirementHandler>();
  1. So now the application has 3 authorization options

5.1 GetFallbackPolicyAsync The policy run for all end points with no authorization annotation. The value could be that it now replaces [Authorize] attribute on all controllers. Note that if you do this, for end points that dont require authorization checks you now need to add the [AllowAnonymous] annotation. This would be FallbackRequirementHandler

5.2 GetDefaultPolicyAsync The policy run for end points with the annotation [Authorize].

5.3 GetPolicyAsync The policy run for end points with the policy name specified in the annotation [Authorize(Policy = "RequireHeaderKeyPolicy")].

References