Portal Community

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 StoredWhat Is Never Stored
Argon2id hash of the secretPlaintext secret
SecretId (GUID)Secret prefix for lookup optimization (stored separately)
Label, CreatedAt, ExpiresAtAny reversible encoding of the secret
IsRevoked flag + revoked reasonSecret after revocation (hash is nulled)
Secrets Cannot Be Recovered

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

MechanismTriggerTokens EffectUse Case
Secret ExpiryAutomatic at expiresAtCannot obtain new tokens; existing tokens remain valid until their expTime-bounded service accounts
Secret RevocationExplicit DELETE callAll tokens from this secret immediately invalidatedSecurity incident, completed rotation
Identity DisableExplicit disable callAll tokens from all secrets immediately invalidatedDecommissioning, 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)
Multi-Secret is Not Multi-Tenant

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.