Protecting endpoints with a custom authorization policy

At the moment, any authenticated user can update or delete questions. We are going to implement and use a custom authorization policy and use it to enforce that only the author of the question can do these operations. Let's carry out the following steps:

  1. In the Startup class, let's add the following using statements:
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Authorization;
using QandA.Authorization;

Note that the reference to the QandA.Authorization namespace doesn't exist yet. We'll implement this in a later step.

  1. We'll need to eventually call an Auth0 web service, so let's make the HTTP client available in the ConfigureServices method. Let's also add an authorization policy called MustBeQuestionAuthor:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddHttpClient();
services.AddAuthorization(options =>
options.AddPolicy("MustBeQuestionAuthor", policy =>
policy.Requirements
.Add(new MustBeQuestionAuthorRequirement())));

}

The authorization policy has its requirements defined in a class called MustBeQuestionAuthorRequirement, which we'll implement in a later step.

  1. We also need to have a handler for the requirement, so let's register this for dependency injection:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddHttpClient();
services.AddAuthorization(...);
services.AddScoped<
IAuthorizationHandler,
MustBeQuestionAuthorHandler>();

}

So, the handler for MustBeQuestionAuthorRequirement will be implemented in a class called MustBeQuestionAuthorHandler.

  1. Our MustBeQuestionAuthorHandler class will need access to the HTTP requests to find out the question that is being requested. We need to register HttpContextAccessor for dependency injection to get access to the HTTP request information in a class. Let's do this now:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddHttpClient();
services.AddAuthorization(...);
services.AddScoped<
IAuthorizationHandler,
MustBeQuestionAuthorHandler>();
services.AddSingleton<
IHttpContextAccessor,
HttpContextAccessor>();

}
  1. We are going to create the MustBeQuestionAuthorRequirement class now. Let's create a folder called Authorization in the root of the project and then create a class called MustBeQuestionAuthorRequirement containing the following:
using Microsoft.AspNetCore.Authorization;

namespace QandA.Authorization
{
public class MustBeQuestionAuthorRequirement :
IAuthorizationRequirement
{
public MustBeQuestionAuthorRequirement()
{
}
}
}
  1. Next, we'll create the handler class for this requirement. Create a class called MustBeQuestionAuthorHandler with the following content:
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using QandA.Data;

namespace QandA.Authorization
{
public class MustBeQuestionAuthorHandler :
AuthorizationHandler<MustBeQuestionAuthorRequirement>
{
private readonly IDataRepository _dataRepository;
private readonly IHttpContextAccessor _httpContextAccessor;

public MustBeQuestionAuthorHandler(
IDataRepository dataRepository,
IHttpContextAccessor httpContextAccessor)
{
_dataRepository = dataRepository;
_httpContextAccessor = httpContextAccessor;
}

protected async override Task
HandleRequirementAsync(
AuthorizationHandlerContext context,
MustBeQuestionAuthorRequirement requirement)
{
// TODO - check that the user is authenticated
// TODO - get the question id from the request
// TODO - get the user id from the name identifier claim
// TODO - get the question from the data repository
// TODO - if the question can't be found go to the next piece of middleware
// TODO - return failure if the user id in the question from the data repository is different to the user id in the request
// TODO - return success if we manage to get here
}
}
}

This inherits from the AuthorizationHandler class, which takes in the requirement it is handling as a generic parameter. We have injected the data repository and the HTTP context into the class.

  1. We now need to implement the HandleRequirementAsync method. The first task is to check that the user is authenticated:
protected async override Task 
HandleRequirementAsync(
AuthorizationHandlerContext context,
MustBeQuestionAuthorRequirement requirement)
{
if (!context.User.Identity.IsAuthenticated)
{
context.Fail();
return;
}

// TODO - get the question id from the request
// TODO - get the user id from the name identifier claim
// TODO - get the question from the data repository
// TODO - if the question can't be found go to the next piece of middleware
// TODO - return failure if the user id in the question from the data repository is different to the user id in the request
// TODO - return success if we manage to get here
}

The context parameter in the method contains information about the user's identity in an Identity property. We use the IsAuthenticated property with the Identity object to determine whether the user is authenticated or not. We call the Fail method on the context argument to tell it that the requirement failed.

  1. Next, we need to get questionId from the request path:
protected async override Task 
HandleRequirementAsync(
AuthorizationHandlerContext context,
MustBeQuestionAuthorRequirement requirement)
{
if (!context.User.Identity.IsAuthenticated)
{
context.Fail();
return;
}

var questionId =
_httpContextAccessor.HttpContext.Request
.RouteValues["questionId"];

int questionIdAsInt = Convert.ToInt32(questionId);

// TODO - get the user id from the name identifier claim
// TODO - get the question from the data repository
// TODO - if the question can't be found go to the next piece of middleware
// TODO - return failure if the user id in the question from the data repository is different to the user id in the request
// TODO - return success if we manage to get here
}

We use the RouteValues dictionary within the HTTP context request to get access to this.

  1. Next, we need to get userId from the user's identity claims:
protected async override Task 
HandleRequirementAsync(
AuthorizationHandlerContext context,
MustBeQuestionAuthorRequirement requirement)
{
...
var questionId =
_httpContextAccessor.HttpContext.Request
.RouteValues["questionId"];
int questionIdAsInt = Convert.ToInt32(questionId);

var userId =
context.User.FindFirst(ClaimTypes.NameIdentifier).Value;


// TODO - get the question from the data repository
// TODO - if the question can't be found go to the next piece of middleware
// TODO - return failure if the user id in the question from the data repository is different to the user id in the request
// TODO - return success if we manage to get here
}

userId is stored in the name identifier claim.

A claim is information about a user from a trusted source. A claim represents what the subject is, not what the subject can do. The ASP.NET Core authentication middleware automatically puts userId in a name identifier claim for us.

We have used the FindFirst method on the User object from the context parameter to get the value of the name identifier claim. The User object is populated with the claims by the authentication middleware earlier in the request pipeline after it has read the access token.

  1. We can now get the question from the data repository. If the question isn't found, we want to pass the requirement because we want to return HTTP status code 404 (not found) rather than 401 (unauthorized). The action method in the controller will then be able to execute and return the 404 HTTP status code:
protected async override Task 
HandleRequirementAsync(
AuthorizationHandlerContext context,
MustBeQuestionAuthorRequirement requirement)
{
...
var userId =
context.User.FindFirst(ClaimTypes.NameIdentifier).Value;

var question =
await _dataRepository.GetQuestion(questionIdAsInt);

if (question == null)
{
// let it through so the controller can return a 404
context.Succeed(requirement);
return;
}

// TODO - return failure if the user id in the question from the data repository is different to the user id in the request
// TODO - return success if we manage to get here
}
  1. We can then check that userId in the request matches the question in the database and return Fail if not:
protected async override Task 
HandleRequirementAsync(
AuthorizationHandlerContext context,
MustBeQuestionAuthorRequirement requirement)
{
...

var question =
await _dataRepository.GetQuestion(questionIdAsInt);
if (question == null)
{
// let it through so the controller can return a 404
context.Succeed(requirement);
return;
}

if (question.UserId != userId)
{
context.Fail();
return;
}

context.Succeed(requirement);
}
  1. The final task is to add the policy we have just created to the Authorize attribute on the relevant action methods in the question controller:
[Authorize(Policy = "MustBeQuestionAuthor")]
[HttpPut("{questionId}")]
public ... PutQuestion(int questionId, QuestionPutRequest questionPutRequest)
...

[Authorize(Policy = "MustBeQuestionAuthor")]
[HttpDelete("{questionId}")]
public ... DeleteQuestion(int questionId)
...

We have now applied our authorization policy to updating and deleting a question.

Unfortunately, we can't use the test access token that Auth0 gives us to try this out but we will circle back to this and confirm that it works in Chapter 12, Interacting with RESTful APIs.

Custom authorization policies give us lots of flexibility and power to implement complex authorization rules. As we have just experienced in our example, a single policy can be implemented centrally and used on different action methods. 

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.20.221.178