PrimusSaaS.Identity.Validator
2.0.1
dotnet add package PrimusSaaS.Identity.Validator --version 2.0.1
NuGet\Install-Package PrimusSaaS.Identity.Validator -Version 2.0.1
<PackageReference Include="PrimusSaaS.Identity.Validator" Version="2.0.1" />
<PackageVersion Include="PrimusSaaS.Identity.Validator" Version="2.0.1" />
<PackageReference Include="PrimusSaaS.Identity.Validator" />
paket add PrimusSaaS.Identity.Validator --version 2.0.1
#r "nuget: PrimusSaaS.Identity.Validator, 2.0.1"
#:package PrimusSaaS.Identity.Validator@2.0.1
#addin nuget:?package=PrimusSaaS.Identity.Validator&version=2.0.1
#tool nuget:?package=PrimusSaaS.Identity.Validator&version=2.0.1
Primus SaaS Identity Validator - .NET SDK
Package version: 2.0.0
Release status: Stable for token validation. Supplemental refresh-token helpers are included for backward compatibility, but they are not part of the core validator boundary.
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.
Validation
- Current workspace validation:
367/367tests passed onnet8.0 - Current workspace validation:
367/367tests passed onnet9.0 - Current workspace validation:
367/367tests passed onnet10.0 - Current workspace validation: coverage passed the configured threshold on all three TFMs
- Scope boundary: token validation and principal normalization only; refresh helpers remain supplemental
Telemetry
- Emits validation, revocation, and JWKS traces through a package
ActivitySource. - Emits exporter-agnostic metrics counters for validation outcomes, revocation outcomes, and JWKS outcomes.
- Consumers still need to wire their own OpenTelemetry exporter or
MeterListenerif they want telemetry shipped out of process. - The package intentionally stays exporter-agnostic and does not depend on a specific telemetry backend.
Combined ASP.NET Core Host
If you are integrating this validator together with PrimusSaaS.MultiTenancy and PrimusSaaS.Rbac in one host, use the verified combined onboarding guide at /docs/modules/combined-dotnet-integration.
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 |
| 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, so you can 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.JwtBearerversion (e.g., .NET 9 projects pull JwtBearer 9.0.11). All frameworks shareSystem.IdentityModel.Tokens.Jwt8.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 Mode
- Claim-agnostic mode: Leave
ClaimMappingsempty 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 setEnableClaimMapping = falseto hard-disable claim normalization globally.
SAML-Derived Claim Normalization
If a SAML broker, federated IdP, or custom assertion mapper emits non-canonical attributes such as uid, mail, displayName, or groups, you can normalize them with the lightweight preset:
options.Issuers.Add(new IssuerConfig
{
Name = "SamlFederation",
Type = IssuerType.Oidc,
Issuer = "https://saml.example/",
Authority = "https://saml.example/",
Audiences = new List<string> { "api://saml" },
ClaimMappings = ClaimMappingOptions.CreateSamlProfile()
});
This is intentionally a claim-normalization helper only. It does not register SAML handlers, process SAML assertions, or turn the validator package into a SAML broker. If your SAML stack already emits canonical WS-Federation claim URIs, the default mapping profile is usually enough.
Supplemental Refresh Helpers
This package still ships refresh-token helpers for existing consumers, but they should be treated as supplemental authentication flow support rather than part of the validator core.
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 evaluation, 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)
- Go to https://auth0.com/signup
- Create account (use Google/GitHub for fastest setup)
- Choose a tenant name (e.g.,
my-appmy-app.auth0.com)
Step 2: Create an API in Auth0
- Go to Dashboard Applications APIs Create API
- Name:
My API(or your app name) - Identifier:
https://my-api(this becomes your Audience) - 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
- Go to Dashboard Applications APIs My API Test tab
- Copy the test token
- 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)
});
Or via appsettings.json:
{
"PrimusIdentity": {
"Issuers": [
{
"Name": "Google",
"Type": "Google",
"Issuer": "https://accounts.google.com",
"Authority": "https://accounts.google.com",
"Audiences": ["<google-client-id>"]
}
]
}
}
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
});
});
Or via appsettings.json:
{
"PrimusIdentity": {
"Issuers": [
{
"Name": "GitHub",
"Type": "GitHub",
"Issuer": "https://token.actions.githubusercontent.com",
"Authority": "https://token.actions.githubusercontent.com",
"Audiences": ["<github-oauth-client-id>"]
}
]
}
}
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
});
});
Or via appsettings.json:
{
"PrimusIdentity": {
"Issuers": [
{
"Name": "Okta",
"Type": "Okta",
"Issuer": "https://dev-12345.okta.com/oauth2/default",
"Authority": "https://dev-12345.okta.com/oauth2/default",
"Audiences": ["api://<okta-client-id>"]
}
]
}
}
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
});
});
Or via appsettings.json:
{
"PrimusIdentity": {
"Issuers": [
{
"Name": "Cognito",
"Type": "Cognito",
"Issuer": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_ABC123",
"Authority": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_ABC123",
"Audiences": ["<app-client-id>"]
}
]
}
}
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 = trueandAllowedMachineToMachineScopesto 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 withappsettings.json; mismatched secrets will surface as 401/500 during validation. WithExpiry(TimeSpan lifetime)sets a relative expiry (oldWithExpiry(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 mentioningLoggingOptionswill 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
LogValidationStepsis true, issuer/audience/kid are logged; subjects are hashed when redaction is on. - Refresh tokens: set
TokenRefresh.UseDurableStore = trueand registerIRefreshTokenStore(e.g.,DistributedRefreshTokenStorefor Redis/SQL viaIDistributedCache).
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 = truein the Auth0 issuer config and optionally restrictAllowedGrantTypestoclient_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
AllowMachineToMachineis 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
Issuerto match the tokensissclaim, 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
AddJwtBearerAuth0 config foroptions.UseAuth0(domain, audience, ...)without changing your controllers; permissions/roles map into standard claims. - Auth0 namespaced roles: set
RoleClaimNameto 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:5000are allowed by default for development (AllowHttpOnLocalhost = true). - Set
AllowHttpOnLocalhost = falseto 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.JwtBearerversion 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 usegty: "client-credentials"(hyphen) andsubends with@clients. - Azure AD: Register an app, expose API scopes or set
Application ID URI(audience), and add a client app withClient credentials. Docs: https://learn.microsoft.com/azure/active-directory/develop/quickstart-register-app. - Configuration (appsettings): set
Issuer,Authority, andAudiencesexactly; for Auth0 allow M2M withAllowMachineToMachine = trueand includeAllowedGrantTypes: ["client_credentials", "client-credentials"]. - CORS: allow your frontend origin (e.g.,
http://localhost:5173/https://localhost:5173) on the API.
Example Projects
| Project | What it shows |
|---|---|
examples/identity/Minimal/ |
Single-issuer JWT validation in a minimal API |
examples/identity/Identity-Validator-Example/ |
Multi-issuer setup with /whoami endpoint |
examples/identity/Advanced/ |
Azure AD + Auth0 + Local, custom claims, policy auth |
See IDENTITY_OVERVIEW.md for the full package catalog and decision matrix.
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(notLoggingOptions). - Auth0 M2M: set
AllowMachineToMachine = trueand include both grant spellings:client_credentialsandclient-credentials. - Azure AD v1 vs v2 issuers: client_credentials tokens often use
https://sts.windows.net/{tenantId}/(v1). Ensure yourIssuer/Authoritymatches the actual token issuer or add both.
Documentation
- TOKEN_GENERATION_GUIDE.md - Complete guide to generating JWT tokens
- LOCAL_DEVELOPMENT_GUIDE.md - Setup guide for offline/local development
- INTEGRATION_PATTERNS.md - Controller, Service, and Middleware examples
- TENANT_RESOLVER_GUIDE.md - Multi-tenant context resolution guide
- PRIMUS_USER_REFERENCE.md - PrimusUser object properties and mapping
- ERROR_HANDLING_GUIDE.md - Handling exceptions and customizing responses
- ERROR_REFERENCE.md - Troubleshooting validation errors
- PRODUCTION_DEPLOYMENT.md - Production deployment best practices
- SECRET_MANAGEMENT.md - Securely managing secrets (Key Vault, User Secrets)
- TESTING_GUIDE.md - Testing guide with Postman & Integration Tests
- CLAIMS_MAPPING.md - Reference for required and optional claims
- ANGULAR_INTEGRATION.md - Integration guide for Angular applications
Support
For issues, questions, or contributions, visit:
- GitHub: https://github.com/primus-saas/identity-validator
- Documentation: https://docs.primus-saas.com
License
MIT License - see LICENSE file for details
| Product | Versions 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 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 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. |
-
net10.0
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 10.0.5)
- Swashbuckle.AspNetCore.SwaggerGen (>= 10.1.7)
- System.IdentityModel.Tokens.Jwt (>= 8.16.0)
-
net8.0
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 8.0.22)
- Microsoft.Extensions.Options (>= 10.0.5)
- Swashbuckle.AspNetCore.SwaggerGen (>= 10.1.7)
- System.IdentityModel.Tokens.Jwt (>= 8.16.0)
-
net9.0
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 9.0.11)
- Microsoft.Extensions.Options (>= 10.0.5)
- Swashbuckle.AspNetCore.SwaggerGen (>= 10.1.7)
- System.IdentityModel.Tokens.Jwt (>= 8.16.0)
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.1 | 95 | 4/19/2026 |
| 2.0.0 | 321 | 1/12/2026 |
| 1.5.0 | 863 | 12/3/2025 |
| 1.3.6 | 286 | 11/30/2025 |
| 1.3.5 | 271 | 11/30/2025 |
| 1.3.3 | 282 | 11/29/2025 |
| 1.3.2 | 141 | 11/29/2025 |
| 1.3.1 | 150 | 11/28/2025 |
| 1.3.0 | 217 | 11/24/2025 |
| 1.2.3 | 210 | 11/24/2025 |
| 1.2.2 | 209 | 11/24/2025 |
| 1.2.1 | 205 | 11/24/2025 |
| 1.2.0 | 200 | 11/23/2025 |
| 1.1.0 | 168 | 11/23/2025 |
| 1.0.0 | 324 | 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.