Skip to content

Claims and Identity

When an HTTP request arrives at the API, the system needs to answer a simple question: who sent it?

This page explains how the codebase turns raw HTTP context into typed user properties that handlers and services can use. If you are new to claims-based identity in .NET, start with the key concepts glossary on the cross-cutting overview page.


The interfaces

Two domain interfaces define what the system needs from user identity.

ICurrentUserService

Answers identity questions: who is this user?

Petfolio.Service.Domain/Common/ICurrentUserService.cs
1
2
3
4
5
6
public interface ICurrentUserService
{
    public string? UserId { get; }
    public string? Email { get; }
    public bool IsAuthenticated { get; }
}
Property Type Source Claim Purpose
UserId string? NameIdentifier (sub) Identifies which user is making the request
Email string? Email Display and communication
IsAuthenticated bool Identity.IsAuthenticated Whether the request has a valid authenticated user

Handlers inject this when they need to know who is performing an action (e.g. logging, audit trails, authorisation checks).

IClaimsPrincipalAccessor

Provides the raw ClaimsPrincipal to CurrentUserService. This abstraction exists so the infrastructure layer does not depend on HttpContext directly.

Petfolio.Service.Domain/Common/IClaimsPrincipalAccessor.cs
1
2
3
4
5
6
using System.Security.Claims;

public interface IClaimsPrincipalAccessor
{
    public ClaimsPrincipal? User { get; }
}

Both interfaces live in the Domain layer (Petfolio.Service.Domain/Common/) and have no dependencies on ASP.NET Core, EF Core, or any external library.


The implementation

A single class, CurrentUserService, implements both ICurrentUserService and ITenantProvider. It reads claims from the ClaimsPrincipal and exposes them as typed properties.

Petfolio.Service.Infrastructure/Services/CurrentUserService.cs
public sealed class CurrentUserService(
    IClaimsPrincipalAccessor claimsPrincipalAccessor) : ICurrentUserService, ITenantProvider
{
    public string? UserId =>
        claimsPrincipalAccessor.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;

    public Guid? AccountId =>
        Guid.TryParse(
            claimsPrincipalAccessor.User?.FindFirst("accountId")?.Value,
            out var id) ? id : null;

    public string? Email =>
        claimsPrincipalAccessor.User?.FindFirst(ClaimTypes.Email)?.Value;

    public bool IsAuthenticated =>
        claimsPrincipalAccessor.User?.Identity?.IsAuthenticated ?? false;
}

Each property is evaluated lazily (on access, not on construction). If the request has no authenticated user, all properties return null or false. There is no exception, no fallback - just nulls.

What each property does

  • UserId - Finds the NameIdentifier claim (the standard sub claim in a JWT) and returns its string value. Returns null if the claim is missing.
  • AccountId - Finds the custom accountId claim, attempts to parse it as a Guid, and returns the result. Returns null if the claim is missing or not a valid GUID. This property is part of ITenantProvider - see Multi-Tenancy.
  • Email - Finds the Email claim and returns its string value. Returns null if missing.
  • IsAuthenticated - Checks whether the ClaimsPrincipal has an authenticated identity. Returns false if there is no principal or no identity.

How claims reach CurrentUserService

CurrentUserService never touches HttpContext directly. Instead, a chain of components passes the claims through.

sequenceDiagram
    participant HTTP as HTTP Request
    participant HC as HttpContext
    participant HCA as HttpContextClaimsPrincipalAccessor
    participant CUS as CurrentUserService
    participant Handler as Command/Query Handler

    HTTP->>HC: Request arrives
    Note over HC: HttpContext.User exists<br/>(ClaimsPrincipal, may be empty)
    HCA->>HC: Reads HttpContext.User
    CUS->>HCA: Calls .User property
    CUS->>CUS: Parses claims into typed properties
    Handler->>CUS: Reads UserId, Email, IsAuthenticated
Step Component Layer What it does
1 ASP.NET Core Framework Creates HttpContext with a User property (a ClaimsPrincipal)
2 HttpContextClaimsPrincipalAccessor API Reads HttpContext.User and exposes it as IClaimsPrincipalAccessor
3 CurrentUserService Infrastructure Reads the ClaimsPrincipal and parses individual claims into typed properties
4 Handlers / Services Application Inject ICurrentUserService to access user identity

HttpContextClaimsPrincipalAccessor

This is the bridge between ASP.NET Core's HttpContext and the domain's IClaimsPrincipalAccessor:

Petfolio.Service.Api/Services/HttpContextClaimsPrincipalAccessor.cs
1
2
3
4
5
public sealed class HttpContextClaimsPrincipalAccessor(
    IHttpContextAccessor httpContextAccessor) : IClaimsPrincipalAccessor
{
    public ClaimsPrincipal? User => httpContextAccessor.HttpContext?.User;
}

It does one thing: reads HttpContext.User and passes it through. This indirection means CurrentUserService is testable without a real HTTP context - in tests, you mock IClaimsPrincipalAccessor instead.

No JWT middleware yet

The codebase does not currently include JWT Bearer middleware. HttpContext.User exists on every ASP.NET Core request, but without authentication middleware, it will be an empty, unauthenticated ClaimsPrincipal. This means CurrentUserService properties will return null / false for all real HTTP requests until JWT validation is implemented. See Planned Features.


DI wiring

The interfaces are registered in two places, matching Clean Architecture layer boundaries.

API layer - Program.cs

Petfolio.Service.Api/Program.cs
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<IClaimsPrincipalAccessor, HttpContextClaimsPrincipalAccessor>();

IHttpContextAccessor is an ASP.NET Core service, so the bridge that reads from it must be registered in the API layer. AddHttpContextAccessor() enables access to the current HttpContext from DI.

Infrastructure layer - DependencyInjection.cs

Petfolio.Service.Infrastructure/DependencyInjection.cs
services.AddScoped<ICurrentUserService, CurrentUserService>();
services.AddScoped<ITenantProvider, CurrentUserService>();

Both interfaces resolve to the same CurrentUserService class. They are registered as scoped, meaning a new instance is created for each HTTP request.

Two registrations, same class

Each AddScoped call creates a separate registration. The DI container creates two instances of CurrentUserService per request - one when ICurrentUserService is resolved and another when ITenantProvider is resolved. This is fine because CurrentUserService is stateless: it reads claims from the same ClaimsPrincipal each time.


Key takeaways

  • ICurrentUserService provides user identity (UserId, Email, IsAuthenticated). Inject it in handlers.
  • ITenantProvider provides tenant context (AccountId). Used by PetfolioDbContext. See Multi-Tenancy.
  • Both are implemented by a single CurrentUserService class in the Infrastructure layer.
  • CurrentUserService reads claims from a ClaimsPrincipal via IClaimsPrincipalAccessor - it never touches HttpContext directly.
  • All properties return null or false when there is no authenticated user. No exceptions, no fallbacks.
  • The IClaimsPrincipalAccessor abstraction makes CurrentUserService fully testable without HTTP infrastructure.