Cloud|

OAuth clients and flows

Configure OAuth clients and choose authorization code or client credentials.

8 min read Updated 2026-09-07 #identity#oauth#oidc

Use authorization code when an integration acts for a person. Use client credentials when a service acts on one application resource.

Both flows use the platform identity model. Applications receive actor and accessSubject; they do not verify OAuth tokens themselves.

Cloud has three client origins:

  • managed: created and configured by an administrator;
  • first_party: seeded by Cloud and protected from editing or deletion;
  • dynamic: untrusted public clients registered automatically through RFC 7591 and authorized through explicit user consent.

The first-party cld command uses Cloud's protected cloud-cli registration for login, refresh, and logout. It does not dynamically register or accept an alternate OAuth client ID. Dynamic registration is for clients without a prior relationship with the Cloud instance.

Authorization-code flow

The flow uses:

text
GET  /oauth/authorize
POST /oauth/token

The authorization request accepts:

Parameter Required Meaning
client_id Yes OAuth client ID
redirect_uri Yes Exact registered redirect URI
response_type Yes Must be code
scope No Space-separated allowed scopes
resource No Exact allowed RFC 8707 resource audience
state No Client state returned unchanged
nonce No Included in OpenID Connect processing
code_challenge Public clients PKCE challenge
code_challenge_method With challenge Must be S256

Every client that uses PKCE must use S256. Public clients must use PKCE; confidential clients may omit it because they also authenticate at the token endpoint. Cloud does not accept PKCE plain.

Dynamic clients additionally require an explicit resource on the same Cloud origin. Before issuing a code, Cloud shows the resource owner the client name, callback host, exact resource, and requested scopes. Approval and denial are single-use and expire after five minutes. Authorization responses include the issuer identifier so clients can reject mix-up attacks.

Exchange the returned code:

http
POST /oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=<code>&
redirect_uri=https%3A%2F%2Fclient.example%2Fcallback&
client_id=<client-id>&
code_verifier=<verifier>

When the authorization request used resource, the token request must repeat the exact same value. Cloud binds the authorization code and any resulting refresh-token family to it. This is required by the Cloud MCP server.

Confidential clients can send credentials through HTTP Basic or the form.

The resulting access token resolves to a user actor. offline_access can produce a refresh token.

Refresh tokens rotate on every successful use. A refresh request can reduce its scopes, but cannot add scopes; the reduced set remains in effect for later rotations. Resource-bound grants must repeat the exact resource on every refresh. Cloud also rechecks the account, client scopes and audiences, profile or explicit client access, and client existence before issuing a replacement. If any check fails, the grant cannot mint another access token.

Authorization codes capture their granted audiences when created. Both the initial access token and its refresh family keep that snapshot; adding a client audience later does not widen an existing grant.

The OpenID Connect UserInfo endpoint accepts only user access tokens that contain openid, were issued for the requesting client, and still refer to an active account and registered client. ID tokens and resource-only access tokens are rejected.

The OpenID Connect sub claim is the immutable Cloud user UUID. Human-readable account names remain available through uid; changing a login name does not change the subject seen by clients.

Client-credentials flow

The OAuth client must:

  • be confidential;
  • reference an active resource-bound service account;
  • allow every requested scope;
  • allow the optional requested resource audience.

Request a token:

http
POST /oauth/token
Authorization: Basic <base64(client_id:client_secret)>
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&
scope=read&
resource=https%3A%2F%2Fcloud.example%2Fapi%2Finventory

Every resource value is an absolute URI without a fragment. When supplied, the resulting access token is valid only for that exact audience.

The token resolves to a resource-bound service-account actor.

The application must still verify:

  • appId;
  • resourceType;
  • resourceId;
  • the service-account access grant;
  • the credential scope cap.

OAuth scopes do not grant domain access. See Resource authorization.

Configure an OAuth client

OAuth client creation supports:

Field Default Meaning
name Required Display name, 1–120 characters
description None Description, up to 1,000 characters
redirectUris [] Allowed authorization-code callbacks
logoutUri None Optional post-logout URI
scopes openid profile email Allowed scopes
audiences cloud Allowed token audiences and resource values
serviceAccountId null Resource service account for client credentials
allowedProfiles user, guest User profiles allowed to authorize
accessMode profiles Profile-based or explicit user/group access
allowedUserIds [] Users allowed in specific mode
allowedGroupIds [] Direct or nested group members allowed in specific mode
isPublic false Public client without a secret

Supported scopes:

text
openid profile email groups offline_access read write admin

Automatic discovery advertises only the delegated dynamic-client subset: openid profile email offline_access read write. The privacy-sensitive groups scope and privileged admin scope remain available only to explicitly configured managed or first-party clients.

Scope, audience, redirect, user, and group lists accept at most 50 entries.

serviceAccountId is valid only for an active resource-bound service account. Clients with a service-account binding must be confidential.

Register a dynamic public client

OAuth clients that do not have a prior relationship with the Cloud instance can discover registration_endpoint in the OpenID configuration and send RFC 7591 metadata to:

text
POST /oauth/register

Cloud accepts authorization-code public clients only. A request must use JSON, contain one to ten exact callback URIs, use token_endpoint_auth_method: none, and request only authorization_code and optional refresh_token grants. Callbacks must use HTTPS or HTTP on localhost, 127.0.0.1, or ::1; user information, fragments, and embedded credentials are rejected. Optional application_type values are native and web.

Dynamic registration does not grant access. The later authorization request must use PKCE S256, an explicit same-origin resource, allowed scopes, and browser consent. Access tokens stay bound to that exact audience. Administrators can identify and revoke dynamic clients from Admin → OAuth; revocation also invalidates existing access and refresh tokens. Abandoned dynamic registrations that never start authorization are cleaned up automatically.

When registration includes scope, Cloud registers exactly that allowed subset. When it omits scope, Cloud uses the advertised delegated default: openid profile email offline_access read write.

Restrict authorization

allowedProfiles rejects account profiles outside the configured list.

With accessMode: "profiles", every allowed profile may authorize. With accessMode: "specific", the user must also be listed directly or belong to an allowed group. Nested group membership is included.

These client restrictions decide who may authorize the OAuth client. They do not replace application resource permissions.

Client creation, updates, secret rotation, and revocation are atomic and recorded in the Cloud audit log. Expired codes and refresh grants are removed by the OAuth app's background cleanup.

The admin API and Admin → OAuth page list clients in bounded pages and can search by name, client ID, or description. API callers use page, per_page (maximum 100), and optional search query parameters.

Validate access tokens

Applications do not import oauthTokens or verify JWTs. Apply auth.requireRole("authenticated"), then read actor and accessSubject from the request context.

Access tokens expire after one hour. Core owns signing-key rotation and publishes public keys for their verification lifetime. Revoking a refresh grant or signing out an OAuth client prevents future refreshes but does not revoke an already issued access token. That token can remain valid until its one-hour expiry. Current-client validation and domain authorization still apply on each request.

The OAuth application still owns the public protocol, clients, consent, authorization codes, refresh families, discovery, and token response. It does not hold a platform signing private key. After it validates the grant, it asks Core's closed OAuth authority to construct and sign the permitted access and ID token shapes. Core reloads the current client and principal before signing. Core and OAuth share CLOUD_OAUTH_BROKER_SECRET, a deployment secret used only for the closed OAuth broker. It is not user authority and cannot submit an arbitrary JWT claim set. No workload credential provisioning is needed. See Runtime configuration for generation, secret ownership, development defaults, and rotation.

Core is the only issuer. OAuth setup checks its broker secret and Core's OAuth signer through the closed readiness endpoint. A failed check prevents startup; failed issuance never falls back to a local signer. No issuance-mode setting remains. Before upgrading, stop all old OAuth replicas: the migration removes oauth.keys, oauth.issuance_state, and the old authorization-code audience trigger. Client and grant data remain in place.

Existing client IDs, secrets, routes, claims, issuer, audiences, one-hour access-token lifetime, authorization codes, and refresh families remain unchanged. Authorization-code, refresh, and client-credentials exchanges use one-shot internal grant reservations; arbitrary claims or client assertions do not cross the authority boundary. A definitive Core grant rejection remains a public invalid_grant response. An unknown transport or server outcome remains server_error and is not retried automatically. Only Core's OAuth-purpose public keys are published. The upgraded Cloud rejects JWTs signed by the old OAuth signer. Existing refresh grants can obtain new tokens; clients without a usable refresh grant need a new authorization. External clients may retain old public keys in their own caches until a refresh or token expiry.

After a Core authority error during refresh, OAuth atomically checks the exact reservation nonce, status, and issuance marker. Only a reservation proven not to have issued tokens is released for a later client retry. If Core already claimed issuance, or the database cannot prove a safe release, the refresh family stays fail-closed. A temporary Core outage therefore does not by itself destroy a provably unused refresh grant.

The audience migration retains and backfills existing code snapshots in one locked transaction. All current writers supply the snapshot directly; old writers are unsupported after the coordinated hard cut. See Runtime configuration.

Continue with Request identity and Resource authorization.

Repository maintainers can run the isolated OAuth upgrade verification to compare the pre-JWT and Core-issued public protocol without using live clients or changing the development stack.

Connect a coding agent

The Cloud skill provides compact working instructions. MCP supplies exact current documentation when details matter.

CLI command
bunx skills add https://docs.example.com

This command installs the working instructions published by this website in the selected coding agent.

For exact, current details, also connect the MCP server through one of the agent tabs.

Installation uses the open-source Vercel Skills CLI.

The skill and MCP complement each other: the skill describes workflows, while MCP supplies current documentation.