| Author: Jayakumar Srinivasan | Date: 02-Dec-2024 |
Introduction
In modern web applications, securing APIs is a critical aspect of development. One common approach is to use middleware to handle authorization, ensuring that only authenticated and authorized users can access certain endpoints. In this article, we will explore how to create OData services using .NET 9.0 and implement a custom authorization middleware that validates a security key passed in the request header. We will also show how to return custom error messages when authorization fails.
Problem Description
While there are numerous examples of implementing authentication and authorization in .NET, there is a lack of clear examples that focus solely on authorization with custom exception messages when authorization fails. This can be particularly challenging when working with OData services, where the need for fine-grained control over data access is paramount. Our goal is to fill this gap by providing a comprehensive guide on how to achieve this using .NET 9.0.
Proposed Solution
To address this problem, we will create a simple OData service that exposes a list of products. We will implement a custom authorization middleware that checks for a security key in the request header and returns a custom error message if the key is missing or invalid. Below is the detailed implementation of the solution.
Project Structure
I have created the following project structure for this article, this will help you in understanding the namespaces and foldere structure in the solution.

Model Class
First, we define a simple Product model class:
namespace CustomAuthenticationDemo001.Model
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
}
Controller Class
Next, we create a ProductsController class that inherits from ODataController and uses the [Authorize] attribute to enforce the custom authorization policy:
namespace CustomAuthenticationDemo001.Controller
{
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OData.Routing.Controllers;
using Microsoft.AspNetCore.OData.Query;
using System.Collections.Generic;
using System.Linq;
using CustomAuthenticationDemo001.Model;
[Authorize(Policy = "SecurityKeyPolicy")]
public class ProductsController : ODataController
{
private static readonly List<Product> Products = new List<Product>
{
new Product { Id = 1, Name = "Product 1", Price = 10.0m },
new Product { Id = 2, Name = "Product 2", Price = 20.0m },
new Product { Id = 3, Name = "Product 3", Price = 30.0m },
new Product { Id = 4, Name = "Product 3", Price = 40.0m },
new Product { Id = 5, Name = "Product 3", Price = 50.0m },
new Product { Id = 6, Name = "Product 3", Price = 60.0m },
new Product { Id = 7, Name = "Product 3", Price = 70.0m },
new Product { Id = 8, Name = "Product 3", Price = 80.0m },
new Product { Id = 9, Name = "Product 3", Price = 90.0m },
new Product { Id = 10, Name = "Product 3", Price = 100.0m }
};
[EnableQuery]
public IActionResult Get()
{
return Ok(Products.AsQueryable());
}
[EnableQuery]
public IActionResult Get(int key)
{
var product = Products.FirstOrDefault(p => p.Id == key);
if (product == null)
{
return NotFound();
}
return Ok(product);
}
}
}
Authorization Handler Class
We then create a custom authorization middleware result handler that returns a custom error message when authorization fails:
namespace CustomAuthenticationDemo001.Middleware.Authentication.Handler
{
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Policy;
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;
public class CustomAuthorizationMiddlewareResultHandler : IAuthorizationMiddlewareResultHandler
{
public async Task HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult)
{
if (authorizeResult.Challenged)
{
context.Response.StatusCode = StatusCodes.Status404NotFound;
var failureMessage = context.Items["AuthorizationFailureMessage"] as string ?? "Authorization failed";
await context.Response.WriteAsync(failureMessage);
}
else
{
await next(context);
}
}
}
}
Authorization Requirement Implementation
We define a custom authorization requirement and handler that checks for the presence of a security key in the request header:
namespace CustomAuthenticationDemo001.Middleware.Authentication.Handler
{
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;
public class SecurityKeyRequirement : IAuthorizationRequirement { }
public class SecurityKeyHandler : AuthorizationHandler<SecurityKeyRequirement>
{
private readonly IHttpContextAccessor _httpContextAccessor;
public SecurityKeyHandler(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SecurityKeyRequirement requirement)
{
string authMessage = string.Empty;
var httpContext = _httpContextAccessor.HttpContext;
if (httpContext.Request.Headers.TryGetValue("SecurityKey", out var securityKey))
{
if (!string.IsNullOrEmpty(securityKey))
{
if (securityKey.Equals("Test"))
{
context.Succeed(requirement);
return Task.CompletedTask;
}
else
{
httpContext.Items["AuthorizationFailureMessage"] = "The security key passed in not authorized";
context.Fail();
return Task.CompletedTask;
}
}
}
// Set a custom failure message
httpContext.Items["AuthorizationFailureMessage"] = "SecurityKey passed is either null or empty";
context.Fail();
return Task.CompletedTask;
}
}
}
Program.cs
Finally, we configure the services and middleware in the Program.cs file
using CustomAuthenticationDemo001.Middleware.Authentication.Handler;
using CustomAuthenticationDemo001.Model;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.OData;
using Microsoft.OData.ModelBuilder;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers().AddOData(options =>
{
var odataBuilder = new ODataConventionModelBuilder();
odataBuilder.EntitySet<Product>("Products");
options.AddRouteComponents("odata", odataBuilder.GetEdmModel())
.Select().Filter().Expand().OrderBy().Count().SetMaxTop(100);
});
builder.Services.AddHttpContextAccessor();
builder.Services.AddSingleton<IAuthorizationHandler, SecurityKeyHandler>();
builder.Services.AddSingleton<IAuthorizationMiddlewareResultHandler, CustomAuthorizationMiddlewareResultHandler>();
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("SecurityKeyPolicy", policy =>
policy.Requirements.Add(new SecurityKeyRequirement()));
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
app.Run();
Benefits
Implementing custom authorization middleware with a custom error message provides several benefits:
- Enhanced Security: By validating a security key in the request header, we ensure that only authorized users can access the OData services.
- Custom Error Handling: Returning custom error messages helps users understand why their request was denied, improving the overall user experience.
- Flexibility: The custom authorization middleware can be easily extended to include additional validation logic or integrate with other authentication mechanisms.