PrimusSaaS.Identity.Validator 2.0.0

dotnet add package PrimusSaaS.Identity.Validator --version 2.0.0
                    
NuGet\Install-Package PrimusSaaS.Identity.Validator -Version 2.0.0
                    
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="PrimusSaaS.Identity.Validator" Version="2.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="PrimusSaaS.Identity.Validator" Version="2.0.0" />
                    
Directory.Packages.props
<PackageReference Include="PrimusSaaS.Identity.Validator" />
                    
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 PrimusSaaS.Identity.Validator --version 2.0.0
                    
#r "nuget: PrimusSaaS.Identity.Validator, 2.0.0"
                    
#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 PrimusSaaS.Identity.Validator@2.0.0
                    
#: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=PrimusSaaS.Identity.Validator&version=2.0.0
                    
Install as a Cake Addin
#tool nuget:?package=PrimusSaaS.Identity.Validator&version=2.0.0
                    
Install as a Cake Tool

Primus SaaS Identity Validator - .NET SDK

Package version: 1.5.0

Official .NET SDK for validating JWT/OIDC tokens from your configured identity providers (Azure AD, Auth0, Cognito, Google, GitHub, Okta, or any JWT issuer). The package is library-only: no Primus-hosted login, no Primus-issued tokens, no outbound calls to Primus.

Module quick starts live in docs-site/docs/modules (Identity, Logging, Notifications). Use those instead of the removed integration guide.


πŸ” Supported Identity Providers

Provider Type Configuration Method Common Use Case
Azure AD OIDC .UseAzureAD() Enterprise SSO (Microsoft 365, Azure)
Auth0 OIDC .UseAuth0() SaaS multi-tenant, social login
Google OIDC .UseGoogle() Google Sign-In, Google Workspace
GitHub OIDC .UseGitHub() Developer tools, open-source platforms
Okta OIDC .UseOkta() Enterprise SSO, workforce identity
AWS Cognito OIDC .UseCognito() AWS applications, user pools
Local JWT JWT IssuerConfig with Secret Development, testing, custom auth
Custom OIDC OIDC IssuerConfig with Authority Any OpenID Connect provider

All providers support multi-issuer configuration β€” use multiple identity providers in the same application.


πŸ“‹ Requirements

Supported Frameworks

Framework Status JwtBearer Version Notes
.NET 9.0 βœ… Supported 9.0.11 Latest - full feature parity
.NET 8.0 βœ… Supported 8.0.22 Recommended LTS
.NET 7.0 βœ… Supported 7.0.20 Full feature parity
.NET 6.0 βœ… Supported 6.0.36 LTS - production ready

Dependency Note: Each target framework uses the matching Microsoft.AspNetCore.Authentication.JwtBearer version (e.g., .NET 9 projects pull JwtBearer 9.0.11). All frameworks share System.IdentityModel.Tokens.Jwt 8.14.0.

SDK Requirements

Important: The .NET SDK version must match or exceed your project's target framework.

Your Project Targets Required SDK Download
.NET 9.0 .NET SDK 9.0+ Download
.NET 8.0 .NET SDK 8.0+ Download
.NET 7.0 .NET SDK 7.0+ Download
.NET 6.0 .NET SDK 6.0+ Download

Check your SDK version:

dotnet --version

Common Issue: If you see NETSDK1045: The current .NET SDK does not support targeting .NET X.0, install the matching SDK version above.


Installation

dotnet add package PrimusSaaS.Identity.Validator

Or via NuGet Package Manager:

Install-Package PrimusSaaS.Identity.Validator

πŸ”„ Claim-Agnostic & Session Persistence

  • Claim-agnostic mode: Leave ClaimMappings empty and skip the role/permission/organization helper attributes or policies. The validator will still authenticate tokens from your configured issuers without mapping or inspecting claims. You can also set EnableClaimMapping = false to hard-disable claim normalization globally.
  • Persistent sessions (refresh tokens): Enable durable refresh storage without adding payment or billing code:
    builder.Services.AddPrimusIdentity(options =>
    {
            options.Issuers = /* your issuers */;
            options.TokenRefresh.Enabled = true;
            options.TokenRefresh.UseDurableStore = true; // persist across restarts
            options.TokenRefresh.AccessTokenTtl = TimeSpan.FromHours(1);
            options.TokenRefresh.RefreshTokenTtl = TimeSpan.FromDays(30);
    });
    // Register your IRefreshTokenStore implementation (e.g., database/redis-backed) in DI.
    
    For dev-only, you may set UseInMemoryStore = true; omit it for production so refresh tokens survive restarts.

πŸš€ Auth0 Quick Start (5 Minutes)

New to Auth0? Follow these steps to secure your API in under 5 minutes.

Step 1: Sign up for Auth0 (FREE)

  1. Go to https://auth0.com/signup
  2. Create account (use Google/GitHub for fastest setup)
  3. Choose a tenant name (e.g., my-app β†’ my-app.auth0.com)

Step 2: Create an API in Auth0

  1. Go to Dashboard β†’ Applications β†’ APIs β†’ Create API
  2. Name: My API (or your app name)
  3. Identifier: https://my-api (this becomes your Audience)
  4. Click Create

Step 3: Install the Package

dotnet add package PrimusSaaS.Identity.Validator

Step 4: Configure Your API (Program.cs)

using PrimusSaaS.Identity.Validator;

var builder = WebApplication.CreateBuilder(args);

// Add Auth0 authentication with one line
builder.Services.AddPrimusIdentity(options =>
{
    options.UseAuth0(
        domain: "my-app.auth0.com",      // From Step 1
        audience: "https://my-api");      // From Step 2

    // Allow client_credentials (M2M) tokens explicitly
    // options.Issuers[0].AllowMachineToMachine = true;
    // or: builder.Services.AddPrimusIdentityForAuth0("my-app.auth0.com", "https://my-api", allowMachineToMachine: true);
});

> Machine-to-machine tokens are **disabled by default**. If your Auth0 APIs issue `client_credentials` tokens, set `AllowMachineToMachine = true` (or use `AddPrimusIdentityForAuth0(..., allowMachineToMachine: true)`) to avoid 401s with "Machine-to-machine tokens are not allowed".

builder.Services.AddControllers();
builder.Services.AddAuthorization();

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapPrimusIdentityAuthDiagnostics(); // surfaces auth failure hints via X-Primus-Auth-Error header
app.Run();

Step 5: Protect Your Endpoints

[ApiController]
[Route("api/[controller]")]
[Authorize] // Requires valid Auth0 token
public class SecureController : ControllerBase
{
    [HttpGet]
    public IActionResult Get() => Ok(new { message = "Hello, authenticated user!" });
}

Step 6: Get a Test Token

  1. Go to Dashboard β†’ Applications β†’ APIs β†’ My API β†’ Test tab
  2. Copy the test token
  3. Test your API:
curl -H "Authorization: Bearer YOUR_TOKEN_HERE" https://localhost:5001/api/secure

βœ… Done! Your API is now secured with Auth0.


πŸ†• Modern Minimal API Integration (.NET 8/9)

This section shows how to wire the validator into modern minimal API and controller pipelines.

Minimal API (Complete Example)

using PrimusSaaS.Identity.Validator;

var builder = WebApplication.CreateBuilder(args);

// 1. Services: Add Primus Identity with your preferred provider
builder.Services.AddPrimusIdentity(options =>
{
    // Option A: Auth0 (one-liner)
    options.UseAuth0("your-tenant.auth0.com", "https://your-api");

    // Option B: Azure AD
    // options.Issuers.Add(new IssuerConfig
    // {
    //     Name = "AzureAD",
    //     Type = IssuerType.AzureAD,
    //     Issuer = "https://login.microsoftonline.com/{tenant-id}/v2.0",
    //     Authority = "https://login.microsoftonline.com/{tenant-id}/v2.0",
    //     Audiences = { "api://your-api-id" }
    // });

    // Logging options (optional)
    options.Logging = new PrimusIdentityLoggingOptions
    {
        MinimumLevel = LogLevel.Information,
        RedactSensitiveData = true,
        LogValidationSteps = true
    };
});

builder.Services.AddAuthorization();

var app = builder.Build();

// 2. Middleware: Order matters!
app.UseAuthentication();
app.UseAuthorization();

// 3. Diagnostics: Exposes auth failure hints (optional but recommended)
app.MapPrimusIdentityDiagnostics();

// 4. Endpoints: Use .RequireAuthorization() for minimal APIs
app.MapGet("/", () => "Hello, World!");

app.MapGet("/whoami", (HttpContext ctx) =>
{
    var user = ctx.GetPrimusUser();
    return Results.Ok(new
    {
        userId = user?.UserId,
        email = user?.Email,
        name = user?.Name,
        roles = user?.Roles,
        issuer = user?.Issuer
    });
}).RequireAuthorization();

app.MapGet("/admin", () => Results.Ok(new { message = "Admin access granted" }))
    .RequireAuthorization(policy => policy.RequireRole("Admin"));

app.Run();

Controller-Based API (Complete Example)

// Program.cs
using PrimusSaaS.Identity.Validator;

var builder = WebApplication.CreateBuilder(args);

// Services
builder.Services.AddPrimusIdentity(options =>
{
    builder.Configuration.GetSection("PrimusIdentity").Bind(options);
});
builder.Services.AddAuthorization();
builder.Services.AddControllers();

var app = builder.Build();

// Middleware pipeline
app.UseAuthentication();
app.UseAuthorization();

// Map controllers and diagnostics
app.MapControllers();
app.MapPrimusIdentityDiagnostics();

app.Run();
// Controllers/SecureController.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using PrimusSaaS.Identity.Validator;

[ApiController]
[Route("api/[controller]")]
public class SecureController : ControllerBase
{
    [HttpGet("public")]
    public IActionResult Public() => Ok(new { message = "Public endpoint" });

    [HttpGet("protected")]
    [Authorize]
    public IActionResult Protected()
    {
        var user = HttpContext.GetPrimusUser();
        return Ok(new
        {
            message = "Authenticated!",
            user = new { user?.UserId, user?.Email, user?.Roles }
        });
    }

    [HttpGet("admin")]
    [Authorize(Roles = "Admin")]
    public IActionResult AdminOnly() => Ok(new { message = "Admin access" });
}

Configuration via appsettings.json

{
  "PrimusIdentity": {
    "Issuers": [
      {
        "Name": "Auth0",
        "Type": "Oidc",
        "Issuer": "https://your-tenant.auth0.com/",
        "Authority": "https://your-tenant.auth0.com/",
        "Audiences": ["https://your-api"],
        "AllowMachineToMachine": true
      }
    ],
    "RequireHttpsMetadata": true,
    "ValidateLifetime": true,
    "ClockSkew": "00:05:00"
  }
}

Middleware Order (Critical!)

// CORRECT ORDER - Authentication must come before Authorization
app.UseAuthentication();    // 1. Validates JWT, sets HttpContext.User
app.UseAuthorization();     // 2. Checks policies, roles, claims

// WRONG - Authorization before Authentication will always fail
// app.UseAuthorization();
// app.UseAuthentication();

Extension Methods Reference

Method Purpose
services.AddPrimusIdentity(options) Register authentication services
services.AddPrimusIdentityForAzureAD(tenantId, clientId) One-liner Azure AD setup
services.AddPrimusIdentityForAuth0(domain, audience) One-liner Auth0 setup
services.AddPrimusDevDiagnostics() Enable dev-mode diagnostics
app.MapPrimusIdentityDiagnostics() Expose /primus-identity/diagnostics endpoint
app.MapPrimusDevDiagnostics() Expose detailed dev diagnostics endpoints
options.AddPrimusSwagger() Auto-configure Swagger security definitions
HttpContext.GetPrimusUser() Get authenticated user info
HttpContext.GetMatchedIssuer() Get which issuer validated the token

🎯 Authorization

Primus Identity Validator handles authentication (validating JWT tokens). For authorization (roles, policies, claims), use ASP.NET Core's standard [Authorize] attribute.

Usage Examples

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    // Requires authentication only
    [HttpGet]
    [Authorize]
    public IActionResult GetUsers() => Ok();

    // Requires Admin OR Manager role
    [HttpPost]
    [Authorize(Roles = "Admin,Manager")]
    public IActionResult CreateUser() => Ok();

    // Requires custom policy
    [HttpDelete("{id}")]
    [Authorize(Policy = "CanDeleteUsers")]
    public IActionResult DeleteUser(int id) => Ok();

    // Class-level with method override
    [HttpGet("public")]
    [AllowAnonymous]  // Override for public endpoint
    public IActionResult PublicEndpoint() => Ok();
}

πŸ”§ One-Liner Provider Setup (v1.5.0+)

Azure AD / Microsoft Entra ID

// Handles both v1.0 (M2M) and v2.0 (interactive) issuers automatically
builder.Services.AddPrimusIdentityForAzureAD(
    tenantId: "your-tenant-id",
    clientId: "api://your-client-id",
    allowMachineToMachine: true);  // default: true

Auth0

builder.Services.AddPrimusIdentityForAuth0(
    domain: "your-tenant.auth0.com",
    audience: "https://your-api",
    allowMachineToMachine: true);  // default: false

πŸ“Š Swagger/OpenAPI Integration (v1.5.0+)

Auto-configure Swagger security definitions based on your Primus Identity issuers.

builder.Services.AddSwaggerGen(options =>
{
    // Auto-add security schemes for all configured issuers
    options.AddPrimusSwagger(primusOptions);
    
    // Or simple Bearer-only setup
    options.AddPrimusBearerSwagger();
    
    // Or Azure AD OAuth2 flow
    options.AddPrimusAzureAdSwagger(
        tenantId: "your-tenant-id",
        clientId: "your-client-id");
    
    // Or Auth0 OAuth2 flow
    options.AddPrimusAuth0Swagger(
        domain: "your-tenant.auth0.com",
        audience: "https://your-api");
});

πŸ” Development Diagnostics (v1.5.0+)

Enhanced diagnostics for debugging authentication issues during development.

Enable Dev Diagnostics

builder.Services.AddPrimusDevDiagnostics(options =>
{
    options.EnableDetailedErrors = true;        // Detailed error messages
    options.IncludeTokenHintsInChallenges = true;  // Hints in WWW-Authenticate
    options.IncludeDebugHeaders = true;         // X-Primus-* debug headers
    options.LogTokenRejectionReasons = true;    // Log why tokens fail
    options.MaxRecentFailures = 100;            // Track last 100 failures
});

// Or auto-detect development environment
builder.Services.AddPrimusDevDiagnostics();  // Uses safe defaults in prod

Map Diagnostics Endpoints

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    // Maps these endpoints under /_primus/diagnostics:
    // GET /_primus/diagnostics - Overview of auth configuration
    // GET /_primus/diagnostics/failures - Recent auth failures
    // GET /_primus/diagnostics/failures/stats - Failure statistics
    // POST /_primus/diagnostics/validate-token - Test token validation
    // DELETE /_primus/diagnostics/failures - Clear failure history
    app.MapPrimusDevDiagnostics();
}

Diagnostics Options Presets

// For development - all diagnostics enabled
options.Diagnostics = PrimusDiagnosticsOptions.ForDevelopment();

// For production - safe defaults, no sensitive info exposed
options.Diagnostics = PrimusDiagnosticsOptions.ForProduction();

Example Diagnostics Response

GET /_primus/diagnostics/failures
{
  "totalSinceStartup": 15,
  "count": 5,
  "failures": [
    {
      "timestamp": "2025-12-01T10:30:00Z",
      "reason": "IssuerNotConfigured",
      "reasonDescription": "Token issuer not found in configured issuers",
      "tokenIssuer": "https://wrong-issuer.auth0.com/",
      "configuredIssuers": ["Auth0", "AzureAD"]
    }
  ]
}

Quick Start (General)

1. Configure in Program.cs or Startup.cs

using PrimusSaaS.Identity.Validator;

var builder = WebApplication.CreateBuilder(args);

// Add Primus Identity validation (multi-issuer)
builder.Services.AddPrimusIdentity(options =>
{
    options.Issuers = new()
    {
        new IssuerConfig
        {
            Name = "AzureAD",
            Type = IssuerType.AzureAD, // Alias for OIDC (Azure-friendly)
            Issuer = "https://login.microsoftonline.com/<TENANT_ID>/v2.0",
            Authority = "https://login.microsoftonline.com/<TENANT_ID>/v2.0",
            Audiences = new List<string> { "api://your-api-id" }
        },
        new IssuerConfig
        {
            Name = "LocalAuth",
            Type = IssuerType.Jwt,
            Issuer = "https://auth.yourcompany.com",
            Secret = "your-local-secret",
            Audiences = new List<string> { "api://your-api-id" }
        }
    };

    options.ValidateLifetime = true;
    options.RequireHttpsMetadata = true; // Set false for local dev only
    options.ClockSkew = TimeSpan.FromMinutes(5);

    // Optional: map claims to tenant context
    options.TenantResolver = claims => new TenantContext
    {
        TenantId = claims.Get("tid") ?? "default",
        Roles = claims.Get<List<string>>("roles") ?? new List<string>()
    };
});

builder.Services.AddAuthorization();

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();
app.Run();

Auth0 (simple helper)

builder.Services.AddPrimusIdentity(options =>
{
    // One-liner with sane defaults (issuer/audience/lifetime validation on)
    options.UseAuth0(
        domain: "your-tenant.auth0.com",
        audience: "https://your-api-identifier",
        auth0 =>
        {
            // Optional: map namespaced roles into [Authorize(Roles="...")]
            auth0.RoleClaimName = "https://your-api-identifier/roles";
        });

    // Add other providers alongside Auth0
    options.Issuers.Add(new IssuerConfig
    {
        Name = "AzureAD",
        Type = IssuerType.AzureAD,
        Issuer = "https://login.microsoftonline.com/<TENANT_ID>/v2.0",
        Authority = "https://login.microsoftonline.com/<TENANT_ID>/v2.0",
        Audiences = new List<string> { "api://your-api-id" }
    });
});

Google OIDC (ID tokens)

builder.Services.AddPrimusIdentity(options =>
{
    options.UseGoogle(audience: "<google-client-id>");
    // Add other providers as needed (AzureAD/Auth0/Local)
});

GitHub OAuth

builder.Services.AddPrimusIdentity(options =>
{
    options.UseGitHub(audience: "<github-oauth-client-id>", github =>
    {
        github.Name = "GitHub OAuth";
        // GitHub tokens include sub, login, email, name claims
    });
});

Okta

builder.Services.AddPrimusIdentity(options =>
{
    options.UseOkta(
        domain: "dev-12345.okta.com",
        audience: "api://<okta-client-id>",
        authorizationServerId: "default",  // or null for org authorization server
        okta =>
        {
            okta.RoleClaimName = "groups";  // Maps Okta groups to roles
        });
});

AWS Cognito user pool

// AWS Cognito user pool
builder.Services.AddPrimusIdentity(options =>
{
    options.UseCognito(
        region: "us-east-1",
        userPoolId: "us-east-1_ABC123",
        audience: "<app-client-id>",
        cognito =>
        {
            cognito.RoleClaimName = "cognito:groups"; // optional, maps into ClaimTypes.Role
        });
});

Machine-to-machine & email verification (Auth0 example)

// Machine-to-machine & email verification (Auth0 example)
builder.Services.AddPrimusIdentity(options =>
{
    options.UseAuth0("your-tenant.auth0.com", "https://your-api-identifier", auth0 =>
    {
        auth0.AllowMachineToMachine = true;
        auth0.AllowedGrantTypes.Add("client-credentials");
        auth0.AllowedMachineToMachineScopes.AddRange(new[] { "read:clients", "write:clients" });
        auth0.RequireEmailVerification = true; // for user tokens
    });
});

M2M scope enforcement

  • Set AllowMachineToMachine = true and AllowedMachineToMachineScopes to restrict scopes on client-credentials tokens.
  • Tokens with scopes outside the allowed list will be rejected.

Local/Test token generation

// Generate a test JWT (HMAC) for local or integration tests
var token = TestTokenBuilder.Create()
    .WithIssuer("https://localhost")
    .WithAudience("api://your-api-id")
    .WithSecret("local-secret")  // match your IssuerConfig secret when validating
    .WithExpiry(TimeSpan.FromHours(8)) // optional lifetime override (defaults to 1 hour)
    .WithClaim("sub", "user-123")
    .WithClaim("email", "test@example.com")
    .Build();
  • Prefer CreateFromConfig(builder.Configuration.GetSection("PrimusIdentity:Issuers:LocalJwt")) to auto-align issuer/audience/secret with appsettings.json; mismatched secrets will surface as 401/500 during validation.
  • WithExpiry(TimeSpan lifetime) sets a relative expiry (old WithExpiry(DateTimeOffset) still works for absolute timestamps).

Using the built-in fake handler (for integration tests)

// In your test host setup (WebApplicationFactory, minimal API, etc.)
services.AddFakePrimusAuth(); // from PrimusSaaS.Identity.Validator.Tests.IntegrationHarness
app.UseFakePrimusAuth();

This authenticates requests with a fixed user (sub, email, name) so you can test APIs without an external IdP. See examples/dotnet-api/FakeAuthApi for a runnable sample.

Logging & diagnostics

  • Configure logging verbosity and redaction via options.Logging:
    • MinimumLevel (default: Information)
    • RedactSensitiveData (default: true)
      • LogValidationSteps (default: true)
      • LogIssuerDetails (default: true)
  • The type is PrimusIdentityLoggingOptions (older docs mentioning LoggingOptions will not compile):
    builder.Services.AddPrimusIdentity(options =>
    {
        options.Logging = new PrimusIdentityLoggingOptions
        {
            MinimumLevel = LogLevel.Information,
            RedactSensitiveData = true,
            LogValidationSteps = true,
            LogIssuerDetails = true
        };
    });
    
  • Expose diagnostics endpoint with app.MapPrimusIdentityAuthDiagnostics();
  • Structured logging: when LogValidationSteps is true, issuer/audience/kid are logged; subjects are hashed when redaction is on.
  • Refresh tokens: set TokenRefresh.UseDurableStore = true and register IRefreshTokenStore (e.g., DistributedRefreshTokenStore for Redis/SQL via IDistributedCache).

Auth0 client_credentials (M2M) tokens

  • Auth0 marks client credentials tokens with gty: "client-credentials" and a subject ending in @clients. These are treated as machine-to-machine tokens.
  • To allow them, set AllowMachineToMachine = true in the Auth0 issuer config and optionally restrict AllowedGrantTypes to client_credentials.
    {
      "Name": "Auth0",
      "Type": "Oidc",
      "Issuer": "https://your-tenant.us.auth0.com/",
      "Authority": "https://your-tenant.us.auth0.com/",
      "Audiences": [ "https://saas-api/" ],
      "AllowMachineToMachine": true,
      "AllowedGrantTypes": [ "client_credentials" ]
    }
    
  • If AllowMachineToMachine is false, validation fails with "Machine-to-machine tokens are not allowed for this issuer."

Multi-provider (Azure AD + Auth0 + Local)

builder.Services.AddPrimusIdentity(options =>
{
    // Azure AD
    options.Issuers.Add(new IssuerConfig
    {
        Name = "AzureAD",
        Type = IssuerType.AzureAD,
        Issuer = "https://login.microsoftonline.com/<TENANT_ID>/v2.0",
        Authority = "https://login.microsoftonline.com/<TENANT_ID>/v2.0",
        Audiences = new List<string> { "api://your-api-id" }
    });

    // Auth0 (one-line helper)
    options.UseAuth0("your-tenant.auth0.com", "https://your-api-identifier", auth0 =>
    {
        auth0.RoleClaimName = "https://your-api-identifier/roles"; // optional
    });

    // Local JWT (shared secret)
    options.Issuers.Add(new IssuerConfig
    {
        Name = "LocalAuth",
        Type = IssuerType.Jwt,
        Issuer = "https://auth.yourcompany.com",
        Secret = "your-local-secret",
        Audiences = new List<string> { "api://your-api-id" }
    });
});
Azure AD issuer formats (v1 vs v2)
  • Azure AD client_credentials (app-only) tokens default to v1 issuers: https://sts.windows.net/{tenantId}/ (no /v2.0).
  • User/interactive tokens typically use v2 issuers: https://login.microsoftonline.com/{tenantId}/v2.0.
  • Configure Issuer to match the token’s iss claim, even if you still use the v2 authority for discovery/JWKS:
options.Issuers.Add(new IssuerConfig
{
    Name = "AzureAD M2M",
    Type = IssuerType.AzureAD,
    Issuer = $"https://sts.windows.net/{tenantId}/",            // matches app-only tokens
    Authority = $"https://login.microsoftonline.com/{tenantId}/v2.0", // discovery/JWKS
    Audiences = { "api://your-api-id" },
    AllowMachineToMachine = true
});

If you accept both interactive and client_credentials flows, add two issuer entries (v2 + v1) with different Name values but the same audience.

Auth0 multi-tenant (resolve per request)

builder.Services.AddPrimusIdentity(options =>
{
    options.Auth0MultiTenant = new Auth0MultiTenantOptions
    {
        ResolveTenant = ctx =>
        {
            // Example: subdomain-based tenant routing
            var host = ctx.Request.Host.Host;
            return host.Split('.').FirstOrDefault();
        }
    };

    options.Auth0MultiTenant.Tenants["client-a"] = new Auth0Options
    {
        Domain = "client-a.auth0.com",
        Audiences = { "https://api-client-a" }
    };

    options.Auth0MultiTenant.Tenants["client-b"] = new Auth0Options
    {
        Domain = "client-b.auth0.com",
        Audiences = { "https://api-client-b" }
    };
});

2. Protect Your API Endpoints

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using PrimusSaaS.Identity.Validator;

[ApiController]
[Route("api/[controller]")]
public class SecureController : ControllerBase
{
    [HttpGet]
    [Authorize] // Requires valid token from a configured issuer
    public IActionResult GetSecureData()
    {
        // Get the authenticated Primus user
        var primusUser = HttpContext.GetPrimusUser();
        
        return Ok(new
        {
            message = "Secure data accessed successfully",
            user = new
            {
                userId = primusUser?.UserId,
                email = primusUser?.Email,
                name = primusUser?.Name,
                roles = primusUser?.Roles,
                issuer = primusUser?.Issuer,
                provider = primusUser?.ProviderName ?? primusUser?.ProviderType
            }
        });
    }

    [HttpGet("admin")]
    [Authorize(Roles = "Admin")] // Requires Admin role from your IdP
    public IActionResult GetAdminData()
    {
        return Ok(new { message = "Admin-only data" });
    }
}
Get matched issuer/provider in controllers

The middleware stores the matched issuer for each request:

using PrimusSaaS.Identity.Validator;

[HttpGet("whoami")]
[Authorize]
public IActionResult WhoAmI()
{
    var issuer = HttpContext.GetMatchedIssuer();
    return Ok(new
    {
        provider = issuer?.Provider ?? issuer?.Name ?? "unknown",
        issuer = issuer?.Issuer,
        audiences = issuer?.Audiences
    });
}

PrimusUser also surfaces Issuer, ProviderName, and ProviderType derived from these values.

3. Access User Information

// In any controller or middleware
var primusUser = HttpContext.GetPrimusUser();

if (primusUser != null)
{
    Console.WriteLine($"User ID: {primusUser.UserId}");
    Console.WriteLine($"Email: {primusUser.Email}");
    Console.WriteLine($"Name: {primusUser.Name}");
    Console.WriteLine($"Roles: {string.Join(", ", primusUser.Roles)}");
    
    // Access additional claims
    foreach (var claim in primusUser.AdditionalClaims)
    {
        Console.WriteLine($"{claim.Key}: {claim.Value}");
    }
}

Configuration Options

Option Required Description Default
Issuers Yes List of issuer configs (Oidc/AzureAD or Jwt) -
ValidateLifetime No Validate token expiration true
RequireHttpsMetadata No Require HTTPS for metadata true
AllowHttpOnLocalhost No Permit HTTP issuer/authority on localhost for development true
ClockSkew No Allowed time difference 5 minutes
JwksCacheTtl No JWKS cache TTL (OIDC) 24 hours
TenantResolver No Map claims to TenantContext null
EnableClaimMapping No Toggle claim normalization/mapping. Set to false for fully claim-agnostic validation. true

IssuerConfig

Field Required Description
Name Yes Friendly name (e.g., AzureAD, LocalAuth)
Type Yes Oidc or Jwt
Issuer Yes Expected iss value to route tokens
Authority OIDC only Authority URL for discovery/JWKS
JwksUrl JWT optional JWKS endpoint (if not using Secret)
Secret JWT optional Symmetric key for HMAC tokens
Audiences Yes Allowed audience values
ClaimMappings No Map provider claims to standard claim types
RoleClaimName No If set, mapped into ClaimTypes.Role
PermissionClaimName No Permission claim to normalize (defaults to permissions)

Configuration from appsettings.json

{
  "PrimusIdentity": {
    "Issuers": [
      {
        "Name": "AzureAD",
        "Type": "Oidc",
        "Issuer": "https://login.microsoftonline.com/<TENANT_ID>/v2.0",
        "Authority": "https://login.microsoftonline.com/<TENANT_ID>/v2.0",
        "Audiences": [ "api://your-api-id" ]
      },
      {
        "Name": "LocalAuth",
        "Type": "Jwt",
        "Issuer": "https://auth.yourcompany.com",
        "Secret": "your-local-secret",
        "Audiences": [ "api://your-api-id" ]
      }
    ],
    "RequireHttpsMetadata": false
  }
}

Generating Tokens for Local JWT Issuer

Important: The Secret, Issuer, and Audience values used when generating tokens MUST EXACTLY MATCH your validator configuration.

Quick Example

using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;

public string GenerateLocalJwtToken(string userId, string email, string name)
{
    // Critical: Load from same configuration source
    var secret = _config["PrimusIdentity:Issuers:1:Secret"];
    var issuer = _config["PrimusIdentity:Issuers:1:Issuer"];
    var audience = _config["PrimusIdentity:Issuers:1:Audiences:0"];
    
    var tokenHandler = new JwtSecurityTokenHandler();
    var key = Encoding.UTF8.GetBytes(secret);
    
    var tokenDescriptor = new SecurityTokenDescriptor
    {
        Subject = new ClaimsIdentity(new[]
        {
            new Claim("sub", userId),
            new Claim("email", email),
            new Claim("name", name)
        }),
        Expires = DateTime.UtcNow.AddHours(1),
        Issuer = issuer,
        Audience = audience,
        SigningCredentials = new SigningCredentials(
            new SymmetricSecurityKey(key),
            SecurityAlgorithms.HmacSha256Signature
        )
    };

    var token = tokenHandler.CreateToken(tokenDescriptor);
    return tokenHandler.WriteToken(token);
}

For complete token generation examples, see TOKEN_GENERATION_GUIDE.md

Troubleshooting

Common Errors

Error Cause Solution
Invalid signature Secret key mismatch Ensure token generation and validation use the same secret
Untrusted issuer Issuer format incorrect Use full URL format (e.g., https://localhost:5265) not name
Invalid audience Audience mismatch Use API identifier format (e.g., api://your-app-id)
Token expired Token past expiration Generate new token or increase ClockSkew

For detailed troubleshooting, see ERROR_REFERENCE.md

Migrating from JwtBearer/Auth0 SDK

  • You can swap existing AddJwtBearer Auth0 config for options.UseAuth0(domain, audience, ...) without changing your controllers; permissions/roles map into standard claims.
  • Auth0 namespaced roles: set RoleClaimName to your namespaced roles claim and [Authorize(Roles = "...")] will work.
  • Minimal migration snippet:
    // Before
    builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(opt =>
        {
            opt.Authority = "https://your-tenant.auth0.com/";
            opt.Audience = "https://your-api-identifier";
        });
    
    // After
    builder.Services.AddPrimusIdentity(options =>
    {
        options.UseAuth0("your-tenant.auth0.com", "https://your-api-identifier", auth0 =>
        {
            auth0.RoleClaimName = "https://your-api-identifier/roles"; // if you had roles mapped
        });
    });
    builder.Services.AddAuthorization();
    
  • Migration helper:
    var auth0 = JwtBearerMigrationHelper.ToAuth0Options(new JwtBearerMigrationHelper.JwtBearerConfig
    {
        Authority = "https://your-tenant.auth0.com/",
        Audience = "https://your-api-identifier",
        RoleClaimName = "https://your-api-identifier/roles"
    });
    var options = new PrimusIdentityOptions();
    options.Issuers.Add(auth0.ToIssuerConfig());
    

Production Deployment

Caution: Never commit secrets to source control! Use Azure Key Vault or environment variables.

Quick Checklist

  • Secrets stored in Azure Key Vault
  • RequireHttpsMetadata: true in production
  • HTTPS redirection enabled
  • CORS configured for production domains
  • Logging and monitoring configured

For complete deployment guide, see PRODUCTION_DEPLOYMENT.md

Client Usage Example

To call your protected API from a client application:

using System.Net.Http.Headers;


var httpClient = new HttpClient();
var jwtToken = "your-jwt-token-here"; // From your auth system or token generator


// Add token to Authorization header
httpClient.DefaultRequestHeaders.Authorization = 
    new AuthenticationHeaderValue("Bearer", jwtToken);

var response = await httpClient.GetAsync("https://your-api.com/api/secure");
var data = await response.Content.ReadAsStringAsync();

Development Tips

Disable HTTPS Requirement for Local Development

builder.Services.AddPrimusIdentity(options =>
{
    options.RequireHttpsMetadata = false; // Allow HTTP in development
    // ... other options
});

Localhost HTTP shortcuts

  • Loopback issuers/authorities such as http://localhost:5000 are allowed by default for development (AllowHttpOnLocalhost = true).
  • Set AllowHttpOnLocalhost = false to enforce HTTPS everywhere, even on localhost.

Enable Detailed Logging

The SDK automatically logs authentication events to the console. For more detailed logging, enable ASP.NET Core logging:

{
  "Logging": {
    "LogLevel": {
      "Microsoft.AspNetCore.Authentication": "Debug"
    }
  }
}

Requirements

  • .NET 6.0, 7.0, 8.0, or 9.0
  • ASP.NET Core 6.0, 7.0, 8.0, or 9.0

Dependency Matrix: See the Supported Frameworks table at the top for the exact Microsoft.AspNetCore.Authentication.JwtBearer version used per framework.

βœ… Integration checklist (Auth0 + Azure AD)

  • Auth0: Create an API (Machine-to-Machine Application) with audience = https://your-api. Enable Client Credentials. Docs: https://auth0.com/docs/get-started/auth0-overview/set-up-apis.
  • Auth0 app settings to capture: Domain (issuer/authority), Client ID/Secret, Audience. M2M tokens use gty: "client-credentials" (hyphen) and sub ends with @clients.
  • Azure AD: Register an app, expose API scopes or set Application ID URI (audience), and add a client app with Client credentials. Docs: https://learn.microsoft.com/azure/active-directory/develop/quickstart-register-app.
  • Configuration (appsettings): set Issuer, Authority, and Audiences exactly; for Auth0 allow M2M with AllowMachineToMachine = true and include AllowedGrantTypes: ["client_credentials", "client-credentials"].
  • CORS: allow your frontend origin (e.g., http://localhost:5173 / https://localhost:5173) on the API.

Known Issues

Namespace Conflict with PrimusSaaS.Logging

If you are using both PrimusSaaS.Identity.Validator and PrimusSaaS.Logging, you may encounter an ambiguous reference error for UsePrimusLogging().

Status: Fixed in PrimusSaaS.Logging >= 1.2.2 (duplicate extension removed). Simply import using PrimusSaaS.Logging.Extensions; and call:

app.UsePrimusLogging();

If you cannot upgrade Logging yet: Use the fully qualified name or an alias.

using PrimusLogging = PrimusSaaS.Logging.Extensions;

// ...

PrimusLogging.LoggingExtensions.UsePrimusLogging(app);

Common pitfalls (save time)

  • Logging options type is PrimusIdentityLoggingOptions (not LoggingOptions).
  • Auth0 M2M: set AllowMachineToMachine = true and include both grant spellings: client_credentials and client-credentials.
  • Azure AD v1 vs v2 issuers: client_credentials tokens often use https://sts.windows.net/{tenantId}/ (v1). Ensure your Issuer/Authority matches the actual token issuer or add both.

Documentation

Support

For issues, questions, or contributions, visit:

License

MIT License - see LICENSE file for details

Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 is compatible.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  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 is compatible.  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 was computed.  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
2.0.0 107 1/12/2026
1.5.0 313 12/3/2025
1.3.6 274 11/30/2025
1.3.5 234 11/30/2025
1.3.3 239 11/29/2025
1.3.2 126 11/29/2025
1.3.1 137 11/28/2025
1.3.0 206 11/24/2025
1.2.3 200 11/24/2025
1.2.2 200 11/24/2025
1.2.1 197 11/24/2025
1.2.0 190 11/23/2025
1.1.0 158 11/23/2025
1.0.0 317 11/21/2025

v2.0.0:
- Standardized Framework Release.
- Renamed all packages to PrimusSaaS.* namespace.
- Synchronized versions across the entire suite.
- Enhanced metadata and fixed consistency issues.