Portal Community

Client Credentials Flow (Full Example)

// Step 1: Exchange clientId + clientSecret for a token
POST /passport/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64("mi-payroll-scheduler-clientid:mics_sk_xxxxxxxxxxxxxxxxxxxxxxxx")

grant_type=client_credentials
&scope=openid%20roles

// Response
{
  "access_token": "eyJhbGciOiJSUzI1NiJ9...",
  "token_type":   "Bearer",
  "expires_in":   3600
}

// Step 2: Use the token in API calls
GET /bizfirstgo/api/payroll/records?month=2026-05
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...

// The receiving service validates the token using Passport's JWKS:
// GET https://passport.bizfirst.ai/.well-known/jwks.json

ManagedIdentityHttpClient

Use the provided ManagedIdentityHttpClient helper to handle token acquisition, caching, and automatic refresh. It uses a token cache keyed on the clientId and refreshes when the token is within 60 seconds of expiry:

namespace BizFirst.Essentials.Passport.ManagedIdentities;

// Register in DI
services.AddManagedIdentityHttpClient(options =>
{
    options.PassportBaseUrl = "https://passport.bizfirst.ai";
    options.ClientId        = configuration["ManagedIdentity:ClientId"];
    options.ClientSecretRef = configuration["ManagedIdentity:SecretReference"];
    options.SecretProvider  = SecretProvider.AzureKeyVault;

    // Token cache settings
    options.CacheTokens      = true;
    options.RefreshThreshold = TimeSpan.FromSeconds(60); // refresh 60s before expiry
});

// Usage — ManagedIdentityHttpClient automatically attaches Bearer token
public class ErpSyncService
{
    private readonly ManagedIdentityHttpClient _http;

    public ErpSyncService(ManagedIdentityHttpClient http)
        => _http = http;

    public async Task SyncAsync(CancellationToken ct)
    {
        // Token is automatically fetched and cached
        // Refreshed transparently when nearing expiry
        var response = await _http.GetAsync(
            "/api/erp/accounts?since=2026-05-01", ct);

        response.EnsureSuccessStatusCode();
        var accounts = await response.Content.ReadFromJsonAsync<AccountList>(ct);
        // ...
    }
}

Token Caching Strategy

ScenarioRecommended ApproachReason
Single service instanceIn-memory cache (MemoryCache)Simple, fast, no external dependency
Multiple instances (scale-out)Distributed cache (Redis)Avoid thundering herd — all instances share one token
Serverless / short-lived functionsNo cache — request per invocationNo warm state; 1-hour token is cheap to obtain
High-frequency micro-servicesSidecar token proxyCentralize token management; reduce credential surface area

Manual Token Acquisition (Without Helper)

// Direct implementation for custom environments
public class ManualTokenService
{
    private readonly HttpClient       _http;
    private readonly ISecretsManager  _secrets;

    public async Task<string> GetTokenAsync(
        string clientId,
        string secretReference,
        CancellationToken ct)
    {
        var clientSecret = await _secrets.GetAsync(secretReference, ct);

        var credentials = Convert.ToBase64String(
            Encoding.UTF8.GetBytes($"{clientId}:{clientSecret}"));

        using var request = new HttpRequestMessage(
            HttpMethod.Post, "https://passport.bizfirst.ai/passport/token");

        request.Headers.Authorization =
            new AuthenticationHeaderValue("Basic", credentials);

        request.Content = new FormUrlEncodedContent(new[]
        {
            new KeyValuePair<string,string>("grant_type", "client_credentials"),
            new KeyValuePair<string,string>("scope",      "openid roles")
        });

        var response = await _http.SendAsync(request, ct);
        response.EnsureSuccessStatusCode();

        var json  = await response.Content.ReadFromJsonAsync<TokenResponse>(ct);
        return json!.AccessToken;
    }
}

Receiving Service — Token Validation

The receiving service does not need special handling for managed identity tokens. Standard Passport token validation applies — the token is validated against the JWKS endpoint. The only difference is the presence of is_service_account: true in the payload.

// AddPassportAuthentication() in the receiving service — no changes needed
builder.Services.AddPassportAuthentication(options =>
{
    options.Authority = "https://passport.bizfirst.ai";
    options.Audience  = "bizfirstgo-api";
    // Managed identity tokens are validated identically to user tokens
});

// Optional: restrict an endpoint to service accounts only
[HttpPost("internal/sync")]
[RequireServiceAccount]  // custom attribute checking IDInfo.IsServiceAccount
public async Task<IActionResult> InternalSync(
    [FromServices] IDInfo caller,
    CancellationToken ct)
{
    if (!caller.IsServiceAccount)
        return Forbid("Endpoint is restricted to service accounts");

    // ...process
}
Do Not Share Managed Identity Credentials Across Services

Create a dedicated managed identity for each service that needs to make authenticated calls. Sharing credentials between services means you cannot revoke access for one service without affecting the other, and audit logs cannot distinguish which service made which calls.

Propagating Identity vs Using Managed Identity

When Service A (acting on behalf of User X) calls Service B, there are two patterns: (1) propagate the user's token to Service B — Service B sees User X as the caller; (2) use Service A's managed identity token — Service B sees the service account. Use pattern 1 when Service B needs to apply user-level permissions. Use pattern 2 for background system calls where no user context exists.