Skip to main content

RA.Utilities.Authorization

NuGet version NuGet Downloads Codecov Publish NuGet GitHub license

RA.Utilities.Authorization provides a strongly-typed, injectable IUserContext service that wraps HttpContext.User so you never hand-parse claims, role-check with magic strings, or inject IHttpContextAccessor into your business logic. A couple of registrations in Program.cs and you have typed access to user identity everywhere.

🎯 Purpose​

In any ASP.NET Core application with authentication, you constantly need the current user's ID, email, roles, and custom claims. The built-in approach — injecting IHttpContextAccessor and manually navigating HttpContext.User.Claims — is verbose, error-prone, and hard to test.

This package replaces all of that with a single injectable service:

builder.Services.AddHttpContextAccessor();
builder.Services.AddUserContext();

From any controller, Minimal API endpoint, or service, you write:

app.MapGet("/me", (IUserContext user) => new { user.UserId, user.Name, user.Email });

The main benefits are:

  • Simplified Access: No more IHttpContextAccessor in your controllers and services. Just inject IUserContext.
  • Strongly-Typed: Get the user's ID as a Guid via UserId, or as a string via Id. Email, Name, roles, and custom claims are all accessible through simple properties and methods.
  • Testable: Easily mock IUserContext in your unit tests to simulate different authenticated users and scenarios.
  • Reduced Boilerplate: Drastically cuts down on repetitive code for accessing user claims.
  • OIDC-aware scope checking: HasScope("api.read") correctly handles space-separated scopes per the OAuth 2.0 standard.

✨ Key Features​

  • One-call registration — AddUserContext() registers IUserContext as scoped (pair it with AddHttpContextAccessor() — see below)
  • Typed user ID — Guid UserId (parsed from NameIdentifier) and string? Id (raw)
  • Common claim properties — Name, Email, IsAuthenticated, IsInRole(string)
  • Claim helpers — GetClaimValue(string) and GetClaimValues(string) for custom claims
  • OIDC scope support — HasScope(string) splits on spaces per the OAuth 2.0 spec
  • Extension methods on ClaimsPrincipal — GetUserId(), HasClaim(type, value), HasScope(value)

🛠️ Getting Started​

Prerequisites​

  • .NET 10.0 or later
  • An ASP.NET Core application with authentication configured (JWT Bearer, cookies, etc.)

Installation​

.NET CLI

dotnet add package RA.Utilities.Authorization

Package Manager Console

Install-Package RA.Utilities.Authorization

PackageReference (csproj)

<PackageReference Include="RA.Utilities.Authorization" Version="10.0.3" />

Hello World​

using RA.Utilities.Authorization.Extensions;
using RA.Utilities.Authorization;

var builder = WebApplication.CreateBuilder(args);

// IHttpContextAccessor is not registered by default in ASP.NET Core
builder.Services.AddHttpContextAccessor();
builder.Services.AddUserContext();

var app = builder.Build();

app.MapGet("/me", (IUserContext user) => new
{
IsAuthenticated = user.IsAuthenticated,
UserId = user.UserId,
Name = user.Name,
Email = user.Email
});

app.Run();
v10.0.3

AddUserContext() registers only IUserContext. As of v10.0.3 it no longer registers IHttpContextAccessor as a side effect — both calls above are required.


🧠 Core Concepts​

How UserContext works​

IUserContext is implemented by the internal UserContext class, a scoped service that wraps ClaimsPrincipal from the current HttpContext:

HTTP Request
│
▼
IHttpContextAccessor.HttpContext.User (ClaimsPrincipal)
│
▼
UserContext constructor ─── stores ClaimsPrincipal? _user
│
▼
Your controller/service injects IUserContext
│
▼
_user.GetUserId() → Guid (via ClaimsPrincipalExtensions)
_user.FindFirst(...) → string? (Id, Name, Email, GetClaimValue)
_user.FindAll(...) → IEnumerable<> (GetClaimValues)
_user.IsInRole(...) → bool
_user.HasClaim(...) → bool (HasClaim, HasScope)

UserContext is registered as scoped — its lifetime aligns with the HTTP request. Its IHttpContextAccessor dependency is a singleton backed by AsyncLocal, which resolves to the current request's HttpContext — register it yourself with builder.Services.AddHttpContextAccessor(), since AddUserContext() does not do this for you.

Extension methods vs IUserContext​

Use caseApproach
Already have a ClaimsPrincipal (e.g., in middleware)user.GetUserId(), user.HasClaim(type, value)
In a controller or service (DI-managed)Inject IUserContext and use .UserId, .Name, .HasClaim(type, value)

The extension methods are the building blocks; IUserContext is the ergonomic wrapper.

Terminology​

  • NameIdentifier claim: ClaimTypes.NameIdentifier (http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier). The JWT middleware maps the sub claim here automatically.
  • Scope claim: OAuth 2.0 / OIDC standard. Scopes are space-separated in a single "scope" claim value. HasScope handles both this standard format and non-standard individual entries.

📖 How-To Guides​

Access the current user in a controller​

[ApiController]
[Route("api/[controller]")]
[Authorize]
public class OrdersController : ControllerBase
{
private readonly IUserContext _user;

public OrdersController(IUserContext user) => _user = user;

[HttpGet]
public IActionResult GetOrders()
{
if (!_user.IsAuthenticated)
return Unauthorized();

Guid ownerId = _user.UserId;
var orders = _orderService.GetByOwner(ownerId);
return Ok(orders);
}
}

Access the current user in a Minimal API endpoint​

app.MapGet("/profile", (IUserContext user) =>
{
return new
{
user.UserId,
user.Name,
user.Email,
IsAdmin = user.IsInRole("Admin")
};
}).RequireAuthorization();

Check OIDC scopes​

app.MapGet("/admin", (IUserContext user) =>
{
if (!user.HasScope("admin.access"))
return Results.Forbid();

return Results.Ok(new { message = "Welcome, admin" });
}).RequireAuthorization();

Read a custom claim​

public class TenantService
{
private readonly IUserContext _user;

public TenantService(IUserContext user) => _user = user;

public string? GetTenantId()
{
return _user.GetClaimValue("tenant_id");
}
}

Use extension methods directly on ClaimsPrincipal​

public static class AuthorizationMiddleware
{
public static Task HandleAsync(HttpContext context, RequestDelegate next)
{
var user = context.User;

if (user.Identity?.IsAuthenticated != true)
return next(context);

Guid userId = user.GetUserId();

if (user.HasClaim("permission", "CanManageUsers"))
{
// grant access
}

return next(context);
}
}

Mock IUserContext in unit tests​

Because IUserContext is an interface, mocking it requires no HttpContext at all:

var mockUser = new Mock<IUserContext>();

mockUser.SetupGet(u => u.IsAuthenticated).Returns(true);
mockUser.SetupGet(u => u.UserId).Returns(Guid.NewGuid());
mockUser.SetupGet(u => u.Email).Returns("test@example.com");
mockUser.Setup(u => u.IsInRole("Admin")).Returns(true);

var user = mockUser.Object;

Assert.True(user.IsAuthenticated);
Assert.NotEqual(Guid.Empty, user.UserId);
Assert.Equal("test@example.com", user.Email);
Assert.True(user.IsInRole("Admin"));

⚙️ Configuration​

This package has no configuration options. There is no appsettings.json section, no options class, and no additional setup beyond AddHttpContextAccessor() and AddUserContext(). The service reads directly from IHttpContextAccessor.HttpContext.User at injection time.


✨ Available Components​

Service Interfaces​

InterfaceRegistrationPurpose
IUserContextScoped (via AddUserContext())Strongly-typed access to the current user's claims, implemented internally by UserContext

Extension Methods​

MethodTypePurpose
AddUserContext()IServiceCollectionRegisters IUserContext as scoped (implemented by UserContext) — does not register IHttpContextAccessor
GetUserId()ClaimsPrincipal?Parses NameIdentifier claim into Guid
HasClaim(type, value)ClaimsPrincipal?Checks for a claim with the given type and value
HasScope(value)ClaimsPrincipal?Checks for an OIDC scope (space-separated or individual)


🙌 Contributing & Support​

Found a bug or have a suggestion?​

Contributing​

Contributions are welcome! Please follow the standard workflow:

  1. Fork the RA.Utilities repository
  2. Create a branch from main with a descriptive name (e.g., feature/add-claim-transforms)
  3. Make your changes — follow the existing coding style and add XML documentation for new public APIs
  4. Submit a pull request to the main branch with a clear description
ResourceURL
NuGetRA.Utilities.Authorization
SourceGitHub — RedonAlla/RA.Utilities
LicenseMIT
Build StatusGitHub Actions