From 20e4ba23d85f87ef3237ac7178c73c235c3e902e Mon Sep 17 00:00:00 2001 From: scabell Date: Fri, 23 Jan 2026 10:15:47 -0700 Subject: [PATCH 1/8] doc changes for client secret rotation --- docs/hydra/guides/oauth2-clients.mdx | 122 +++++++++++++++++++++++++++ docs/reference/api.json | 86 +++++++++++++++++++ 2 files changed, 208 insertions(+) diff --git a/docs/hydra/guides/oauth2-clients.mdx b/docs/hydra/guides/oauth2-clients.mdx index 9de5b81c38..0566fd4755 100644 --- a/docs/hydra/guides/oauth2-clients.mdx +++ b/docs/hydra/guides/oauth2-clients.mdx @@ -131,6 +131,128 @@ See [API documentation](../../reference/api#tag/oAuth2/operation/patchOAuth2Clie ```` +## Rotate OAuth2 client secret + +OAuth2 client secret rotation allows you to change a client's secret without downtime. When you rotate a secret, the old secret remains valid until you explicitly clean it up, allowing you to update all services using the client credentials without service interruption. + +### How secret rotation works + +1. **Rotate the secret**: Generate a new secret for the client +2. **Both secrets work**: Old and new secrets both authenticate until cleanup +3. **Update services**: Update your applications to use the new secret +4. **Cleanup**: Manually remove old rotated secrets once all services are updated + +### Rotate client secret + +To rotate an OAuth2 client secret, use the following methods: + +````mdx-code-block + + + +```bash +curl -X POST https://{project.slug}.projects.oryapis.com/admin/clients/{client-id}/secret/rotate \ + -H "Authorization: Bearer ory_pat_..." +``` + +The response includes the new `client_secret`. **Save this value immediately** - it will not be shown again. + +See [API documentation](../../reference/api#tag/oAuth2/operation/oauth2RotateClientSecret). + + + + +```typescript +import { Configuration, OAuth2Api } from "@ory/client" + +const ory = new OAuth2Api( + new Configuration({ + basePath: `https://${projectSlug}.projects.oryapis.com`, + accessToken: "ory_pat_..." + }) +) + +const { data: client } = await ory.oauth2RotateClientSecret({ + id: clientId +}) + +// Save the new client_secret immediately +console.log("New secret:", client.client_secret) +``` + + + +```` + +### Clear rotated secrets + +Once all services have been updated to use the new secret, you can remove the old rotated secrets to revoke access using the old credentials: + +````mdx-code-block + + + +```bash +curl -X DELETE https://{project.slug}.projects.oryapis.com/admin/clients/{client-id}/secret/rotate \ + -H "Authorization: Bearer ory_pat_..." +``` + +After cleanup, only the current secret will be valid. Old secrets will no longer authenticate. + +See [API documentation](../../reference/api#tag/oAuth2/operation/oauth2DeleteRotatedClientSecrets). + + + + +```typescript +await ory.oauth2DeleteRotatedClientSecrets({ + id: clientId +}) + +// Old secrets are now revoked +``` + + + +```` + +### Secret rotation workflow example + +Here's a complete workflow for rotating a client secret: + +```bash +# 1. Get current client +CLIENT_ID="your-client-id" + +# 2. Rotate the secret +NEW_SECRET=$(curl -X POST "https://{project.slug}.projects.oryapis.com/admin/clients/$CLIENT_ID/secret/rotate" \ + -H "Authorization: Bearer ory_pat_..." | jq -r '.client_secret') + +echo "New secret: $NEW_SECRET" + +# 3. Update your applications with the new secret +# (Both old and new secrets work during this period) + +# 4. Verify the new secret works +curl -X POST "https://{project.slug}.projects.oryapis.com/oauth2/token" \ + -u "$CLIENT_ID:$NEW_SECRET" \ + -d "grant_type=client_credentials" + +# 5. Once all services are updated, clean up old secrets +curl -X DELETE "https://{project.slug}.projects.oryapis.com/admin/clients/$CLIENT_ID/secret/rotate" \ + -H "Authorization: Bearer ory_pat_..." + +# Old secret is now revoked +``` + +:::tip Zero-downtime credential rotation +Secret rotation enables zero-downtime credential updates. Both the old and new secrets remain valid until you manually clean up the rotated secrets, allowing you to update all your services without service interruption. +::: + +:::warning Security best practice +Rotated secrets remain valid indefinitely until you explicitly clean them up. Always remove old rotated secrets once your migration is complete to ensure that compromised credentials cannot be used. +::: + ## Delete OAuth2 client To delete an existing OAuth2 client, use the following methods: diff --git a/docs/reference/api.json b/docs/reference/api.json index a45f6dd220..ef1d3a9eea 100644 --- a/docs/reference/api.json +++ b/docs/reference/api.json @@ -6914,6 +6914,10 @@ "format": "date-time", "type": "string" }, + "rotated_secrets": { + "description": "OAuth 2.0 Client Rotated Secrets\n\nRotatedSecrets holds previously rotated secrets that are still valid for authentication. This allows for secret rotation without downtime. The secrets are stored in hashed format and remain valid until explicitly cleaned up.", + "type": "string" + }, "userinfo_signed_response_alg": { "description": "OpenID Connect Request Userinfo Signed Response Algorithm\n\nJWS alg algorithm [JWA] REQUIRED for signing UserInfo Responses. If this is specified, the response will be JWT\n[JWT] serialized, and signed using JWS. The default, if omitted, is for the UserInfo Response to return the Claims\nas a UTF-8 encoded JSON object using the application/json content-type.", "type": "string" @@ -11948,6 +11952,88 @@ "tags": ["oAuth2"] } }, + "/admin/clients/{id}/secret/rotate": { + "post": { + "description": "Rotate the client secret of an OAuth 2.0 Client. The old secret will be moved to the rotated_secrets list and will remain valid until explicitly cleaned up. This allows for zero-downtime credential rotation.\n\nThe new secret will be returned in the response. Make sure to store it securely as it will not be retrievable later.", + "operationId": "oauth2RotateClientSecret", + "parameters": [ + { + "description": "OAuth 2.0 Client ID", + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/oAuth2Client" + } + } + }, + "description": "oAuth2Client" + }, + "404": { + "$ref": "#/components/responses/errorOAuth2NotFound" + }, + "default": { + "$ref": "#/components/responses/errorOAuth2Default" + } + }, + "security": [ + { + "oryAccessToken": [] + } + ], + "summary": "Rotate OAuth2 Client Secret", + "tags": ["oAuth2"] + }, + "delete": { + "description": "Delete all rotated secrets from an OAuth 2.0 Client. This will revoke access for all previously rotated secrets, leaving only the current secret valid.\n\nUse this endpoint after you have updated all services to use the new secret following a rotation.", + "operationId": "oauth2DeleteRotatedClientSecrets", + "parameters": [ + { + "description": "OAuth 2.0 Client ID", + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/oAuth2Client" + } + } + }, + "description": "oAuth2Client" + }, + "404": { + "$ref": "#/components/responses/errorOAuth2NotFound" + }, + "default": { + "$ref": "#/components/responses/errorOAuth2Default" + } + }, + "security": [ + { + "oryAccessToken": [] + } + ], + "summary": "Delete OAuth2 Client Rotated Secrets", + "tags": ["oAuth2"] + } + }, "/admin/courier/messages": { "get": { "description": "Lists all messages by given status and recipient.", From 092745e4112627a23460ebf8f53746695dbb3a3e Mon Sep 17 00:00:00 2001 From: scabell Date: Fri, 23 Jan 2026 10:34:17 -0700 Subject: [PATCH 2/8] run format --- docs/hydra/guides/oauth2-clients.mdx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/hydra/guides/oauth2-clients.mdx b/docs/hydra/guides/oauth2-clients.mdx index 0566fd4755..9061581c14 100644 --- a/docs/hydra/guides/oauth2-clients.mdx +++ b/docs/hydra/guides/oauth2-clients.mdx @@ -133,7 +133,9 @@ See [API documentation](../../reference/api#tag/oAuth2/operation/patchOAuth2Clie ## Rotate OAuth2 client secret -OAuth2 client secret rotation allows you to change a client's secret without downtime. When you rotate a secret, the old secret remains valid until you explicitly clean it up, allowing you to update all services using the client credentials without service interruption. +OAuth2 client secret rotation allows you to change a client's secret without downtime. When you rotate a secret, the old secret +remains valid until you explicitly clean it up, allowing you to update all services using the client credentials without service +interruption. ### How secret rotation works @@ -186,7 +188,8 @@ console.log("New secret:", client.client_secret) ### Clear rotated secrets -Once all services have been updated to use the new secret, you can remove the old rotated secrets to revoke access using the old credentials: +Once all services have been updated to use the new secret, you can remove the old rotated secrets to revoke access using the old +credentials: ````mdx-code-block @@ -245,13 +248,12 @@ curl -X DELETE "https://{project.slug}.projects.oryapis.com/admin/clients/$CLIEN # Old secret is now revoked ``` -:::tip Zero-downtime credential rotation -Secret rotation enables zero-downtime credential updates. Both the old and new secrets remain valid until you manually clean up the rotated secrets, allowing you to update all your services without service interruption. -::: +:::tip Zero-downtime credential rotation Secret rotation enables zero-downtime credential updates. Both the old and new secrets +remain valid until you manually clean up the rotated secrets, allowing you to update all your services without service +interruption. ::: -:::warning Security best practice -Rotated secrets remain valid indefinitely until you explicitly clean them up. Always remove old rotated secrets once your migration is complete to ensure that compromised credentials cannot be used. -::: +:::warning Security best practice Rotated secrets remain valid indefinitely until you explicitly clean them up. Always remove old +rotated secrets once your migration is complete to ensure that compromised credentials cannot be used. ::: ## Delete OAuth2 client From cf5d2be078e84a178f6b65b6472227416a5f33ea Mon Sep 17 00:00:00 2001 From: Arne Luenser Date: Tue, 10 Feb 2026 17:57:03 +0100 Subject: [PATCH 3/8] fix: update API documentation links for OAuth2 client secrets --- docs/hydra/guides/oauth2-clients.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/hydra/guides/oauth2-clients.mdx b/docs/hydra/guides/oauth2-clients.mdx index 9061581c14..4c3493eb18 100644 --- a/docs/hydra/guides/oauth2-clients.mdx +++ b/docs/hydra/guides/oauth2-clients.mdx @@ -159,7 +159,7 @@ curl -X POST https://{project.slug}.projects.oryapis.com/admin/clients/{client-i The response includes the new `client_secret`. **Save this value immediately** - it will not be shown again. -See [API documentation](../../reference/api#tag/oAuth2/operation/oauth2RotateClientSecret). +See [API documentation](../../reference/api#tag/oAuth2/operation/rotateOAuth2ClientSecret). @@ -174,7 +174,7 @@ const ory = new OAuth2Api( }) ) -const { data: client } = await ory.oauth2RotateClientSecret({ +const { data: client } = await ory.rotateOAuth2ClientSecret({ id: clientId }) @@ -202,13 +202,13 @@ curl -X DELETE https://{project.slug}.projects.oryapis.com/admin/clients/{client After cleanup, only the current secret will be valid. Old secrets will no longer authenticate. -See [API documentation](../../reference/api#tag/oAuth2/operation/oauth2DeleteRotatedClientSecrets). +See [API documentation](../../reference/api#tag/oAuth2/operation/deleteRotatedOAuth2ClientSecrets). ```typescript -await ory.oauth2DeleteRotatedClientSecrets({ +await ory.deleteRotatedOAuth2ClientSecrets({ id: clientId }) From 018837b94f8605bd68eb64191a8a0c2d3bf36b50 Mon Sep 17 00:00:00 2001 From: Arne Luenser Date: Tue, 10 Feb 2026 18:03:55 +0100 Subject: [PATCH 4/8] fix: revert auto-generated api.json --- docs/reference/api.json | 86 ----------------------------------------- 1 file changed, 86 deletions(-) diff --git a/docs/reference/api.json b/docs/reference/api.json index ef1d3a9eea..a45f6dd220 100644 --- a/docs/reference/api.json +++ b/docs/reference/api.json @@ -6914,10 +6914,6 @@ "format": "date-time", "type": "string" }, - "rotated_secrets": { - "description": "OAuth 2.0 Client Rotated Secrets\n\nRotatedSecrets holds previously rotated secrets that are still valid for authentication. This allows for secret rotation without downtime. The secrets are stored in hashed format and remain valid until explicitly cleaned up.", - "type": "string" - }, "userinfo_signed_response_alg": { "description": "OpenID Connect Request Userinfo Signed Response Algorithm\n\nJWS alg algorithm [JWA] REQUIRED for signing UserInfo Responses. If this is specified, the response will be JWT\n[JWT] serialized, and signed using JWS. The default, if omitted, is for the UserInfo Response to return the Claims\nas a UTF-8 encoded JSON object using the application/json content-type.", "type": "string" @@ -11952,88 +11948,6 @@ "tags": ["oAuth2"] } }, - "/admin/clients/{id}/secret/rotate": { - "post": { - "description": "Rotate the client secret of an OAuth 2.0 Client. The old secret will be moved to the rotated_secrets list and will remain valid until explicitly cleaned up. This allows for zero-downtime credential rotation.\n\nThe new secret will be returned in the response. Make sure to store it securely as it will not be retrievable later.", - "operationId": "oauth2RotateClientSecret", - "parameters": [ - { - "description": "OAuth 2.0 Client ID", - "in": "path", - "name": "id", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/oAuth2Client" - } - } - }, - "description": "oAuth2Client" - }, - "404": { - "$ref": "#/components/responses/errorOAuth2NotFound" - }, - "default": { - "$ref": "#/components/responses/errorOAuth2Default" - } - }, - "security": [ - { - "oryAccessToken": [] - } - ], - "summary": "Rotate OAuth2 Client Secret", - "tags": ["oAuth2"] - }, - "delete": { - "description": "Delete all rotated secrets from an OAuth 2.0 Client. This will revoke access for all previously rotated secrets, leaving only the current secret valid.\n\nUse this endpoint after you have updated all services to use the new secret following a rotation.", - "operationId": "oauth2DeleteRotatedClientSecrets", - "parameters": [ - { - "description": "OAuth 2.0 Client ID", - "in": "path", - "name": "id", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/oAuth2Client" - } - } - }, - "description": "oAuth2Client" - }, - "404": { - "$ref": "#/components/responses/errorOAuth2NotFound" - }, - "default": { - "$ref": "#/components/responses/errorOAuth2Default" - } - }, - "security": [ - { - "oryAccessToken": [] - } - ], - "summary": "Delete OAuth2 Client Rotated Secrets", - "tags": ["oAuth2"] - } - }, "/admin/courier/messages": { "get": { "description": "Lists all messages by given status and recipient.", From 8c18152ac45f8409a0b8e368ba444cd7aba9bcb5 Mon Sep 17 00:00:00 2001 From: vinckr Date: Wed, 11 Feb 2026 19:26:39 -0300 Subject: [PATCH 5/8] chore: format --- docs/hydra/guides/oauth2-clients.mdx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/hydra/guides/oauth2-clients.mdx b/docs/hydra/guides/oauth2-clients.mdx index 4c3493eb18..05c14adbfb 100644 --- a/docs/hydra/guides/oauth2-clients.mdx +++ b/docs/hydra/guides/oauth2-clients.mdx @@ -139,10 +139,10 @@ interruption. ### How secret rotation works -1. **Rotate the secret**: Generate a new secret for the client -2. **Both secrets work**: Old and new secrets both authenticate until cleanup -3. **Update services**: Update your applications to use the new secret -4. **Cleanup**: Manually remove old rotated secrets once all services are updated +1. Rotate the secret: Generate a new secret for the client +2. Both secrets work: Old and new secrets both authenticate until cleanup +3. Update services: Update your applications to use the new secret +4. Cleanup: Manually remove old rotated secrets once all services are updated ### Rotate client secret @@ -157,7 +157,7 @@ curl -X POST https://{project.slug}.projects.oryapis.com/admin/clients/{client-i -H "Authorization: Bearer ory_pat_..." ``` -The response includes the new `client_secret`. **Save this value immediately** - it will not be shown again. +The response includes the new `client_secret`. Save this value immediately - it will not be shown again. See [API documentation](../../reference/api#tag/oAuth2/operation/rotateOAuth2ClientSecret). From 29299a099ad3bb748759c80352109cac9c9407c6 Mon Sep 17 00:00:00 2001 From: vinckr Date: Wed, 11 Feb 2026 19:30:16 -0300 Subject: [PATCH 6/8] chore: format --- docs/hydra/guides/oauth2-clients.mdx | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/docs/hydra/guides/oauth2-clients.mdx b/docs/hydra/guides/oauth2-clients.mdx index 05c14adbfb..a0fa6aefd1 100644 --- a/docs/hydra/guides/oauth2-clients.mdx +++ b/docs/hydra/guides/oauth2-clients.mdx @@ -137,14 +137,14 @@ OAuth2 client secret rotation allows you to change a client's secret without dow remains valid until you explicitly clean it up, allowing you to update all services using the client credentials without service interruption. -### How secret rotation works +##### How secret rotation works 1. Rotate the secret: Generate a new secret for the client 2. Both secrets work: Old and new secrets both authenticate until cleanup 3. Update services: Update your applications to use the new secret 4. Cleanup: Manually remove old rotated secrets once all services are updated -### Rotate client secret +##### Rotate client secret To rotate an OAuth2 client secret, use the following methods: @@ -186,7 +186,7 @@ console.log("New secret:", client.client_secret) ```` -### Clear rotated secrets +##### Clear rotated secrets Once all services have been updated to use the new secret, you can remove the old rotated secrets to revoke access using the old credentials: @@ -219,7 +219,7 @@ await ory.deleteRotatedOAuth2ClientSecrets({ ```` -### Secret rotation workflow example +##### Secret rotation workflow example Here's a complete workflow for rotating a client secret: @@ -248,12 +248,19 @@ curl -X DELETE "https://{project.slug}.projects.oryapis.com/admin/clients/$CLIEN # Old secret is now revoked ``` -:::tip Zero-downtime credential rotation Secret rotation enables zero-downtime credential updates. Both the old and new secrets -remain valid until you manually clean up the rotated secrets, allowing you to update all your services without service -interruption. ::: +:::tip Zero-downtime credential rotation -:::warning Security best practice Rotated secrets remain valid indefinitely until you explicitly clean them up. Always remove old -rotated secrets once your migration is complete to ensure that compromised credentials cannot be used. ::: +Secret rotation enables zero-downtime credential updates. Both the old and new secrets remain valid until you manually clean up +the rotated secrets, allowing you to update all your services without service interruption. + +::: + +:::warning Security best practice + +Rotated secrets remain valid indefinitely until you explicitly clean them up. Always remove old rotated secrets once your +migration is complete to ensure that compromised credentials cannot be used. + +::: ## Delete OAuth2 client From 056df208e3e7bdcc00e7b226f141b136477e6984 Mon Sep 17 00:00:00 2001 From: Arne Luenser Date: Thu, 9 Jul 2026 15:49:32 +0200 Subject: [PATCH 7/8] docs: address review feedback on client secret rotation Apply the terminology suggestions from the review: singular "secret", and "client service" used consistently. Also fix the rotation endpoint path. All four curl examples used /admin/clients/{id}/secret/rotate, but the implemented route is /admin/clients/{id}/secrets/rotate, so every example returned 404. Answer the two open review questions in the text: say where to store the new secret, and confirm that secrets do not expire on their own. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/hydra/guides/oauth2-clients.mdx | 45 ++++++++++++++-------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/docs/hydra/guides/oauth2-clients.mdx b/docs/hydra/guides/oauth2-clients.mdx index a0fa6aefd1..9cba2d6c1c 100644 --- a/docs/hydra/guides/oauth2-clients.mdx +++ b/docs/hydra/guides/oauth2-clients.mdx @@ -134,15 +134,14 @@ See [API documentation](../../reference/api#tag/oAuth2/operation/patchOAuth2Clie ## Rotate OAuth2 client secret OAuth2 client secret rotation allows you to change a client's secret without downtime. When you rotate a secret, the old secret -remains valid until you explicitly clean it up, allowing you to update all services using the client credentials without service -interruption. +remains valid until you remove it, allowing you to update all your client services without service interruption. ##### How secret rotation works -1. Rotate the secret: Generate a new secret for the client -2. Both secrets work: Old and new secrets both authenticate until cleanup -3. Update services: Update your applications to use the new secret -4. Cleanup: Manually remove old rotated secrets once all services are updated +1. Generate a new secret for the client service. Both the old and the new secret authenticate from this point on. +2. Update your client services to use the new secret. +3. Test that the client services can authenticate with the new secret. +4. Manually remove the old secret. Only the new secret authenticates from this point on. ##### Rotate client secret @@ -153,11 +152,12 @@ To rotate an OAuth2 client secret, use the following methods: ```bash -curl -X POST https://{project.slug}.projects.oryapis.com/admin/clients/{client-id}/secret/rotate \ +curl -X POST https://{project.slug}.projects.oryapis.com/admin/clients/{client-id}/secrets/rotate \ -H "Authorization: Bearer ory_pat_..." ``` -The response includes the new `client_secret`. Save this value immediately - it will not be shown again. +The response includes the new `client_secret`. Ory shows this value only once. Store it immediately in the secret store that your +client services read from, such as a secrets manager or an encrypted environment configuration. See [API documentation](../../reference/api#tag/oAuth2/operation/rotateOAuth2ClientSecret). @@ -186,21 +186,20 @@ console.log("New secret:", client.client_secret) ```` -##### Clear rotated secrets +##### Remove old secret -Once all services have been updated to use the new secret, you can remove the old rotated secrets to revoke access using the old -credentials: +Once all services are updated to use the new secret, remove the old secret to revoke access using the old secret: ````mdx-code-block ```bash -curl -X DELETE https://{project.slug}.projects.oryapis.com/admin/clients/{client-id}/secret/rotate \ +curl -X DELETE https://{project.slug}.projects.oryapis.com/admin/clients/{client-id}/secrets/rotate \ -H "Authorization: Bearer ory_pat_..." ``` -After cleanup, only the current secret will be valid. Old secrets will no longer authenticate. +After removing the old secret, only the current (new) secret is valid. The old secret can no longer authenticate. See [API documentation](../../reference/api#tag/oAuth2/operation/deleteRotatedOAuth2ClientSecrets). @@ -212,7 +211,7 @@ await ory.deleteRotatedOAuth2ClientSecrets({ id: clientId }) -// Old secrets are now revoked +// Old secret is now revoked. ``` @@ -228,21 +227,21 @@ Here's a complete workflow for rotating a client secret: CLIENT_ID="your-client-id" # 2. Rotate the secret -NEW_SECRET=$(curl -X POST "https://{project.slug}.projects.oryapis.com/admin/clients/$CLIENT_ID/secret/rotate" \ +NEW_SECRET=$(curl -X POST "https://{project.slug}.projects.oryapis.com/admin/clients/$CLIENT_ID/secrets/rotate" \ -H "Authorization: Bearer ory_pat_..." | jq -r '.client_secret') echo "New secret: $NEW_SECRET" -# 3. Update your applications with the new secret -# (Both old and new secrets work during this period) +# 3. Update your client services with the new secret +# (Both the old and the new secret work during this period) # 4. Verify the new secret works curl -X POST "https://{project.slug}.projects.oryapis.com/oauth2/token" \ -u "$CLIENT_ID:$NEW_SECRET" \ -d "grant_type=client_credentials" -# 5. Once all services are updated, clean up old secrets -curl -X DELETE "https://{project.slug}.projects.oryapis.com/admin/clients/$CLIENT_ID/secret/rotate" \ +# 5. Once all client services are updated, remove the old secret +curl -X DELETE "https://{project.slug}.projects.oryapis.com/admin/clients/$CLIENT_ID/secrets/rotate" \ -H "Authorization: Bearer ory_pat_..." # Old secret is now revoked @@ -250,15 +249,15 @@ curl -X DELETE "https://{project.slug}.projects.oryapis.com/admin/clients/$CLIEN :::tip Zero-downtime credential rotation -Secret rotation enables zero-downtime credential updates. Both the old and new secrets remain valid until you manually clean up -the rotated secrets, allowing you to update all your services without service interruption. +Secret rotation enables zero-downtime credential updates. Both the old and new secrets remain valid until you manually remove the +old secret, allowing you to update all your client services without service interruption. ::: :::warning Security best practice -Rotated secrets remain valid indefinitely until you explicitly clean them up. Always remove old rotated secrets once your -migration is complete to ensure that compromised credentials cannot be used. +Secrets remain valid indefinitely until you explicitly remove them. Always remove old secrets once your secret rotation process is +complete to ensure that compromised credentials cannot be used. ::: From 0c9953a0227521d38609c8d9c1a5d3289581ba08 Mon Sep 17 00:00:00 2001 From: Arne Luenser Date: Tue, 14 Jul 2026 11:06:36 +0200 Subject: [PATCH 8/8] docs: address review feedback and align secret rotation docs with implementation - Pluralize 'secrets' and use 'can be used to authenticate' wording per review - Rename subheading to 'Rotate OAuth2 client secret' - Document auth method restriction (client_secret_basic/client_secret_post), the five-secret retention limit, and that set/patch clears rotated secrets Co-Authored-By: Claude Fable 5 --- docs/hydra/guides/oauth2-clients.mdx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/hydra/guides/oauth2-clients.mdx b/docs/hydra/guides/oauth2-clients.mdx index 82705796d2..15f906043a 100644 --- a/docs/hydra/guides/oauth2-clients.mdx +++ b/docs/hydra/guides/oauth2-clients.mdx @@ -187,12 +187,20 @@ remains valid until you remove it, allowing you to update all your client servic ##### How secret rotation works -1. Generate a new secret for the client service. Both the old and the new secret authenticate from this point on. +1. Generate a new secret for the client service. Both the old and the new secrets can be used to authenticate from this point on. 2. Update your client services to use the new secret. 3. Test that the client services can authenticate with the new secret. 4. Manually remove the old secret. Only the new secret authenticates from this point on. -##### Rotate client secret +:::info + +Secret rotation is available only for clients that use the `client_secret_basic` or `client_secret_post` token endpoint +authentication method. Ory keeps up to five rotated secrets per client. When you rotate again, the oldest rotated secret is +dropped and can no longer authenticate. Setting a new secret through the update or patch client APIs clears all rotated secrets. + +::: + +##### Rotate OAuth2 client secret To rotate an OAuth2 client secret, use the following methods: @@ -282,7 +290,7 @@ NEW_SECRET=$(curl -X POST "https://{project.slug}.projects.oryapis.com/admin/cli echo "New secret: $NEW_SECRET" # 3. Update your client services with the new secret -# (Both the old and the new secret work during this period) +# (Both the old and the new secrets work during this period) # 4. Verify the new secret works curl -X POST "https://{project.slug}.projects.oryapis.com/oauth2/token" \