ASP.NET
Core Identity
Server 4 Policy based Authorization Handler
services.AddAuthorization(options => options.AddPolicy("SalesDepartmentOrAdminPolicy", policy => policy.RequireAssertion(
context => context.User.Claims.Any(
x => (x.Type.Equals(CustomClaimTypes.Department) && x.Value.Equals("Sales")) ||
(x.Type.Equals(ClaimTypes.Role) && x.Value.Equals("admin"))
))));
Furthermore,
if Client would like to do advanced authorization-checking by itself, we can
add one or more custom Authorization
Handlers and put the validation logics on them.
Custom Authorization Handler is often used when the user’s
required information is kept on the client side not on the JWT, so that we
can reject the request (response 401 Unauthorized) depends on the information
not on JWT even if the JWT is validated ok.
|
l Check the user’s email domain is expected
l Check the user’s name is certain user
PS. Notice
we will use the claim (email, user
name) on JWT in the following sample
codes to validate the user in the 2 Authorization
Handlers.
However
this can be done by using AuthorizationPolicyBuilder
as well, so the purpose of this tutorial is to learn how to use them :P
▋Docker 18.05.0-ce
▋ASP.NET Core 3.0.100
▋IdentityServer4 3.0.1
▋IdentityModel 3.10.10
The
source code is on my Github.
▋Install Microsoft.AspNetCore.Authorization
The
related namespace:
using Microsoft.AspNetCore.Authorization;
▋Authorization Requirement
Before
creating Authorzation Handler, we have to create Authorization Requirement that
specified the requirement to meet.
In other
words, the Authorization Handler will be succeed only if the user’s information
suits the requirement.
Here are
the Authorization Requirements:
▋EmailDomainRequirement.cs
public class EmailDomainRequirement : IAuthorizationRequirement
{
/// <summary>
/// The domain name
/// </summary>
public string Domain { get; set; }
public EmailDomainRequirement(string domain)
{
this.Domain = domain;
}
}
▋UserNameRequirement.cs
public class UserNameRequirement : IAuthorizationRequirement
{
/// <summary>
/// User's name
/// </summary>
public string UserName { get; set; }
public UserNameRequirement(string username)
{
this.UserName = username;
}
}
▋Authorization Handler
Now we
can create Authorization Handler with the Authorization Requirement as the generic
type like this,
using Microsoft.AspNetCore.Authorization;
public class CusomAuthorizationHandler : AuthorizationHandler<CustomRequirement>
{
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, CustomRequirement requirement)
{
// … Validation
logic here
}
Notice
that we can use multiple Authorization
Handlers on a single authorization policy, they will be executed one by one just like as a chain,
no matter one of them validates OK or NG. (We can disable the option that will
be mentioned at the later part of this article!)
Since the
Authorization Handlers are validate one by one, we have to check the previous
validation result at the next one!
Property
|
Type
|
Description
|
Default value
|
|
Boolean
|
Flag indicating whether the current
authorization processing has succeeded.
|
False
|
|
Boolean
|
Flag indicating whether the current
authorization processing has failed.
|
False
|
And the 2
methods to set the value of above property:
Mthod
|
Return type
|
Description
|
|
Boolean
|
Called to mark the specified
requirement as being successfully evaluated.
|
|
Boolean
|
Called to indicate HasSucceeded
will never return true, even if all requirements are met.
|
So what we
have to do in an Authorization Handler:
l To check if the user’s JWT is valid or not, use context.User
Identity’s IsAuthenticated
flag instead of context.HasFailed or context.HasSucceeded.
That is because the Default JWT Authorization handler DOES NOT set values on context.HasFailed
and context.HasSucceeded.
l Check context.HasFailed if the authorization process
had been already failed and there is no need to do the current validation.
l Call context.Succeed(requirement) when current
validation is OK or call context.Fail() when current validation fails.
using Microsoft.AspNetCore.Authorization;
public class CusomAuthorizationHandler : AuthorizationHandler<CustomRequirement>
{
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, CustomRequirement requirement)
{
ClaimsPrincipal userClaim = context.User;
// 1. Check
if the Default Authenticate was failed (i.e. JWT is invalid)
if (context.User.Identity == null || !context.User.Identities.Any(i => i.IsAuthenticated))
{
context.Fail();
return;
}
// 2. Check
if the previous Handler had been failed
if (context.HasFailed)
{
context.Fail();
return;
}
// 3.
Do the custom validation by comparing the information from AuthorizationHandlerContext
to Requirement
bool isMeetRequirement = true; // Update
the validation logic here …
if (isMeetRequirement)
{
context.Succeed(requirement);
return;
}
else
context.Fail();
await Task.CompletedTask;
}
Now we
know how to create an Authorization Handler, lets create one by validating the email’s
domain name.
▋EmailDomainAuthHandler.cs
using Microsoft.AspNetCore.Authorization;
public class EmailDomainAuthHandler : AuthorizationHandler<EmailDomainRequirement>
{
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, EmailDomainRequirement requirement)
{
ClaimsPrincipal userClaim = context.User;
if (context.HasFailed || userClaim.Identity == null || !userClaim.Identities.Any(i => i.IsAuthenticated))
{
context.Fail();
this.logger.LogWarning($"Skip validating with {nameof(EmailDomainAuthHandler)} cus the authorization process had been failed!");
return;
}
var userEmailClaim = userClaim.Claims.FirstOrDefault(c => c.Type.Equals(ClaimTypes.Email));
if (userEmailClaim != null)
{
// Get domain name from email
var address = new MailAddress(userEmailClaim.Value);
string userDomain = address.Host; // e.q. google.com
// Verify domain
if (userDomain.Equals(requirement.Domain))
{
context.Succeed(requirement);
return;
}
}
context.Fail();
await Task.CompletedTask;
}
}
The code
of the other Authorization Handler: UserNameAuthHandler.cs is here.
▋Inject Custom Authorization Handler and create a new
policy for API
To use
the custom Authorization Handler, we have to inject them on Startup: ConfigureServices.
Then we
can create a new policy with specified Authorization Requirement(s).
Notice that the injection order of
Authorization Handlers will be the order for invoking them!
▋Startup.cs
The below policy shows that the user
must be with email domain as “fake.com” and the user name must be “jblin”.
public void ConfigureServices(IServiceCollection services)
{
// … skip
#region Enable custom Authorization Handlers (The registration order matters!)
services.AddSingleton<IAuthorizationHandler, EmailDomainAuthHandler>();
services.AddSingleton<IAuthorizationHandler, UserNameAuthHandler>();
services.AddAuthorization(options =>
{
var emailDomainRequirement = new EmailDomainRequirement("fake.com");
var userNameRequirement = new UserNameRequirement("jblin");
options.InvokeHandlersAfterFailure = false; // Default: true
options.AddPolicy("DoaminAndUsernamePolicy", policy =>
policy.AddRequirements(emailDomainRequirement, userNameRequirement));
});
#endregion
}
Now we
can use this new policy to secure our API!
[HttpGet]
[Route("AdminOrUserWithCusomHandler/Get")]
[Authorize(Policy = "DoaminAndUsernamePolicy")]
public ActionResult<string> AdminOrUserWithCusomHandlerGet()
{
return "Yes, only an Admin or User can access this API!";
}
▋(Optional) Set authentication handlers should not be
invoked after a failure.
services.AddAuthorization(options =>
{
var emailDomainRequirement = new EmailDomainRequirement("fake.com");
var userNameRequirement = new UserNameRequirement("jblin");
options.InvokeHandlersAfterFailure = false; // Default: true
options.AddPolicy("DoaminAndUsernamePolicy", policy =>
policy.AddRequirements(emailDomainRequirement, userNameRequirement));
});
So the
chain will be stopped when one of the custom Authorization Handler fails!
▋Demo
▋Scenario 1.
Valid JWT/Email domain/User name
Response 200:
Logs:
▋Scenario 2.
Valid JWT/Email domain but invalid User name
Response 403:
Logs:
▋Scenario 3.
Valid JWT/User name but invalid Email domain
Response
403:
Logs:
▋Scenario 3.
Valid JWT/User name but invalid Email domain with InvokeHandlersAfterFailure=false
Logs:
Response
403 as the previous scenario but the logs did not show
“Skip validating with UserNmaeAuthHandler cus the authorization process had
been failed!”
沒有留言:
張貼留言