External Token Validation
Configure Passport to accept and validate JWT tokens issued by an external identity provider — configure the issuer, JWKS URI, audience, and group claim mapping.
Federation Configuration
External IdP federation is configured in the tenant's identity provider settings. Each external IdP is registered with its issuer URL, JWKS URI, and group mapping configuration.
// Tenant federation configuration (stored in Passport)
POST /passport/admin/tenants/{tenantId}/federation
Authorization: Bearer {admin-token}
Content-Type: application/json
{
"providerId": "okta-main",
"displayName": "Okta (Main Directory)",
"issuer": "https://company.okta.com/oauth2/default",
"jwksUri": "https://company.okta.com/oauth2/default/v1/keys",
"audience": "api://bizfirstgo",
"tenantIdClaim": "tenant_id", // which claim contains the BizFirstGO tenant ID
"userIdClaim": "sub",
"emailClaim": "email",
"groupsClaim": "groups",
"groupMapping": {
"BizFirst-Admins": "admin",
"BizFirst-Managers": "manager",
"BizFirst-Users": "user",
"BizFirst-Viewers": "viewer"
}
}
appsettings.json Configuration
// Multi-issuer configuration in appsettings.json
{
"Passport": {
"Authority": "https://passport.bizfirst.ai", // default Passport issuer
"ExternalProviders": [
{
"ProviderId": "okta-main",
"Issuer": "https://company.okta.com/oauth2/default",
"JwksUri": "https://company.okta.com/oauth2/default/v1/keys",
"Audience": "api://bizfirstgo",
"TenantIdClaim": "tenant_id",
"UserIdClaim": "sub",
"EmailClaim": "email",
"GroupsClaim": "groups",
"JwksRefreshIntervalMinutes": 10
},
{
"ProviderId": "azure-ad",
"Issuer": "https://login.microsoftonline.com/{tenantId}/v2.0",
"JwksUri": "https://login.microsoftonline.com/{tenantId}/discovery/v2.0/keys",
"Audience": "api://bizfirstgo",
"GroupsClaim":"groups"
}
]
}
}
Implementing a Custom IExternalTokenProvider
// Custom provider — for IdPs not covered by built-in providers
public sealed class CustomEnterpriseTokenProvider : IExternalTokenProvider
{
private readonly IConfiguration _config;
private readonly IJwksCache _jwksCache;
public string Issuer => _config["Federation:CustomProvider:Issuer"]!;
public async Task<ExternalIdentityClaims?> ValidateTokenAsync(
string token,
CancellationToken ct)
{
try
{
// Fetch JWKS (cached)
var jwks = await _jwksCache.GetAsync(
_config["Federation:CustomProvider:JwksUri"]!, ct);
// Validate JWT
var handler = new JsonWebTokenHandler();
var result = await handler.ValidateTokenAsync(token, new TokenValidationParameters
{
ValidIssuer = Issuer,
ValidAudience = _config["Federation:CustomProvider:Audience"],
IssuerSigningKeys = jwks,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromSeconds(60)
});
if (!result.IsValid) return null;
var claims = result.ClaimsIdentity.Claims.ToDictionary(c => c.Type, c => c.Value);
return new ExternalIdentityClaims
{
UserId = claims.GetValueOrDefault("sub") ?? "",
TenantId = claims.GetValueOrDefault("tenant_id") ?? "",
Email = claims.GetValueOrDefault("email") ?? "",
DisplayName = claims.GetValueOrDefault("name"),
Groups = claims
.Where(c => c.Key == "groups")
.Select(c => c.Value)
.ToList()
};
}
catch (Exception)
{
return null;
}
}
}
// Registration
builder.Services.AddExternalTokenProvider<CustomEnterpriseTokenProvider>();
JWKS Caching
External IdPs' JWKS endpoints are cached to prevent excessive outbound calls. The cache is refreshed when a token references a kid (key ID) not found in the cache — standard JWKS rotation handling.
| Cache Behavior | Value |
|---|---|
| Default JWKS cache TTL | 10 minutes |
| Cache key | JWKS URI |
| Refresh trigger | Unknown kid in token header |
| Maximum refresh rate | 1 refresh per 30 seconds (prevents thundering herd) |
Always configure the Audience field for each external provider. Without audience validation, tokens issued by your IdP for other applications (Salesforce, Jira, etc.) would be accepted by BizFirstGO. This is a critical security boundary — never set ValidateAudience = false.