Registry stack documentation: machine-readable Markdown.
Index of all pages: https://docs.registrystack.org/dev/llms.txt
Full corpus: https://docs.registrystack.org/dev/llms-full.txt

# Configure OAuth client credentials

> Configure Relay-owned OAuth client credentials with a closed request encoding, token response profile, and private destination binding.

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

## When to use this

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.

## Before you start

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](../../reference/project-configuration/) for the complete generated
field contract.
This page covers the decisions and verification path.

## Select the token request encoding

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

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

```yaml
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:

```yaml
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.

## Select the response profile

Choose one closed response profile.

`oauth2_bearer` accepts exactly:

```json
{
  "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:

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

Its token response contains only:

```json
{
  "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.

## Declare scope and audience

Add only values required by the reviewed authorization server:

```yaml
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.

## Bind private destinations and secrets

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

```yaml
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.

## Author strict token fixtures

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

```yaml
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.

## Verify the boundary

Run the complete offline fixture set:

```sh
registryctl test
```

Then inspect the redacted plan:

```sh
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:

```sh
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.

## Troubleshooting

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