PMQ.Identity 1.1.2

dotnet add package PMQ.Identity --version 1.1.2
                    
NuGet\Install-Package PMQ.Identity -Version 1.1.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="PMQ.Identity" Version="1.1.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="PMQ.Identity" Version="1.1.2" />
                    
Directory.Packages.props
<PackageReference Include="PMQ.Identity" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add PMQ.Identity --version 1.1.2
                    
#r "nuget: PMQ.Identity, 1.1.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package PMQ.Identity@1.1.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=PMQ.Identity&version=1.1.2
                    
Install as a Cake Addin
#tool nuget:?package=PMQ.Identity&version=1.1.2
                    
Install as a Cake Tool

PMQ.Identity

Provider-agnostic identity module for authentication and authorization in ASP.NET Core applications.
Supports External (OIDC/JWT — e.g. Keycloak, Auth0) and Local (self-issued JWT — e.g. ASP.NET Identity) modes.

NuGet License: MIT

Installation

dotnet add package PMQ.Identity

Quick Start

1. Add configuration to appsettings.json

Choose one of the two modes:

<details> <summary><b>External Mode (Keycloak example)</b></summary>

{
  "IdentitySettings": {
    "Mode": "External",
    "External": {
      "Authority": "https://keycloak.example.com/realms/my-realm",
      "Audience": "my-api"
    }
  }
}

</details>

<details> <summary><b>Local Mode (self-issued JWT)</b></summary>

{
  "IdentitySettings": {
    "Mode": "Local",
    "Local": {
      "Issuer": "https://my-api.example.com",
      "Audience": "https://my-api.example.com",
      "SecretKey": "a-very-long-secret-key-at-least-32-characters!!",
      "TokenExpirationMinutes": 60
    }
  }
}

</details>

2. Register services in Program.cs

builder.Services.AddPmqIdentity(builder.Configuration);

var app = builder.Build();

app.UsePmqIdentity();

3. Protect endpoints

[Authorize(Policy = PmqPolicies.Authenticated)]
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    private readonly ICurrentUser _currentUser;

    public OrdersController(ICurrentUser currentUser)
    {
        _currentUser = currentUser;
    }

    [HttpGet("me")]
    public IActionResult Me()
    {
        return Ok(new
        {
            _currentUser.Id,
            _currentUser.Email,
            _currentUser.Roles
        });
    }

    [Authorize(Policy = PmqPolicies.AdminOnly)]
    [HttpDelete("{id}")]
    public IActionResult Delete(int id) => NoContent();
}

External Mode — Keycloak Example

Use this mode when authentication is handled by an external OIDC provider. PMQ.Identity only validates incoming JWTs — it does not manage users or passwords.

Configuration

{
  "IdentitySettings": {
    "Mode": "External",
    "External": {
      "Authority": "https://keycloak.example.com/realms/my-realm",
      "Audience": "my-api"
    }
  }
}

Registration with Keycloak-specific claims mapping

Keycloak uses realm_access.roles instead of the standard role claim. You can map it:

builder.Services.AddPmqIdentity(builder.Configuration, options =>
{
    options
        .UseExternal()
        .ConfigureClaimsMapping(claims =>
        {
            claims.UserIdClaimType = "sub";
            claims.EmailClaimType = "email";
            claims.RoleClaimType = "realm_roles"; // mapped via Keycloak protocol mapper
        });
});

Advanced: custom token validation and events

builder.Services.AddPmqIdentity(builder.Configuration, options =>
{
    options.UseExternal(external =>
    {
        external.RequireHttpsMetadata = true;

        external.ConfigureTokenValidation = tvp =>
        {
            tvp.ValidateIssuer = true;
            tvp.ValidateAudience = true;
        };

        external.ConfigureEvents = events =>
        {
            events.OnAuthenticationFailed = context =>
            {
                Console.WriteLine($"Auth failed: {context.Exception.Message}");
                return Task.CompletedTask;
            };
        };
    });
});

Local Mode — ASP.NET Identity Example

Use this mode when your API issues its own JWT tokens. You provide the implementations for user storage and password hashing — PMQ.Identity handles token generation, validation, and the authentication pipeline.

1. Implement the required interfaces

User entity
public class AppUser : IdentityUser, IIdentityUser
{
    public string Id => base.Id;
    public string Email => base.Email!;

    public IReadOnlyCollection<Claim> Claims => new List<Claim>
    {
        new(JwtRegisteredClaimNames.Sub, Id),
        new(JwtRegisteredClaimNames.Email, Email),
        new(ClaimTypes.Role, "User")
    };
}
User store (backed by ASP.NET Identity / EF Core)
public class AppUserStore : IUserStore
{
    private readonly UserManager<AppUser> _userManager;

    public AppUserStore(UserManager<AppUser> userManager)
    {
        _userManager = userManager;
    }

    public async Task<IIdentityUser?> FindByEmailAsync(string email, CancellationToken ct = default)
    {
        return await _userManager.FindByEmailAsync(email);
    }

    public async Task<IIdentityUser?> FindByIdAsync(string id, CancellationToken ct = default)
    {
        return await _userManager.FindByIdAsync(id);
    }

    public async Task<string?> GetPasswordHashAsync(string userId, CancellationToken ct = default)
    {
        var user = await _userManager.FindByIdAsync(userId);
        return user?.PasswordHash;
    }
}
Password hasher (wrapping ASP.NET Identity's hasher)
public class AppPasswordHasher : IPasswordHasher
{
    private readonly PasswordHasher<AppUser> _hasher = new();

    public string Hash(string password)
    {
        return _hasher.HashPassword(null!, password);
    }

    public bool Verify(string password, string hash)
    {
        if (string.IsNullOrEmpty(hash)) return false;
        var result = _hasher.VerifyHashedPassword(null!, hash, password);
        return result != PasswordVerificationResult.Failed;
    }
}

2. Register services

// ASP.NET Identity + EF Core setup
builder.Services.AddDbContext<AppDbContext>(o =>
    o.UseNpgsql(builder.Configuration.GetConnectionString("Default")));

builder.Services.AddIdentityCore<AppUser>()
    .AddEntityFrameworkStores<AppDbContext>();

// PMQ.Identity — Local mode
builder.Services.AddPmqIdentity(builder.Configuration, options =>
{
    options.UseLocal();
});

// Register your implementations
builder.Services.AddScoped<IUserStore, AppUserStore>();
builder.Services.AddScoped<IPasswordHasher, AppPasswordHasher>();

3. Create a login endpoint

[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
    private readonly AuthenticationService _authService;

    public AuthController(AuthenticationService authService)
    {
        _authService = authService;
    }

    [HttpPost("login")]
    public async Task<IActionResult> Login([FromBody] LoginRequest request, CancellationToken ct)
    {
        var result = await _authService.AuthenticateAsync(request.Email, request.Password, ct);
        if (result is null)
            return Unauthorized();

        return Ok(result); // { accessToken, expiresIn }
    }
}

public record LoginRequest(string Email, string Password);

Authorization

PMQ.Identity comes with built-in policies and a dynamic policy provider.

Built-in policies

Policy Description
PmqPolicies.Authenticated Requires the user to be authenticated
PmqPolicies.AdminOnly Requires the "Admin" role

Dynamic role policies

// Require a specific role — no manual policy registration needed
[Authorize(Policy = "RequireRole:Manager")]
[HttpGet("reports")]
public IActionResult Reports() => Ok();

// Or use the helper method
[Authorize(Policy = PmqPolicies.HasRole("Editor"))]
[HttpPut("{id}")]
public IActionResult Edit(int id) => Ok();

Custom policies

builder.Services.AddPmqIdentity(builder.Configuration, options =>
{
    options.ConfigureAuthorization(auth =>
    {
        auth.AddPolicy("MinAge18", policy =>
            policy.RequireClaim("age", "18"));
    });
});

ICurrentUser

Inject ICurrentUser anywhere to access the authenticated user's information:

public class OrderService
{
    private readonly ICurrentUser _currentUser;

    public OrderService(ICurrentUser currentUser)
    {
        _currentUser = currentUser;
    }

    public void DoWork()
    {
        var userId   = _currentUser.Id;
        var email    = _currentUser.Email;
        var roles    = _currentUser.Roles;
        var tenant   = _currentUser.FindClaim("tenant_id");
        var scopes   = _currentUser.FindClaims("scope");
    }
}

Claims Mapping

Different providers use different claim types. Configure the mapping to match your provider:

builder.Services.AddPmqIdentity(builder.Configuration, options =>
{
    options.ConfigureClaimsMapping(claims =>
    {
        claims.UserIdClaimType = "sub";
        claims.EmailClaimType  = "email";
        claims.RoleClaimType   = "realm_roles";
    });
});

API Reference

Services registered

Service Lifetime Mode Description
ICurrentUser Scoped Both Access to the authenticated user
ITokenService Scoped Local JWT token generation
AuthenticationService Scoped Local Credential validation + token issuance

Interfaces to implement (Local mode)

Interface Purpose
IUserStore User lookup by email/id and password hash retrieval
IPasswordHasher Password hashing and verification
IIdentityUser User entity with id, email, and claims

License

MIT

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.2 134 8/12/2026
1.1.1 224 8/5/2026
1.1.0 108 8/5/2026
1.0.0 133 4/7/2026