Managed Identity Credentials
A managed identity authenticates using a clientId and clientSecret — understand the credential lifecycle, the ManagedIdentityCredentialService API, and how secrets are stored and validated.
ManagedIdentityCredentialService
namespace BizFirst.Essentials.Passport.ManagedIdentities;
public interface IManagedIdentityCredentialService
{
// Generate a new secret for a managed identity
// Returns the plaintext secret — stored only once
Task<ManagedIdentitySecret> GenerateSecretAsync(
Guid managedIdentityId,
GenerateSecretRequest request,
CancellationToken ct = default);
// List all active secrets (IDs and metadata only — never plaintext)
Task<IReadOnlyList<ManagedIdentitySecretInfo>> ListSecretsAsync(
Guid managedIdentityId,
CancellationToken ct = default);
// Revoke a specific secret by its secretId
Task RevokeSecretAsync(
Guid managedIdentityId,
Guid secretId,
string reason,
CancellationToken ct = default);
// Validate clientId + clientSecret (used by authentication pipeline)
Task<ManagedIdentityValidationResult> ValidateAsync(
string clientId,
string clientSecret,
CancellationToken ct = default);
}
public class ManagedIdentitySecret
{
public required Guid SecretId { get; init; }
public required string ClientSecret { get; init; } // PLAINTEXT — shown once
public required string Label { get; init; } // e.g. "primary", "rotation-2026-05"
public DateTimeOffset CreatedAt { get; init; }
public DateTimeOffset? ExpiresAt { get; init; } // null = never expires
}
public class ManagedIdentitySecretInfo
{
public required Guid SecretId { get; init; }
public required string Label { get; init; }
public bool IsActive { get; init; }
public DateTimeOffset CreatedAt { get; init; }
public DateTimeOffset? ExpiresAt { get; init; }
public DateTimeOffset? LastUsedAt{ get; init; }
// ClientSecret is NEVER returned here — only in GenerateSecretAsync
}
Secret Storage Model
Passport never stores plaintext secrets. The clientSecret is hashed using Argon2id before persistence. The plaintext value is available only in the immediate creation response.
| What Is Stored | What Is Never Stored |
|---|---|
| Argon2id hash of the secret | Plaintext secret |
| SecretId (GUID) | Secret prefix for lookup optimization (stored separately) |
| Label, CreatedAt, ExpiresAt | Any reversible encoding of the secret |
| IsRevoked flag + revoked reason | Secret after revocation (hash is nulled) |
Once the GenerateSecretAsync response is returned, the plaintext secret is gone from Passport's memory. Store it immediately in a secrets manager. If you lose it, you must generate a new secret — there is no recovery path.
Generating an Additional Secret via API
// Generate a secondary secret (for rotation — old secret remains active)
POST /passport/admin/managed-identities/{managedIdentityId}/credentials/secrets
Authorization: Bearer {admin-token}
Content-Type: application/json
{
"label": "rotation-2026-05",
"expiresIn": "P90D" // ISO 8601 duration — 90 days from now
// Omit expiresIn for non-expiring secrets
}
// Response — SECRET SHOWN ONLY ONCE
{
"secretId": "sec-guid-5678",
"clientSecret": "mics_sk_yyyyyyyyyyyyyyyyyyyyyyyy",
"label": "rotation-2026-05",
"createdAt": "2026-05-25T10:00:00Z",
"expiresAt": "2026-08-23T10:00:00Z"
}
Listing Active Secrets
// List secrets — metadata only, never plaintext
GET /passport/admin/managed-identities/{managedIdentityId}/credentials/secrets
Authorization: Bearer {admin-token}
// Response
{
"secrets": [
{
"secretId": "sec-guid-1234",
"label": "primary",
"isActive": true,
"createdAt": "2026-01-01T00:00:00Z",
"expiresAt": null,
"lastUsedAt": "2026-05-25T09:55:00Z"
},
{
"secretId": "sec-guid-5678",
"label": "rotation-2026-05",
"isActive": true,
"createdAt": "2026-05-25T10:00:00Z",
"expiresAt": "2026-08-23T10:00:00Z",
"lastUsedAt": null
}
]
}
Revoking a Secret
// Revoke old secret after rotation is complete
DELETE /passport/admin/managed-identities/{managedIdentityId}/credentials/secrets/{secretId}
Authorization: Bearer {admin-token}
Content-Type: application/json
{
"reason": "rotation-complete"
}
// Response
{
"secretId": "sec-guid-1234",
"revokedAt": "2026-05-25T12:00:00Z",
"reason": "rotation-complete"
}
// All tokens obtained using the revoked secret are immediately invalidated
// Active tokens from this secret are added to the revocation list
Secret Expiry vs Revocation
| Mechanism | Trigger | Tokens Effect | Use Case |
|---|---|---|---|
| Secret Expiry | Automatic at expiresAt | Cannot obtain new tokens; existing tokens remain valid until their exp | Time-bounded service accounts |
| Secret Revocation | Explicit DELETE call | All tokens from this secret immediately invalidated | Security incident, completed rotation |
| Identity Disable | Explicit disable call | All tokens from all secrets immediately invalidated | Decommissioning, security incident |
Secret Labels and Multi-Secret Support
A managed identity can hold multiple active secrets simultaneously. This is the foundation of zero-downtime credential rotation. Each secret has a label — use labels to identify the purpose or generation of each secret:
// Recommended label conventions
"primary" // the current operational secret
"rotation-YYYY-MM" // the incoming replacement secret during rotation
"break-glass" // emergency access secret (stored in separate vault)
"ci-pipeline" // secret used specifically by CI/CD systems
// A managed identity typically has 1 active secret
// During rotation: 2 active secrets (old + new)
// After rotation: 1 active secret (new only)
Multiple active secrets for a single managed identity are all scoped to the same tenant and have the same roles. They are equivalent for authentication purposes — they produce identical tokens. Multiple secrets exist only to facilitate zero-downtime rotation, not to separate permissions.