-
Notifications
You must be signed in to change notification settings - Fork 68
Howto Rotate OIDC secrets
Rotate OIDC client secrets when a secret is compromised, when your security policy requires periodic rotation, or when you're decommissioning a client registration.
Scope: this covers the Bindery-side client secret (what Bindery uses to authenticate with your IdP). It does not cover rotating the IdP's own signing keys (JWKS rotation) — that is handled automatically by Bindery's JWKS cache.
The procedure varies by IdP:
Authelia: Edit the clients entry in configuration.yml. Replace the secret value with a new one generated via openssl rand -hex 32. Restart Authelia.
Authentik: Go to Applications → Providers → [your Bindery provider] → Edit. Regenerate the client secret. Copy the new value.
Keycloak: Go to Clients → bindery → Credentials → Regenerate. Copy the new client secret.
Google: Go to Google Cloud Console → Credentials → [your OAuth client] → Edit. Click Reset secret. Copy the new secret.
At this point the old secret is immediately invalid at the IdP — existing Bindery sessions are still valid (they use the HMAC session cookie, not the OIDC client secret), but new logins will fail until step 2 is complete.
Warning
There is no per-provider endpoint. The OIDC providers API works on the whole list at once ([]ProviderConfig). A PUT replaces the entire provider list with whatever you send. If you PUT a single-provider array while you have multiple providers configured, you will delete every other provider. Always fetch the full array, edit one entry, and PUT the full array back.
Go to Settings → General → SSO / OIDC Providers, open the provider, paste the new secret, and save. The UI fetches the full list and reposts it for you, so you can't accidentally drop other providers.
The endpoint uses secret-preservation semantics on a per-provider basis: for an existing provider, an empty client_secret keeps its stored secret; a new provider with an empty client_secret is rejected with 400. This is what lets you repost the full array without knowing the other providers' secrets.
Step 2a — fetch the full array (secrets come back empty/omitted):
curl -s http://bindery:8787/api/v1/auth/oidc/providers \
-H "X-Api-Key: <admin-key>" > providers.json
# Returns a JSON ARRAY of all providers; client_secret is empty/omittedStep 2b — edit just the one provider's client_secret in providers.json, leaving every other provider entry exactly as returned (their empty secrets will be preserved on write).
Step 2c — PUT the full array back:
curl -X PUT http://bindery:8787/api/v1/auth/oidc/providers \
-H "X-Api-Key: <admin-key>" \
-H "Content-Type: application/json" \
--data @providers.jsonThe rotated provider gets its new secret; every other provider keeps its stored secret because its client_secret was left empty.
Expected result: 200 OK. No restart required.
Open a private/incognito window and complete a login via the provider. If the login succeeds, the rotation is complete.
Expected result: successful login, existing sessions unaffected.
Users already logged in should not be logged out. Their HMAC session cookies are independent of the OIDC client secret. Ask a logged-in user to confirm they can still access Bindery — or check via API:
# This should return 200 if the session cookie is still valid
curl -s -b "bindery_session=<existing-cookie>" \
http://bindery:8787/api/v1/auth/status | jq .authenticatedThere is no .../providers/<id> route and no separate create route. Everything goes through two endpoints that operate on the entire provider list:
| Endpoint | Body | Effect |
|---|---|---|
GET /api/v1/auth/oidc/providers |
— | Returns a JSON array of all providers; client_secret empty/omitted |
PUT /api/v1/auth/oidc/providers |
JSON array of ALL providers ([]ProviderConfig) |
Replaces the whole list with what you send |
Warning
Because PUT replaces the entire list, never send a single-provider array when you have more than one provider — the others are deleted. Always read-modify-write the full array.
client_secret is never returned by GET. On PUT, each array entry is evaluated independently:
client_secret for that entry |
Provider already exists? | Effect |
|---|---|---|
| Non-empty string | yes/no | Secret set to the new value |
| Empty / omitted | existing id | Stored secret is preserved |
| Empty / omitted | new id | Rejected: 400 client_secret required for new provider: <id>
|
This is what lets you repost the full array after editing one secret: every untouched provider keeps its empty client_secret, so its stored secret is preserved.
# 1. Fetch the full array
curl -s http://bindery:8787/api/v1/auth/oidc/providers \
-H "X-Api-Key: <admin-key>" > providers.json
# 2. Edit ONLY the target provider's client_secret in providers.json
# (leave every other entry's client_secret empty, as returned)
# 3. PUT the full array back
curl -X PUT http://bindery:8787/api/v1/auth/oidc/providers \
-H "X-Api-Key: <admin-key>" \
-H "Content-Type: application/json" \
--data @providers.jsonAppend a new entry (with a non-empty client_secret) to the array from GET, then PUT the whole array. A new entry with an empty secret returns 400 client_secret required for new provider: <id>.
The session signing secret is separate from OIDC client secrets. Rotating it invalidates every active Bindery session immediately for all users — use this only for security incidents, not routine rotation.
Settings → General → Security → Rotate session secret, or:
curl -X POST http://bindery:8787/api/v1/auth/session-secret/rotate \
-H "X-Api-Key: <admin-key>"Expected result: all users are logged out and must re-authenticate. OIDC users are redirected to their IdP login page; local-password users see the Bindery login form.
| Symptom | Cause | Fix |
|---|---|---|
New logins fail with invalid_client after updating Bindery |
Old secret still cached somewhere, or PUT body was malformed | Verify the update: GET /api/v1/auth/oidc/providers — check the provider's client_id. Re-PUT the full array with the correct secret. |
| Some OIDC providers vanished after a rotation | You PUT a single-provider array — it replaced the whole list | Re-add them: GET the (now shortened) array, append the missing providers with their secrets, PUT the full array. Always read-modify-write. |
invalid_client immediately after IdP rotation |
Bindery hasn't been updated yet (step 1 completed but not step 2) | Complete step 2 — update the secret in Bindery |
| Session secret rotation logged everyone out | Expected — that's what it does | Warn users before rotating the session secret. It is not the same as rotating the OIDC client secret. |
| Want to rotate secret without knowing the other providers' secrets |
client_secret is not returned by GET |
Repost the full array with empty secrets on the others — existing providers preserve their stored secret (see API scripting notes above) |
PUT with empty client_secret silently leaves old secret active |
Secret-preservation semantics — empty string means "keep existing" | Pass the new secret value explicitly; don't pass "" thinking it clears the field |
400 Bad Request when adding a new provider |
New providers require a non-empty client_secret
|
Append the new provider (with a non-empty client_secret) to the array and PUT the full list — there is no separate create route |
See also: Troubleshooting — OIDC | docs/auth-oidc.md
Getting started
Setup guides
How-to guides — proxy auth (v1.0)
How-to guides — OIDC (v1.0)
- Google Sign-In
- GitHub OAuth via Dex
- Authelia as OIDC provider
- Authentik
- Keycloak
- Rotate OIDC client secrets
- Recover from broken OIDC
How-to guides — multi-user (v1.0)
Reference
Contributing