RA.Utilities.Authorization
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
IHttpContextAccessorin your controllers and services. Just injectIUserContext. - Strongly-Typed: Get the user's ID as a
GuidviaUserId, or as a string viaId. Email, Name, roles, and custom claims are all accessible through simple properties and methods. - Testable: Easily mock
IUserContextin 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()registersIUserContextas scoped (pair it withAddHttpContextAccessor()— see below) - Typed user ID —
Guid UserId(parsed from NameIdentifier) andstring? Id(raw) - Common claim properties —
Name,Email,IsAuthenticated,IsInRole(string) - Claim helpers —
GetClaimValue(string)andGetClaimValues(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();
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 case | Approach |
|---|---|
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 thesubclaim here automatically. - Scope claim: OAuth 2.0 / OIDC standard. Scopes are space-separated in a single
"scope"claim value.HasScopehandles 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
| Interface | Registration | Purpose |
|---|---|---|
IUserContext | Scoped (via AddUserContext()) | Strongly-typed access to the current user's claims, implemented internally by UserContext |
Extension Methods
| Method | Type | Purpose |
|---|---|---|
AddUserContext() | IServiceCollection | Registers 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?
- GitHub Issues: Report a bug or request a feature
- Discussions: Ask questions or start a discussion
Contributing
Contributions are welcome! Please follow the standard workflow:
- Fork the RA.Utilities repository
- Create a branch from
mainwith a descriptive name (e.g.,feature/add-claim-transforms) - Make your changes — follow the existing coding style and add XML documentation for new public APIs
- Submit a pull request to the
mainbranch with a clear description
Links
| Resource | URL |
|---|---|
| NuGet | RA.Utilities.Authorization |
| Source | GitHub — RedonAlla/RA.Utilities |
| License | MIT |
| Build Status | GitHub Actions |