Skip to content
Registry StackDocsDevelopment (unreleased)

Configure OAuth client credentials

View as Markdown

Configure OAuth 2.0 client credentials when a reviewed HTTP source requires a bearer token before Relay can perform an authorized source request.

Use this authentication type only for a source whose token endpoint, request encoding, response shape, scopes, and token lifetime have source-owner approval.

OAuth changes source authentication, not source or claim ownership. Relay owns the token exchange and source authorization header. Rhai scripts and callers receive no client secret, token, or token response.

You need:

  • A project created with the http template
  • The reviewed token endpoint and source endpoint
  • The required token request encoding
  • The exact accepted token response shape
  • Operator-owned client id and client secret values
  • Synthetic token success and failure observations

Use project configuration for the complete generated field contract. This page covers the decisions and verification path.

In the integration’s source.auth, select JSON or form encoding.

Use JSON when the token endpoint requires an application/json body:

source:
auth:
type: oauth2_client_credentials
request: json
response_profile: oauth2_bearer

Use form when the endpoint requires an application/x-www-form-urlencoded body:

source:
auth:
type: oauth2_client_credentials
request: form
response_profile: oauth2_bearer

Both encodings send grant_type=client_credentials plus the client id and client secret. The request body remains Relay-owned and unavailable to Rhai.

Choose one closed response profile.

oauth2_bearer accepts exactly:

{
"access_token": "SYNTHETIC_FIXTURE_TOKEN",
"token_type": "Bearer",
"expires_in": 300
}

access_token must be non-empty and bounded. token_type must be exactly Bearer. expires_in must be an integer in the supported range. Relay limits cache lifetime by the token expiry and the optional authored refresh_skew.

oauth2_bearer_no_expiry accepts exactly:

source:
auth:
type: oauth2_client_credentials
request: json
response_profile: oauth2_bearer_no_expiry

Its token response contains only:

{
"access_token": "SYNTHETIC_FIXTURE_TOKEN",
"token_type": "Bearer"
}

The no-expiry profile rejects expires_in and refresh_skew. Token caching is disabled. Relay performs a fresh bounded exchange for every authorized execution.

Both profiles reject unknown or duplicate members, refresh tokens, identity tokens, redirects, non-success status, unexpected content type, and oversized responses.

Add only values required by the reviewed authorization server:

source:
auth:
type: oauth2_client_credentials
request: form
response_profile: oauth2_bearer
scope: records.read records.verify
audience: https://registry.invalid
refresh_skew: 20s

scope is one bounded space-separated string. audience is one bounded token request value. Neither value can come from caller input or a source response.

Registryctl 1.0 project authoring does not expose a separate OAuth resource field. If an authorization server requires the OAuth resource parameter rather than audience, record that as an unsupported contract instead of relabeling the value or adding an arbitrary request member.

In environments/<environment>.yaml, bind the source and token destinations:

integrations:
person-record:
source:
origin: https://registry.invalid
credential:
client_id: { secret: REGISTRY_CLIENT_ID }
client_secret: { secret: REGISTRY_CLIENT_SECRET }
generation: 1
oauth:
origin: https://identity.invalid
path: /oauth/token
generation: 1

The integration id must match the project. The environment contains secret references, not values. The private source origin and token endpoint may differ, but both are fixed private bindings owned by Relay. Redirects and destination fallback fail closed.

The operator supplies the referenced client values only to consultation Relay. Keep them out of project YAML, fixtures, scripts, generated portable artifacts, logs, and command arguments.

When rotating either credential value, update the operator secret and increment credential.generation. When changing the token destination binding, increment oauth.generation. Then rerun test, check, review compare, and build; review and sign every lane Registryctl reports as affected.

For each source fixture, place the token interaction before the source interaction:

interactions:
- expect:
method: POST
path: /oauth/token
body:
grant_type: client_credentials
scope: records.read records.verify
audience: https://registry.invalid
respond:
status: 200
headers: { Content-Type: application/json }
body:
access_token: SYNTHETIC_FIXTURE_TOKEN
token_type: Bearer
expires_in: 300

Match the expected body to the selected JSON or form encoding. Keep the token synthetic.

Add fixtures that reject:

  • Non-success token status
  • Redirect
  • Missing or unexpected media type
  • Missing, duplicate, or unknown response members
  • Empty or oversized access token
  • Token type other than exact Bearer
  • Missing, non-integer, or invalid expires_in for oauth2_bearer
  • Any expires_in for oauth2_bearer_no_expiry
  • Oversized response body
  • Authorization denial before the token request

Do not copy an upstream error body into retained fixture results or logs.

Run the complete offline fixture set:

Terminal window
registryctl test

Then inspect the redacted plan:

Terminal window
registryctl check --explain

Confirm the request encoding, response profile, scope, audience, token destination, secret consumers, generations, source destination, and bounds. The plan must keep credentials and token acquisition in consultation Relay.

Use the generic local environment only after the fixtures pass:

Terminal window
registryctl dev --detach
registryctl dev smoke
registryctl dev down

The denial scenario must report zero token and source calls. The authorized scenario reports one token call for the no-expiry profile. For the expiry profile, its first authorized execution reports one token exchange; Relay owns subsequent expiry-bound cache behavior.

SymptomCauseFix
The token fixture rejects a success responseThe body differs from the selected closed profile.Correct the fixture or select the profile that matches the reviewed endpoint.
refresh_skew is rejectedThe no-expiry profile cannot cache or refresh.Remove the field or use oauth2_bearer with required integer expiry.
The server requires resourceProject authoring does not expose that token parameter.Keep the integration unsupported until a reviewed contract adds it.
A redirect failsToken destination redirects are outside the fixed authority.Bind the final reviewed HTTPS origin and path directly.
A token or credential appears outside RelayThe secret boundary failed.Stop testing, rotate the credential, and report the failure through the repository security process.