Multi-Issuer Configuration
Accept tokens from multiple identity providers (Auth0, Azure AD, Local JWT) in a single API. Perfect for enterprise scenarios with multiple IdPs or migration between providers.
Use Cases
| Scenario | Configuration |
|---|---|
| Enterprise with Azure AD + partner Auth0 | Azure AD + Auth0 |
| Migration from Auth0 to Azure AD | Azure AD + Auth0 |
| Production + Dev testing | External provider + Local JWT |
| Multi-tenant SaaS | Multiple Azure AD tenants |
| Full demo (all) | Azure AD + Auth0 + Local JWT |
Quick Setup: All Three Providers
appsettings.json
{
"PrimusIdentity": {
"RequireHttpsMetadata": true,
"ValidateLifetime": true,
"Issuers": [
{
"Name": "Auth0-Production",
"Type": "Auth0",
"Authority": "https://YOUR-TENANT.auth0.com/",
"Issuer": "https://YOUR-TENANT.auth0.com/",
"Audiences": [ "https://your-api" ]
},
{
"Name": "AzureAD-Corporate",
"Type": "AzureAD",
"Authority": "https://login.microsoftonline.com/YOUR-TENANT-ID/v2.0",
"Issuer": "https://login.microsoftonline.com/YOUR-TENANT-ID/v2.0",
"Audiences": [ "api://YOUR-CLIENT-ID" ]
},
{
"Name": "LocalDev",
"Type": "Jwt",
"Secret": "your-256-bit-secret-key-at-least-32-chars-long!!",
"Issuer": "https://local-dev-issuer",
"Audiences": [ "local-dev-api" ]
}
],
"Diagnostics": {
"EnableDetailedErrors": true,
"IncludeTokenHintsInChallenges": true,
"IncludeDebugHeaders": true,
"LogTokenRejectionReasons": true,
"MaxRecentFailures": 50,
"AutoDetectDevelopment": true
}
}
}
Program.cs
using PrimusSaaS.Identity.Validator;
var builder = WebApplication.CreateBuilder(args);
// Single line configures ALL issuers
builder.Services.AddPrimusIdentity(opts =>
builder.Configuration.GetSection("PrimusIdentity").Bind(opts));
builder.Services.AddControllers();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
That's it! Primus Identity automatically validates tokens from any configured issuer.
How Multi-Issuer Validation Works
graph TD
A[Incoming Token] --> B{Extract Issuer}
B --> C{Match Configured Issuer?}
C -->|Auth0| D[Validate with Auth0 JWKS]
C -->|Azure AD| E[Validate with Azure AD JWKS]
C -->|Local| F[Validate with Signing Key]
C -->|No Match| G[401 Unauthorized]
D --> H{Valid?}
E --> H
F --> H
H -->|Yes| I[Set User Principal]
H -->|No| G
- Token arrives with
Authorization: Bearer xxx - Primus extracts the
iss(issuer) claim - Matches issuer to configured provider
- Validates signature using provider's method
- Sets
HttpContext.Userwith claims
Complete Working Example
using PrimusSaaS.Identity.Validator;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
// Configure multi-issuer authentication
builder.Services.AddPrimusIdentity(opts =>
builder.Configuration.GetSection("PrimusIdentity").Bind(opts));
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI();
app.UseAuthentication();
app.UseAuthorization();
// Health check
app.MapGet("/", () => new {
status = "healthy",
auth = "Multi-Issuer (Auth0 + Azure AD + Local)"
});
// Works with ANY configured issuer
app.MapGet("/me", [Authorize] (HttpContext ctx) =>
{
// Detect which issuer authenticated this request
var issuer = ctx.User.FindFirst("iss")?.Value ?? "unknown";
var provider = DetectProvider(issuer);
return new {
provider,
issuer,
userId = ctx.User.FindFirst("sub")?.Value,
email = GetEmail(ctx.User, provider),
name = GetName(ctx.User, provider)
};
});
// Show all claims (debugging)
app.MapGet("/debug/claims", [Authorize] (HttpContext ctx) =>
{
var issuer = ctx.User.FindFirst("iss")?.Value;
return new {
issuer,
provider = DetectProvider(issuer),
claims = ctx.User.Claims.Select(c => new { c.Type, c.Value })
};
});
// Provider-specific endpoint example
app.MapGet("/azure-only", [Authorize] (HttpContext ctx) =>
{
var issuer = ctx.User.FindFirst("iss")?.Value ?? "";
if (!issuer.Contains("microsoftonline"))
{
return Results.Forbid();
}
return Results.Ok(new { message = "Azure AD users only!" });
});
app.Run();
// Helper methods
string DetectProvider(string? issuer)
{
if (string.IsNullOrEmpty(issuer)) return "unknown";
if (issuer.Contains("auth0")) return "Auth0";
if (issuer.Contains("microsoftonline") || issuer.Contains("sts.windows.net")) return "AzureAD";
if (issuer.Contains("local")) return "Local";
return "unknown";
}
string? GetEmail(ClaimsPrincipal user, string provider)
{
return provider switch
{
"AzureAD" => user.FindFirst("preferred_username")?.Value ?? user.FindFirst("email")?.Value,
"Auth0" => user.FindFirst("email")?.Value,
"Local" => user.FindFirst("email")?.Value,
_ => user.FindFirst("email")?.Value
};
}
string? GetName(ClaimsPrincipal user, string provider)
{
return user.FindFirst("name")?.Value;
}
Configuration Patterns
Pattern 1: Production + Development
{
"PrimusIdentity": {
"Issuers": [
{
"Name": "Auth0-Prod",
"Type": "Auth0",
"Authority": "https://prod-tenant.auth0.com/",
"Issuer": "https://prod-tenant.auth0.com/",
"Audiences": [ "https://api.example.com" ],
"AllowMachineToMachine": true
},
{
"Name": "LocalDev",
"Type": "Jwt",
"Secret": "dev-secret-key-at-least-32-characters!!",
"Issuer": "https://local-dev",
"Audiences": [ "local-api" ]
}
]
}
}
Use case: Production uses Auth0, developers can test with local tokens.
Pattern 2: Enterprise Migration
{
"PrimusIdentity": {
"Issuers": [
{
"Name": "Auth0-Legacy",
"Type": "Auth0",
"Authority": "https://legacy.auth0.com/",
"Issuer": "https://legacy.auth0.com/",
"Audiences": [ "https://api.example.com" ],
"AllowMachineToMachine": true
},
{
"Name": "AzureAD-New",
"Type": "AzureAD",
"Authority": "https://login.microsoftonline.com/new-tenant-id/v2.0",
"Issuer": "https://login.microsoftonline.com/new-tenant-id/v2.0",
"Audiences": [ "api://new-client-id" ]
}
]
}
}
Use case: Migrating from Auth0 to Azure AD while supporting both.
Pattern 3: Multi-Tenant SaaS
{
"PrimusIdentity": {
"Issuers": [
{
"Name": "Tenant-Acme",
"Type": "AzureAD",
"Authority": "https://login.microsoftonline.com/acme-tenant-id/v2.0",
"Issuer": "https://login.microsoftonline.com/acme-tenant-id/v2.0",
"Audiences": [ "api://app-client-id" ]
},
{
"Name": "Tenant-Contoso",
"Type": "AzureAD",
"Authority": "https://login.microsoftonline.com/contoso-tenant-id/v2.0",
"Issuer": "https://login.microsoftonline.com/contoso-tenant-id/v2.0",
"Audiences": [ "api://app-client-id" ]
},
{
"Name": "Tenant-Fabrikam",
"Type": "Auth0",
"Authority": "https://fabrikam.auth0.com/",
"Issuer": "https://fabrikam.auth0.com/",
"Audiences": [ "https://api.saas.com" ]
}
]
}
}
Use case: Different enterprise customers using different IdPs.
Pattern 4: B2B with Partner Access
{
"PrimusIdentity": {
"Issuers": [
{
"Name": "Internal-AzureAD",
"Type": "AzureAD",
"Authority": "https://login.microsoftonline.com/our-tenant-id/v2.0",
"Issuer": "https://login.microsoftonline.com/our-tenant-id/v2.0",
"Audiences": [ "api://internal-app-id" ]
},
{
"Name": "Partner-Auth0",
"Type": "Auth0",
"Authority": "https://partner.auth0.com/",
"Issuer": "https://partner.auth0.com/",
"Audiences": [ "https://partner-api" ]
}
]
}
}
Use case: Internal users via Azure AD, external partners via Auth0.
Environment-Based Configuration
appsettings.json (Base)
{
"PrimusIdentity": {
"ValidateLifetime": true,
"ClockSkew": "00:05:00"
}
}
appsettings.Development.json
{
"PrimusIdentity": {
"Diagnostics": {
"EnableDetailedErrors": true,
"IncludeTokenHintsInChallenges": true,
"IncludeDebugHeaders": true,
"LogTokenRejectionReasons": true,
"MaxRecentFailures": 50,
"AutoDetectDevelopment": true
},
"Issuers": [
{
"Name": "LocalDev",
"Type": "Jwt",
"Secret": "dev-secret-key-at-least-32-characters!!",
"Issuer": "https://local-dev",
"Audiences": [ "local-api" ]
}
]
}
}
appsettings.Production.json
{
"PrimusIdentity": {
"Diagnostics": {
"EnableDetailedErrors": false,
"IncludeTokenHintsInChallenges": false,
"IncludeDebugHeaders": false,
"LogTokenRejectionReasons": false,
"MaxRecentFailures": 0,
"AutoDetectDevelopment": false
},
"Issuers": [
{
"Name": "Auth0",
"Type": "Auth0",
"Authority": "https://prod.auth0.com/",
"Issuer": "https://prod.auth0.com/",
"Audiences": [ "https://api.example.com" ]
},
{
"Name": "AzureAD",
"Type": "AzureAD",
"Authority": "https://login.microsoftonline.com/prod-tenant-id/v2.0",
"Issuer": "https://login.microsoftonline.com/prod-tenant-id/v2.0",
"Audiences": [ "api://prod-client-id" ]
}
]
}
}
Testing Multi-Issuer
[Fact]
public async Task Api_AcceptsAuth0Token()
{
var token = GetAuth0TestToken(); // From Auth0 dashboard
_client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
var response = await _client.GetAsync("/me");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadFromJsonAsync<UserInfo>();
Assert.Equal("Auth0", body.Provider);
}
[Fact]
public async Task Api_AcceptsAzureAdToken()
{
var token = GetAzureAdTestToken(); // From Azure CLI
_client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
var response = await _client.GetAsync("/me");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadFromJsonAsync<UserInfo>();
Assert.Equal("AzureAD", body.Provider);
}
[Fact]
public async Task Api_AcceptsLocalToken()
{
var token = TestTokenGenerator.GenerateToken();
_client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
var response = await _client.GetAsync("/me");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadFromJsonAsync<UserInfo>();
Assert.Equal("Local", body.Provider);
}
[Fact]
public async Task Api_RejectsUnknownIssuer()
{
var token = CreateTokenWithUnknownIssuer();
_client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
var response = await _client.GetAsync("/me");
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
Troubleshooting
Error: "IDX10501: Signature validation failed"
Cause: Token from unconfigured issuer.
Debug:
var handler = new JwtSecurityTokenHandler();
var jwt = handler.ReadJwtToken(token);
Console.WriteLine($"Token issuer: {jwt.Issuer}");
// Compare with configured issuers
Error: Tokens from one provider work, others don't
Cause: Configuration mismatch.
Checklist:
- Authority URL correct for each provider
- Audience matches token's
audclaim - For Azure AD: correct TenantId and ClientId
- For Local: signing key matches
Next Steps
| Want to... | See Guide |
|---|---|
| Provider-specific setup | Auth0 -> / Azure AD -> / Local -> |
| Enable local/dev tokens | Local JWT Guide -> |
| Revisit basics quickly | Identity Quick Start -> |