Local OpenIddict Certificates
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
- First run: The
DevelopmentRuntimeSecretsProvidergenerates two 2048-bit RSA self-signed certificates withCN=Zeeq Development Signing CertificateandCN=Zeeq Development Encryption Certificate, exports them as password-less PFX files, and atomically writes them (temp file + move) to.secrets/local-certs/. - 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.
- Split worker mode: When
ZEEQ_ASPIRE_MODE=splitstarts bothzeeq-serverandzeeq-workerconcurrently, 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