Portal Community

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 BehaviorValue
Default JWKS cache TTL10 minutes
Cache keyJWKS URI
Refresh triggerUnknown kid in token header
Maximum refresh rate1 refresh per 30 seconds (prevents thundering herd)
Audience Validation Is Mandatory

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.