Dynamic Client Registration (DCR) Runbook — BFF Client
Purpose
- Ensure the BFF registers/updates its OIDC client in the IdP and rotates signing keys safely without clobbering other client settings.
Scope
- Applies to the BFF
private_key_jwtclient managed byms_bff/src/bootstrap/dcr_bootstrap.py. - Uses inline
jwkson the client record (notjwks_uri). No IdP restart is required.
See also IdP guide: services/idp/how-to/mint-iat-and-dcr for minting the Initial Access Token (IAT) and performing DCR.
- IdP DCR guide:
IdP/docs/DCR_Guide.md - Bootstrap helper:
ms_bff_spike/ms_bff/src/bootstrap/dcr_bootstrap.py - Compose wiring:
CRUDService/docker-compose-authzen4.yml(bff service env)
Key environment variables
- IDP_BASE_URL: internal IdP base (e.g.,
http://idp-app:8002) - DCR_ENABLED: "true" to run bootstrap on start
- SKIP_DCR_BOOTSTRAP: set "true" to skip
- DCR_IAT: Initial Access Token for first-time registration
- DCR_CLIENT_ID: Desired client id (e.g.,
bff-server) - DCR_CLIENT_PROFILE: Template/profile (e.g.,
code-flow-pkjwt) - DCR_SIGNING_KEY: Path to PEM private key (e.g.,
/app/keys/bff-sig-001.pem) - BFF_KID: Kid to advertise in JWKS (e.g.,
bff-sig-001) - DCR_REDIRECT_URIS: Optional list; if set, see redirect update modes below
Rotation controls (non-destructive)
- DCR_ROTATION_SAFE: "true" → enables safe rotation behavior; forces redirect merge
- DCR_REDIRECT_UPDATE_MODE: "auto" | "replace" | "merge" | "none"
- auto (default): only update if provided and differs
- replace: use provided values exactly
- merge: union of existing + provided (order preserved, no dups)
- none: never modify redirect_uris
- BFF_JWK_ROTATE_AFTER_DAYS: Days after which a new key is generated in-memory (default 30)
- BFF_JWK_RETIRE_AFTER_DAYS: Retire very old keys by kid timestamp (default 60; ≤0 disables)
- BFF_JWK_MAX_KEYS: Cap the number of keys kept (default 3; ≤0 disables cap)
Safe rotation — TL;DR
- Set env to avoid redirect churn and keep overlap during rotation:
DCR_ROTATION_SAFE=true
# optional explicit controls
# DCR_REDIRECT_UPDATE_MODE=merge
# BFF_JWK_MAX_KEYS=3
# BFF_JWK_RETIRE_AFTER_DAYS=60
# BFF_JWK_ROTATE_AFTER_DAYS=30
- Deploy/restart the BFF. The bootstrapper will:
- Generate a new RSA key if the PEM is missing or older than rotate_after_days
- PATCH the client
jwks(andredirect_urisper mode) - Persist the new PEM only after PATCH succeeds
For an immediate rotation
- Temporarily set
BFF_JWK_ROTATE_AFTER_DAYS=0for one restart, or delete the PEM file so a new one is generated and published.
Redirect URIs
- If you provide
DCR_REDIRECT_URIS, the update behavior followsDCR_REDIRECT_UPDATE_MODE. - With
DCR_ROTATION_SAFE=true, merge semantics are used to avoid removing existing values.
RAT/IAT handling
- On first registration,
DCR_IATmust be present and valid. - If PATCH returns 401/403, the bootstrapper attempts RAT auto-renew when
BOOTSTRAP_ADMIN_TOKENis set; otherwise it raises a clear admin action message. - To issue a new RAT in the IdP: see the admin DCR endpoints (e.g.,
/api/admin/dcr/initial-access-tokensand/api/admin/dcr/clients/{client_id}/tokens).
Operational notes
- No IdP config or compose changes are needed for normal rotations; the IdP uses the stored inline JWKS immediately.
- Update only if any component has statically pinned the public key/kid outside IdP.
Observability
- Logs: look for "JWKS rotated successfully" and optional redirect updates.
- Metrics (when
PROM_PUSHGATEWAY_URLis set):bff_dcr_status{status="success|rat_cached|rat_invalid|jwks_rotation_failed"}.
Errors and fixes
-
No cached creds and missing
DCR_IAT→ provision a new IAT in IdP admin, setDCR_IAT, restart. -
invalid_tokenor expired IAT/RAT → issue a fresh token via IdP admin; restart. -
PATCH 4xx without auto-renew → confirm permissions/scopes, refresh IAT/RAT, or set
DCR_FORCE_REPLACE=trueand retry registration. -
Registers/updates the BFF’s OAuth client at the IdP at startup
-
Publishes/rotates the BFF’s JWKS (public keys for
private_key_jwt) -
Caches
client_idand theregistration_access_token(RAT) on disk -
Pushes a status metric to Prometheus Pushgateway when configured
What DCR does (in our stack)
- Registers/updates the BFF’s OAuth client at the IdP at startup
- Publishes/rotates the BFF’s JWKS (public keys for
private_key_jwt) - Caches
client_idand theregistration_access_token(RAT) on disk - Pushes a status metric to Prometheus Pushgateway when configured
High‑level flow (bootstrap)
How to set it up
- One-time bootstrap
DCR_ENABLED: "true"SKIP_DCR_BOOTSTRAP: "false"DCR_FORCE_REPLACE: "false"DCR_IAT: "<fresh IAT>"- Ensure the DCR payload includes
token_endpoint_auth_method: private_key_jwtandjwks_uri: https://<your-bff-host>/.well-known/jwks.json
- After success (and from then on)
- Remove
DCR_IATfrom environment/config - Keep:
DCR_ENABLED: "true",SKIP_DCR_BOOTSTRAP: "false",DCR_FORCE_REPLACE: "false" - Result: on every start the bootstrap is a no-op (client exists). It may PATCH client metadata when needed, but does not consume an IAT.
- Remove
Prerequisites (IdP side)
- DCR enabled per
DCR_Guide.mdand routes exposed (/api/oidc/registerCRUD) - Issue an Initial‑Access‑Token (IAT) for the BFF client
Enable DCR for the BFF
- Configure environment for the bff container (compose excerpt):
environment:
DCR_ENABLED: "true" # enable the feature in your entrypoint
SKIP_DCR_BOOTSTRAP: "false" # run the bootstrap helper on start
IDP_BASE_URL: http://idp-app:8002 # internal IdP base (container‑to‑container)
DCR_IAT: <initial_access_token> # IAT issued by IdP
DCR_CLIENT_ID: bff-server # desired client_id (optional)
DCR_CLIENT_PROFILE: code-flow-pkjwt # IdP client profile/template
DCR_REDIRECT_URIS: https://<host>/auth/callback # space or comma separated
DCR_SIGNING_KEY: /app/keys/bff-sig-001.pem # PEM used for private_key_jwt
BFF_KID: bff-sig-001 # kid advertised in JWKS
DCR_ROTATION_SAFE: "true" # non-destructive rotation mode
DCR_REDIRECT_UPDATE_MODE: auto # or merge|replace|none
BFF_CLIENT_CRED_PATH: /app/keys/bff_client.json # persisted creds JSON
BFF_JWK_ROTATE_AFTER_DAYS: "30" # in‑place rotation threshold
BFF_JWK_RETIRE_AFTER_DAYS: "60" # drop very old keys
PROM_PUSHGATEWAY_URL: http://prometheus-pushgateway:9091 # optional metrics
- Run the bootstrap on start. Options:
- Call the module from your entrypoint:
python -m ms_bff.src.bootstrap.dcr_bootstrap
- Or execute the script directly:
python /app/ms_bff_spike/ms_bff/src/bootstrap/dcr_bootstrap.py
- Verify
- IdP:
GET /api/oidc/register/{client_id}shows the BFF with the advertisedkid - BFF: creds JSON exists at
BFF_CLIENT_CRED_PATHwithclient_id(+ RAT when applicable) - Metrics: optionally,
bff_dcr_status{status="success"}=1is pushed to Pushgateway - JWKS: BFF serves
/.well-known/jwks.jsonand the client’sjwks_uriin IdP points to that URL
Operational notes (from code)
- If cached RAT is invalid/expired, the helper can use
BOOTSTRAP_ADMIN_TOKENto renew it via the IdP admin endpoint and retry the PATCH. - JWKS rotation is additive and capped to 3 keys; very old keys are dropped based on
BFF_JWK_RETIRE_AFTER_DAYS. - The helper is idempotent and can be run on every start; it only writes the PEM after a successful JWKS update/registration.
Auto-renew vs auto-rotate
- Access tokens: already auto-refreshed by the BFF’s background token refresh. Nothing to change.
- Registration/secret:
- Don’t use
client_secretrotation for security; preferprivate_key_jwt+jwks_uri. - To “auto-rotate,” rotate the BFF’s signing key and update the JWKS served at
/.well-known/jwks.json. - Best practice: publish both old and new keys in JWKS for a grace period, then drop the old key. The IdP will re-fetch via
jwks_uriautomatically. - This achieves continuous key rotation without using DCR again.
- Don’t use
Troubleshooting
- Missing IAT → bootstrap fails fast with a clear admin action message
- 401/403 from IdP → reissue RAT or set
DCR_FORCE_REPLACE=trueto re‑register - Logs include truncated error bodies and friendly messages from the helper
If you must rotate a shared client_secret (not recommended)
- The public DCR PATCH endpoint does not support updating
client_secret. - Use an admin path or out-of-band update in the IdP instead.
- Prefer
private_key_jwtwithjwks_urito avoid shared secret distribution.
See also
- IdP DCR Guide: how to turn on DCR and issue IATs —
IdP/docs/DCR_Guide.md