Skip to content

Managing OAuth2 Clients

oauth2 client confidential public cimd graphql client secret scopes

Each OAuth2 client represents a third-party application that can authenticate users through your Nhost project. How you manage clients depends on your use case.

TypeHas SecretPKCE RequiredDescription
ConfidentialYes (bcrypt hash)NoServer-side apps that can securely store a secret
PublicNoYesBrowser or mobile apps that cannot keep a secret
CIMDNoYesURL-based client IDs resolved via metadata document (RFC 9728)

A client is confidential when it has a clientSecretHash and public when this field is null. The client type is determined entirely by whether a secret hash is present.

If you want to integrate third-party services with your project — like connecting Outline as a wiki, Grafana for dashboards, or an MCP tool — you manage the OAuth2 clients yourself as an admin. This is the most common use case.

The Nhost Dashboard provides a built-in UI for creating and managing clients. Go to your project’s dashboard, then navigate to the OAuth2 Provider section.

Create OAuth2 client from the dashboard

From here you can create clients, set redirect URIs, choose scopes, and manage secrets.

For most projects, the dashboard is all you need. The rest of this page covers the GraphQL API approach, which is useful when you want to automate client management or let your users create their own clients.

If you want your users to create their own integrations — for example, letting them add a “Sign in with your app” button to their own services — you’ll need to:

  1. Expose client management through the GraphQL API
  2. Configure permissions so users can manage oauth2 clients
  3. Build a management UI in your application
FieldTypeRequiredDescription
clientSecretHashStringNoBcrypt hash of the client secret. If set, the client is confidential. If null, the client is public. A database trigger validates the value is a valid bcrypt hash.
redirectUrisString[]YesList of allowed redirect URIs. The authorization request’s redirect_uri must exactly match one of these.
scopesString[]NoScopes the client is allowed to request. Defaults to all scopes: openid, profile, email, phone, offline_access, graphql.
metadataJSONNoArbitrary JSON metadata (e.g. { "description": "My app" }). Not used by the auth server — for your own reference.
createdByUUIDNoUser ID of the creator. Sets a relationship to the users table.

The following fields are auto-generated and returned in responses:

FieldDescription
clientIdAuto-generated identifier with nhoa_ prefix (e.g. nhoa_a1b2c3d4e5f67890)
createdAtTimestamp of creation
updatedAtTimestamp of last update (auto-maintained by trigger)

Use a GraphQL mutation to create a client with a bcrypt-hashed secret. The clientId (prefixed with nhoa_) is auto-generated.

mutation CreateConfidentialClient($object: authOauth2Clients_insert_input!) {
insertAuthOauth2Client(object: $object) {
clientId
redirectUris
scopes
createdAt
updatedAt
metadata
}
}

Variables:

{
"object": {
"clientSecretHash": "$2b$10$...",
"redirectUris": ["https://myapp.example.com/callback"],
"scopes": ["openid", "profile", "email"],
"metadata": { "description": "My server-side application" }
}
}

The secret must be hashed with bcrypt before passing it. A database trigger validates that the value is a valid bcrypt hash.

In JavaScript (using bcryptjs):

import bcrypt from 'bcryptjs';
// Generate a random secret
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const array = new Uint8Array(48);
crypto.getRandomValues(array);
const plainSecret = Array.from(array, (b) => chars[b % chars.length]).join('');
// Hash it with bcrypt before sending to the API
const clientSecretHash = await bcrypt.hash(plainSecret, 10);

In Node.js / Bun (using native APIs):

import { randomBytes } from 'crypto';
const plainSecret = randomBytes(32).toString('hex');
const clientSecretHash = await Bun.password.hash(plainSecret, {
algorithm: 'bcrypt',
cost: 10,
});

Create a client without a clientSecretHash. Public clients must use PKCE for the authorization flow.

mutation CreatePublicClient($object: authOauth2Clients_insert_input!) {
insertAuthOauth2Client(object: $object) {
clientId
redirectUris
scopes
}
}

Variables:

{
"object": {
"redirectUris": ["http://localhost:3000/callback"],
"scopes": ["openid", "profile", "email"]
}
}

The response will have clientSecretHash: null, confirming it is a public client.

query ListOAuth2Clients {
authOauth2Clients(order_by: { createdAt: desc }) {
clientId
redirectUris
scopes
metadata
createdAt
updatedAt
}
}

Update redirect URIs, scopes, description, or rotate the secret:

mutation UpdateClient($clientId: String!, $changes: authOauth2Clients_set_input!) {
updateAuthOauth2Clients(
where: { clientId: { _eq: $clientId } }
_set: $changes
) {
returning {
clientId
redirectUris
scopes
updatedAt
}
}
}

Variables (updating redirect URIs):

{
"clientId": "nhoa_a1b2c3d4e5f67890",
"changes": {
"redirectUris": ["https://myapp.example.com/callback", "https://myapp.example.com/callback2"]
}
}

To rotate a confidential client’s secret, hash the new secret and update clientSecretHash:

{
"clientId": "nhoa_a1b2c3d4e5f67890",
"changes": {
"clientSecretHash": "$2b$10$<new_hash>"
}
}

The old secret stops working immediately.

Converting Between Public and Confidential

Section titled “Converting Between Public and Confidential”
  • Public to confidential: Set clientSecretHash to a bcrypt hash
  • Confidential to public: Set clientSecretHash to null
mutation DeleteClient($clientId: String!) {
deleteAuthOauth2Clients(where: { clientId: { _eq: $clientId } }) {
affected_rows
}
}

Deleting a client also cascades to all associated authorization requests, codes, and refresh tokens.

By default, only the admin role has access to the oauth2_clients table. If you want users to manage their own clients, you need to configure permissions on the table.

Here is a reference configuration that scopes each user to the clients they created. You are free to adjust these permissions to match your needs.

Users can create clients. A column preset automatically sets created_by to the authenticated user’s ID.

Insert permissions

Users can only read their own clients (where created_by matches their user ID).

Select permissions

Users can update their own clients (where created_by matches their user ID).

Update permissions

Users can delete their own clients (where created_by matches their user ID).

Delete permissions

For a full working example of creating and managing OAuth2 clients from a React application (including secret generation with bcrypt, scope selection, and secret rotation), see OAuth2Providers.tsx in the react-demo example.

ScopeDescription
openidRequired for OIDC. Triggers ID token issuance.
profileAdds name, picture, locale to ID token and UserInfo
emailAdds email, email_verified to ID token and UserInfo
phoneAdds phone_number, phone_number_verified to ID token and UserInfo
offline_accessRecognized in discovery metadata. Refresh tokens are always issued.
graphqlAdds GraphQL-compatible claims (https://hasura.io/jwt/claims) to access tokens

When creating a client, the scopes array defines which scopes the client is allowed to request. If no scopes are specified, the default is all six scopes.