Runtime Configuration

Local OpenIddict Certificates

How development OpenIddict certificates are persisted locally so DCR/MCP client tokens survive server restarts.

Local OpenIddict Certificates

Why file-backed certificates?

In local development, OpenIddict uses self-signed certificates to sign and encrypt tokens (access tokens, refresh tokens, authorization codes). These certificates must remain stable across server restarts so that OAuth/MCP clients registered through Dynamic Client Registration (DCR) — such as Cursor, VS Code, or the MCP Inspector — do not need to re-authenticate every time the server restarts.

OpenIddict's built-in AddDevelopmentSigningCertificate() and AddDevelopmentEncryptionCertificate() helpers persist certificates to the X.509 CurrentUser/My store. On macOS, that store is the login keychain, which cannot retain the RSA private key in a retrievable form. As a result, every process restart silently regenerates the key material, and tokens issued before the restart become invalid (causing invalid_grant errors and forcing a full re-authentication).

Zeeq's DevelopmentRuntimeSecretsProvider solves this by generating self-signed certificates once, persisting them as password-less PFX files on disk, and reloading them on every run.

Where the certificates live

By default, certificates are stored under:

.secrets/local-certs/
├── signing.pfx      # Used to sign tokens
└── encryption.pfx   # Used to encrypt tokens

This directory is relative to the server process working directory, which under Aspire is src/backend/Zeeq.Runtime.Server/. It sits alongside the existing Data Protection key ring at .secrets/data-protection-keys/.

The entire .secrets/ directory is gitignored — certificates are never committed.

How they work

  1. First run: The DevelopmentRuntimeSecretsProvider generates two 2048-bit RSA self-signed certificates with CN=Zeeq Development Signing Certificate and CN=Zeeq Development Encryption Certificate, exports them as password-less PFX files, and atomically writes them (temp file + move) to .secrets/local-certs/.
  2. Every subsequent run: The provider loads the existing PFX files from disk. The private key is loaded from the file data — the macOS keychain is never involved, so the key remains usable across restarts.
  3. Split worker mode: When ZEEQ_ASPIRE_MODE=split starts both zeeq-server and zeeq-worker concurrently, both processes converge on the same certificate material thanks to the atomic first-run write.

Rotating certificates

To rotate development certificates (which invalidates all existing dev tokens and triggers one re-authentication):

rm -rf src/backend/Zeeq.Runtime.Server/.secrets/local-certs/

The directory will be recreated with new certificates on the next server start.

Configuration

The certificate directory can be overridden via configuration:

{
  "Auth": {
    "OpenIddict": {
      "DevelopmentCertificatePath": "/path/to/custom/cert-dir"
    }
  }
}

Production

This is development-only. Production deployments continue to use ConfiguredCertificateRuntimeSecretsProvider, which loads managed PFX certificates from configured paths via Auth:OpenIddict:SigningCertificatePath and Auth:OpenIddict:EncryptionCertificatePath.

Optional: Clean up orphaned keychain certificates

If you previously ran the server with the old keychain-based development certificates, you may have orphaned entries in your login keychain. They are harmless but can be removed:

security delete-certificate -c "OpenIddict Server Signing Certificate" 2>/dev/null || true
security delete-certificate -c "OpenIddict Server Encryption Certificate" 2>/dev/null || true