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
<PackageReference Include="PrimusSaaS.Identity.Validator" Version="2.0.0" />
<PackageVersion Include="PrimusSaaS.Identity.Validator" Version="2.0.0" />
<PackageReference Include="PrimusSaaS.Identity.Validator" />
paket add PrimusSaaS.Identity.Validator --version 2.0.0
#r "nuget: PrimusSaaS.Identity.Validator, 2.0.0"
#:package PrimusSaaS.Identity.Validator@2.0.0
#addin nuget:?package=PrimusSaaS.Identity.Validator&version=2.0.0
#tool nuget:?package=PrimusSaaS.Identity.Validator&version=2.0.0
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 |
| 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.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 & Session Persistence
- 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. - Persistent sessions (refresh tokens): Enable durable refresh storage without adding payment or billing code:
For dev-only, you may setbuilder.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.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-appβmy-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)
});
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 = 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 tokenβsissclaim, 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.
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 | 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. |
-
net6.0
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 6.0.36)
- Microsoft.Extensions.Options (>= 7.0.0)
- Swashbuckle.AspNetCore.SwaggerGen (>= 6.5.0)
- System.IdentityModel.Tokens.Jwt (>= 8.14.0)
-
net7.0
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 7.0.20)
- Microsoft.Extensions.Options (>= 7.0.0)
- Swashbuckle.AspNetCore.SwaggerGen (>= 6.5.0)
- System.IdentityModel.Tokens.Jwt (>= 8.14.0)
-
net8.0
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 8.0.22)
- Microsoft.Extensions.Options (>= 7.0.0)
- Swashbuckle.AspNetCore.SwaggerGen (>= 6.5.0)
- System.IdentityModel.Tokens.Jwt (>= 8.14.0)
-
net9.0
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 9.0.11)
- Microsoft.Extensions.Options (>= 7.0.0)
- Swashbuckle.AspNetCore.SwaggerGen (>= 6.5.0)
- System.IdentityModel.Tokens.Jwt (>= 8.14.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.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.