Skip to Content

API Clients

API Clients let external services exchange an organization-scoped OIDC access token for a Rhesis JWT using RFC 8693 token exchange. Use them for machine-to-machine integrations that should authenticate through your identity provider instead of a human API token.

API Clients are an Enterprise Edition feature and require SSO to be configured for the organization.

How token exchange works

  1. An organization admin creates an API Client in Rhesis.
  2. Rhesis returns a plaintext client_secret exactly once.
  3. The external service obtains an OIDC access token from the organization’s IdP.
  4. The service calls POST /auth/token-exchange with RFC 8693 form fields.
  5. Rhesis validates the subject token against the organization’s SSO issuer and returns a Rhesis access token.

The exchange audience binds the request to one organization:

audience.txt
rhesis:org:acme

The slug after rhesis:org: must match the organization’s slug.

An exchange can also name the project the resulting token should be scoped to, using the RFC 8693 resource parameter:

resource.txt
urn:rhesis:project:<project-uuid>

The requesting user must be a member of that project. Omitting resource yields a token that can only see organization-level rows (project_id IS NULL); it cannot see anything scoped to a specific project.

Create an API Client

Create clients with the API:

create_api_client.sh
curl -X POST "https://api.example.com/organizations/<org-id>/auth-clients" -H "Authorization: Bearer $RHESIS_API_KEY" -H "Content-Type: application/json" -d '{
    "client_id": "warehouse-sync",
    "name": "Warehouse Sync",
    "expected_subject_azp": "warehouse-sync",
    "expected_subject_audience": "account",
    "allowed_scopes": ["read", "offline_access"],
    "default_scope": "read"
}'

Request fields:

FieldRequiredDescription
client_idYesPublic identifier matching ^[a-z0-9][a-z0-9_-]{2,63}$
nameNoHuman-readable label for the admin UI
expected_subject_azpYesRequired azp claim on the subject token
expected_subject_audienceYesRequired aud claim on the subject token
allowed_scopesYesSupported values: read, full, offline_access
default_scopeYesSingle scope applied when token exchange omits scope

The plaintext client_secret is returned only on create and rotate. Copy it immediately and store it in your secret manager.

Exchange a token

Call POST /auth/token-exchange with application/x-www-form-urlencoded. Client credentials can be sent with HTTP Basic authentication or in the form body, but not both.

token_exchange.sh
SUBJECT_TOKEN="<oidc-access-token-from-your-idp>"

curl -X POST "https://api.example.com/auth/token-exchange" -u "warehouse-sync:$CLIENT_SECRET" -H "Content-Type: application/x-www-form-urlencoded" --data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" --data-urlencode "subject_token=$SUBJECT_TOKEN" --data-urlencode "subject_token_type=urn:ietf:params:oauth:token-type:access_token" --data-urlencode "audience=rhesis:org:acme" --data-urlencode "resource=urn:rhesis:project:11111111-2222-3333-4444-555555555555" --data-urlencode "scope=read offline_access"

The resulting access token behaves like a project-scoped rh-* API key: every request it authenticates is scoped to that one project, the same way X-Project-Id scopes a browser session. There’s no per-request override — a service that needs to work across several projects exchanges once per project. Project membership is re-checked on every request, so removing the exchanged user from the project cuts off access immediately, without waiting for the token to expire.

Successful responses follow the OAuth token response shape:

token_exchange_response.json
{
  "access_token": "<rhesis-jwt>",
  "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
  "token_type": "Bearer",
  "expires_in": 900,
  "scope": "read offline_access",
  "refresh_token": "<refresh-token>",
  "refresh_expires_in": 604800
}

refresh_token is present only when the resolved scope includes offline_access. Both lifetimes are deployment-configurable (JWT_ACCESS_TOKEN_EXPIRE_MINUTES and JWT_REFRESH_TOKEN_EXPIRE_DAYS); the values above are the defaults.

Discover available projects

resource needs a project UUID, and project UUIDs are not visible anywhere in the IdP or in the client configuration. To find one, exchange once with no resource and use the resulting token to list the projects the subject user belongs to:

list_projects.sh
SUBJECT_TOKEN="<oidc-access-token-from-your-idp>"

# Step 1: exchange with no resource -- yields an org-scoped token
ACCESS_TOKEN=$(curl -s -X POST "https://api.example.com/auth/token-exchange" -u "warehouse-sync:$CLIENT_SECRET" -H "Content-Type: application/x-www-form-urlencoded" --data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" --data-urlencode "subject_token=$SUBJECT_TOKEN" --data-urlencode "subject_token_type=urn:ietf:params:oauth:token-type:access_token" --data-urlencode "audience=rhesis:org:acme" | jq -r .access_token)

# Step 2: list the projects the subject user is a member of
curl -s "https://api.example.com/projects/" -H "Authorization: Bearer $ACCESS_TOKEN" | jq '.[] | {id, name}'

GET /projects/ returns every project the org member is a member of, org-scoped rather than project-scoped, so the token from step 1 works for it without a resource. Pick an id from the list and pass it as the resource in a second exchange to get a project-scoped token.

The Java SDK exposes the same call as client.projects().list() — point RhesisClientBuilder.apiKey(...) at the org-scoped token from step 1, list projects, then re-point it at the project-scoped token from the second exchange.

Manage clients

EndpointPurpose
POST /organizations/{org_id}/auth-clientsCreate a client and return one-shot secret
GET /organizations/{org_id}/auth-clientsList clients without secrets
GET /organizations/{org_id}/auth-clients/{id}Read one client without secret
POST /organizations/{org_id}/auth-clients/{id}/rotateRotate secret and token epoch
POST /organizations/{org_id}/auth-clients/{id}/disableDisable a client
POST /organizations/{org_id}/auth-clients/{id}/enableRe-enable a client
DELETE /organizations/{org_id}/auth-clients/{id}Delete a disabled client

Rotating a client secret also advances the client’s token epoch. Existing client-bound refresh chains stop working after rotation.

Common errors

  • invalid_request: the audience is malformed or multi-valued, the resource is malformed or multi-valued, client credentials are missing, or a required form field is absent.
  • invalid_target: the organization slug does not exist, SSO is not configured, API Clients are not enabled, the resource does not resolve to a project in that organization, the project is inactive or deleted, or the requesting user is not a member of that project.
  • invalid_client: the supplied client credentials are wrong.
  • invalid_grant: the subject token is invalid, expired, replayed, or does not match the configured azp and audience.
  • invalid_scope: requested scopes are not allowed for the client.