Manage OAuth 2.0 and OpenID Connect clients
OAuth2 clients are applications that securely authenticate with an authorization server to obtain access to an HTTP service. Every OAuth2 client is either confidential or public, depending on whether it can keep a secret. Applications that run on infrastructure you control, for example, backends and services, are confidential clients. They authenticate with a client secret. Applications distributed to users, for example, single-page apps, mobile, and native apps, are public clients. Anyone can read the code and extract an embedded secret, so they authenticate with PKCE and have no secret at all.
For each of your confidential clients, you register a client record with the client's secret for authentication. Public clients do
not have a client secret in the registered client record. In Ory, you set whether the client is confidential or public with
token_endpoint_auth_method value, where none means public.
This documentation article explains how to manage OAuth2 clients using the Ory Console, Ory SDK, Ory CLI, and Ory REST APIs.
Create OAuth2 client
To create a new OAuth2 client, use the following methods:
Clients that use the client_secret_basic or client_secret_post token-endpoint-auth-method are able to perform secret
rotation.
- Ory Console
- Ory CLI
- Ory SDK
- REST API
The Ory Console is a web-based user interface that allows you to manage OAuth2 clients. To create a new client:
- Go to OAuth 2 → Clients and applications in the Ory Console
- Click Create OAuth2 Client and complete the form.
- When creating a confidential client, copy the client secret when printed. It is only shown once.
ory create oauth2-client \
--grant-type authorization_code --grant-type refresh_token --grant-type client_credentials \
--response-type code \
--scope openid --scope offline_access \
--token-endpoint-auth-method client_secret_post \
--redirect-uri https://my-app.com/callback --redirect-uri http://my-other-app.com/callback
// Copyright © 2026 Ory Corp
import { Configuration, OAuth2Api } from "@ory/client"
const ory = new OAuth2Api(
new Configuration({
basePath: `https://${process.env.ORY_PROJECT_SLUG}.projects.oryapis.com`,
accessToken: process.env.ORY_API_KEY,
}),
)
export async function createOAuth2Client() {
await ory.createOAuth2Client({
oAuth2Client: {
grant_types: ["authorization_code", "refresh_token"],
redirect_uris: ["https://example.com"],
scope: "offline openid",
token_endpoint_auth_method: "client_secret_post",
},
})
}
See API documentation.
Redirect URI requirements
Redirect URIs must match exactly. Wildcard patterns such as https://*.example.com/callback are not supported in any environment,
including development mode.
The OAuth 2.0 specification (RFC 6749, Section 3.1.2.2) requires exact URI matching. On shared platforms such as Heroku, Vercel, and Netlify, wildcard subdomains would allow any tenant on that platform to intercept authorization codes or tokens intended for your application.
Dynamic preview and review environments
If your CI/CD pipeline creates ephemeral environments with unpredictable URLs, use one of these approaches.
Programmatic client updates (recommended)
Update the client's redirect_uris via the API when a review app is deployed, and remove the URI when the environment is torn
down. This approach works for any number of preview environments and requires no manual intervention.
Add the preview URL when the review app is deployed:
// Copyright © 2026 Ory Corp
import { Configuration, OAuth2Api } from "@ory/client"
const ory = new OAuth2Api(
new Configuration({
basePath: `https://${process.env.ORY_PROJECT_SLUG}.projects.oryapis.com`,
accessToken: process.env.ORY_API_KEY,
}),
)
export async function addRedirectUri(clientId: string, newUri: string) {
const { data: client } = await ory.getOAuth2Client({ id: clientId })
await ory.setOAuth2Client({
id: clientId,
oAuth2Client: {
...client,
redirect_uris: [...(client.redirect_uris ?? []), newUri],
},
})
}
Remove the URI when the review app is destroyed:
// Copyright © 2026 Ory Corp
import { Configuration, OAuth2Api } from "@ory/client"
const ory = new OAuth2Api(
new Configuration({
basePath: `https://${process.env.ORY_PROJECT_SLUG}.projects.oryapis.com`,
accessToken: process.env.ORY_API_KEY,
}),
)
export async function removeRedirectUri(clientId: string, uriToRemove: string) {
const { data: client } = await ory.getOAuth2Client({ id: clientId })
await ory.setOAuth2Client({
id: clientId,
oAuth2Client: {
...client,
redirect_uris: (client.redirect_uris ?? []).filter(
(u) => u !== uriToRemove,
),
},
})
}
Explicit URI listing
If the set of preview URLs is small and stable, register each URI explicitly when creating or updating the client:
ory update oauth2-client {client.id} \
--redirect-uri https://preview-1.example.com/callback \
--redirect-uri https://preview-2.example.com/callback
Custom domain
Point a stable domain you control at the review environment (for example, preview.yourapp.com). The redirect URI stays constant
across deployments and requires no client updates.
Update OAuth2 client
To update an existing OAuth2 client, use the following methods:
If you use secret rotation, setting a new secret through the update client API clears all rotated secrets.
- Ory Console
- Ory CLI
- Ory SDK
- REST API
- Go to OAuth 2 → Clients and applications in the Ory Console.
- Locate the client you want to update.
- Click on the pen symbol to update the client's configuration.
- When you are finished, scroll to the top and click Save.
ory update oauth2-client --project <project-id> --workspace <workspace-id> {client.id} \
--grant-type authorization_code --grant-type refresh_token --grant-type client_credentials \
--response-type code \
--scope openid --scope offline_access \
--token-endpoint-auth-method client_secret_post \
--redirect-uri https://a-new-callback
// Copyright © 2026 Ory Corp
import { Configuration, OAuth2Api, OAuth2Client } from "@ory/client"
const ory = new OAuth2Api(
new Configuration({
basePath: `https://${process.env.ORY_PROJECT_SLUG}.projects.oryapis.com`,
accessToken: process.env.ORY_API_KEY,
}),
)
export async function updateOAuth2Client(
id: string,
update: Partial<OAuth2Client>,
) {
// setOAuth2Client replaces all values (empty ones too),
// which is why we include the original client.
const { data: original } = await ory.getOAuth2Client({ id })
await ory.setOAuth2Client({
id,
oAuth2Client: {
...original,
...update,
},
})
}
See API documentation.
Patch OAuth2 client
To partially update an existing OAuth2 client, use the following methods:
If you use secret rotation, setting a new secret through the patch client API clears all rotated secrets.
- Ory Console
- Ory SDK
- REST API
- Go to OAuth 2 → Clients and applications in the Ory Console.
- Locate the client you want to update.
- Click on the pen symbol to update the client's configuration.
- When you are finished, scroll to the top and click Save.
// Copyright © 2026 Ory Corp
import { Configuration, JsonPatch, OAuth2Api } from "@ory/client"
const ory = new OAuth2Api(
new Configuration({
basePath: `https://${process.env.ORY_PROJECT_SLUG}.projects.oryapis.com`,
accessToken: process.env.ORY_API_KEY,
}),
)
export async function patchOAuth2Client(id: string, patches: JsonPatch[]) {
await ory.patchOAuth2Client({
id,
jsonPatch: [
...patches,
{
op: "replace",
path: "owner",
value: "New owner",
},
],
})
}
See API documentation.
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 remove it. This allows you to update the secret wherever your applications store it, then restart or redeploy anything that reads it, without service interruption.
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.
If you use secret rotation, setting a new secret through the update or patch client APIs clears all rotated secrets.
How secret rotation works
- Rotation generates a new secret for the client. Both the old and the new secrets can be used to authenticate from this point on.
- Update the secret wherever it's stored, then restart or redeploy whatever applications read it.
- Verify that the application can authenticate with the new secret.
- Delete the old secret. Only the new secret authenticates from this point on.
Rotate OAuth2 client secret
To rotate an OAuth2 client secret, use the following methods:
- REST API
- Ory SDK
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. Ory returns this value once and stores only its hash, so it can't be
retrieved later. Save it now, wherever you keep the current secret.
See API documentation.
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.rotateOAuth2ClientSecret({
id: clientId
})
// Save the new client_secret immediately
console.log("New secret:", client.client_secret)
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.
Remove old secret
Once your application is authenticating with the new secret everywhere, delete the old secret to revoke access:
- REST API
- Ory SDK
curl -X DELETE https://{project.slug}.projects.oryapis.com/admin/clients/{client-id}/secrets/rotate \
-H "Authorization: Bearer ory_pat_..."
After removing the old secret, only the current (new) secret is valid. The old secret can no longer authenticate.
See API documentation.
await ory.deleteRotatedOAuth2ClientSecrets({
id: clientId
})
// Old secret is now revoked.
Secret rotation workflow example
Here's a complete workflow for rotating a client secret:
# 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/secrets/rotate" \
-H "Authorization: Bearer ory_pat_..." | jq -r '.client_secret')
echo "New secret: $NEW_SECRET"
# 3. Update your client's services with the new secret
# (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" \
-u "$CLIENT_ID:$NEW_SECRET" \
-d "grant_type=client_credentials"
# 5. Once all your client's 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
Delete OAuth2 client
To delete an existing OAuth2 client, use the following methods:
- Ory Console
- Ory CLI
- Ory SDK
- REST API
- Go to OAuth 2 → Clients and applications in the Ory Console.
- Locate the client you want to update.
- Click on pen symbol to update the client's configuration.
- Scroll to the bottom and click Delete Client.
ory delete oauth2-client {client.id}
// Copyright © 2026 Ory Corp
import { Configuration, OAuth2Api } from "@ory/client"
const ory = new OAuth2Api(
new Configuration({
basePath: `https://${process.env.ORY_PROJECT_SLUG}.projects.oryapis.com`,
accessToken: process.env.ORY_API_KEY,
}),
)
export async function deleteOAuth2Client(id: string) {
await ory.deleteOAuth2Client({ id })
}
See API documentation.
OpenID Dynamic Client Registration
OpenID Dynamic Client Registration enables automatic registration of OAuth2 clients with the authorization server. When enabled, clients can be created, retrieved, updated, patched, and deleted dynamically without manual configuration. To enable OpenID Dynamic Client Registration, use the Ory CLI:
ory patch oauth2-config --project <project-id> --workspace <workspace-id>
--replace "/oidc/dynamic_client_registration/enabled=true"
OpenID Connect dynamic registration involves the use of a registration_access_token, which is a bearer token that allows a
client to make requests to the OpenID Connect dynamic registration endpoint. The token is issued by the authorization server and
can only be used by the client that it was issued to.
It's important to note that the registration_access_token is a sensitive piece of information that should be kept secure. It
should only be used by the client that it was issued to and should not be shared with any other parties.
Register OAuth2 and OpenID Connect clients
Use the SDK or REST API to register an OAuth2 and OpenID Connect client:
- Ory SDK
- REST API
// Copyright © 2026 Ory Corp
import { Configuration, OidcApi } from "@ory/client"
const ory = new OidcApi(
new Configuration({
basePath: `https://${process.env.ORY_PROJECT_SLUG}.projects.oryapis.com`,
accessToken: process.env.ORY_API_KEY,
}),
)
export async function createOidcDynamicClient() {
const { data } = await ory.createOidcDynamicClient({
oAuth2Client: {
grant_types: ["authorization_code", "refresh_token"],
redirect_uris: ["https://example.com"],
scope: "offline openid",
token_endpoint_auth_method: "client_secret_post",
},
})
console.log(data.registration_access_token) // Write this down, it is only sent once!
console.log(data.client_id, data.client_secret /* ... */)
}
See API documentation.
The response includes the registration_access_token which is needed to manage the client. The token will only be shown once!
Get OAuth2 and OpenID Connect clients
The GET endpoint requires the client to authenticate with the registration_access_token regardless of the
token_endpoint_auth_method. It can be used to retrieve the OAuth2 and OpenID Connect client.
- Ory SDK
- REST API
// Copyright © 2026 Ory Corp
import { Configuration, OidcApi } from "@ory/client"
const ory = new OidcApi(
new Configuration({
basePath: `https://${process.env.ORY_PROJECT_SLUG}.projects.oryapis.com`,
accessToken: process.env.ORY_API_KEY,
}),
)
export async function getOidcDynamicClient(
id: string,
registrationAccessToken: string,
) {
const { data } = await ory.getOidcDynamicClient(
{
id,
},
{
headers: {
Authorization: `Bearer ${registrationAccessToken}`,
},
},
)
}
See API documentation.
Update OAuth2 and OpenID Connect clients
The POST endpoint requires the client to authenticate with the registration_access_token regardless of the
token_endpoint_auth_method. It can be used to update the OAuth2 and OpenID Connect client.
- Ory SDK
- REST API
// Copyright © 2026 Ory Corp
import { Configuration, OAuth2Client, OidcApi } from "@ory/client"
const ory = new OidcApi(
new Configuration({
basePath: `https://${process.env.ORY_PROJECT_SLUG}.projects.oryapis.com`,
accessToken: process.env.ORY_API_KEY,
}),
)
export async function createOidcDynamicClient(
id: string,
updatedClient: OAuth2Client,
) {
const { data } = await ory.setOidcDynamicClient({
id: id,
oAuth2Client: {
...updatedClient,
grant_types: ["authorization_code", "refresh_token"],
// ...
},
})
console.log(data.registration_access_token) // Write this down, it is only sent once!
}
See API documentation.
Delete OAuth2 and OpenID Connect clients
The DELETE endpoint requires the client to authenticate with the registration_access_token regardless of the
token_endpoint_auth_method. It can be used to delete the OAuth2 and OpenID Connect client.
- Ory SDK
- REST API
// Copyright © 2026 Ory Corp
import { Configuration, OidcApi } from "@ory/client"
const ory = new OidcApi(
new Configuration({
basePath: `https://${process.env.ORY_PROJECT_SLUG}.projects.oryapis.com`,
accessToken: process.env.ORY_API_KEY,
}),
)
export async function deleteOidcDynamicClient(
id: string,
registrationAccessToken: string,
) {
await ory.deleteOidcDynamicClient(
{
id,
},
{
headers: {
Authorization: `Bearer ${registrationAccessToken}`,
},
},
)
}
See` API documentation.
Example OAuth2 clients
Here are some examples of creating OAuth2 clients with different options:
Client credentials
ory create oauth2-client \
--grant-type client_credentials \
--scope my-scope \
--token-endpoint-auth-method client_secret_basic
Token endpoint auth method
ory create oauth2-client \
--grant-type authorization_code \
--response-type code \
--scope openid \
--token-endpoint-auth-method client_secret_post \
--redirect-uri https://my-app.com/callback
Multiple redirect URIs
ory create oauth2-client \
--grant-type authorization_code --grant-type refresh_token \
--response-type code \
--scope openid --scope offline_access \
--token-endpoint-auth-method client_secret_post \
--redirect-uri https://my-app.com/callback --redirect-uri http://my-other-app.com/callback