# OAuth 2.0 — Auth Code + Token Exchange

Use this skill when the provider requires an **extra endpoint call** between receiving the authorization code and the final usable `access_token`. The exchange step converts or upgrades the initial token before it can be used for API requests.

## When to Use

✅ Use when the provider has a dedicated exchange endpoint separate from the standard token URL.
✅ Use when the initial `access_token` must be converted using a custom `grant_type` (e.g. `ig_exchange_token`, `fb_exchange_token`).
✅ Use when a single-use token must be converted to a permanent one.

❌ Do NOT use for standard `grant_type: refresh_token` flows → use `auth-oauth2-code-refresh`.
❌ Do NOT use when the initial token is already long-lived and directly usable → use `auth-oauth2-code-longterm`.

**Known exchange patterns:** Slack, Instagram, WhatsApp Business, Plaid

---

## Sub-patterns

Three distinct exchange patterns exist. Identify which one the provider uses before implementing.

| Sub-pattern | Trigger | Examples |
|---|---|---|
| **A — Bot/App token exchange** | Exchange initial token for a refresh-capable token via separate endpoint | Slack |
| **B — Short-to-long exchange** | Convert short-lived token to long-lived via custom grant type | Instagram, WhatsApp Business |
| **C — Public-to-permanent exchange** | Convert single-use public token to permanent access token | Plaid |

---

## Sub-pattern A — Bot/App Token Exchange (Slack)

Initial `access_token` cannot be refreshed directly. Must first call an exchange endpoint to get a `refresh_token`, then use standard refresh flow.

Uses a `condition` to skip the exchange step if `refreshToken` is already stored.

```yaml
# Inside oauth2 authentication entry:
# type: oauth2
# label: ...
# grantType: authorization_code
# setupFields: ...
# configFields: ...
authorization:
  type: oauth2
  authorizationUrl: https://provider.com/oauth/authorize
  authorizationParams:
    response_type: code
    client_id: $.credentials.clientId
    redirect_uri: ${apiHostUri}/connect/oauth2/{provider}/callback
    scope: '{{$.credentials.scopes ?? "{default scopes}"}}'
    state: stackone
  tokenUrl: https://provider.com/oauth/token
  token: $.credentials.accessToken
  includeBearer: true
refreshAuthentication:
  action:
    actionId: refresh_token_{provider}
    categories:
      - internal
    actionType: refresh_token
    label: Refresh Token
    description: Exchange and refresh {Provider} OAuth2 token
    steps:
      - stepId: exchange_token
        description: Exchange access token for refresh-capable token
        condition: '{{missing($.credentials.refreshToken)}}'
        stepFunction:
          functionName: request
          parameters:
            baseUrl: https://provider.com/api
            url: /oauth/exchange
            method: post
            authorization:
              type: none
            args:
              - name: Content-Type
                value: application/x-www-form-urlencoded
                in: headers
              - name: client_id
                value: $.credentials.clientId
                in: body
              - name: client_secret
                value: $.credentials.clientSecret
                in: body
              - name: token
                value: $.credentials.accessToken
                in: body
      - stepId: refresh_token_request
        description: Get new access token using refresh token
        stepFunction:
          functionName: request
          parameters:
            baseUrl: https://provider.com/api
            url: /oauth/token
            method: post
            authorization:
              type: none
            args:
              - name: Content-Type
                value: application/x-www-form-urlencoded
                in: headers
              - name: grant_type
                value: refresh_token
                in: body
              - name: client_id
                value: $.credentials.clientId
                in: body
              - name: client_secret
                value: $.credentials.clientSecret
                in: body
              - name: refresh_token
                value: '{{$.credentials.refreshToken ?? $.steps.exchange_token.output.data.refresh_token}}'
                in: body
      - stepId: map_token_response
        description: Map new tokens to credentials
        stepFunction:
          functionName: map_fields
          version: '2'
          parameters:
            fields:
              - targetFieldKey: accessToken
                expression: $.steps.refresh_token_request.output.data.access_token
                type: string
              - targetFieldKey: refreshToken
                expression: $.steps.refresh_token_request.output.data.refresh_token
                type: string
              - targetFieldKey: expiresIn
                expression: $.steps.refresh_token_request.output.data.expires_in
                type: number
            dataSource: $
    result:
      data: $.steps.map_token_response.output.data
```

**Real example — Slack** (`slack/slack.connector.s1.yaml`):
- Exchange endpoint: `POST https://slack.com/api/oauth.v2.exchange`
- Refresh endpoint: `POST https://slack.com/api/oauth.v2.access`
- `refresh_token` value: `{{$.credentials.refreshToken ?? $.steps.exchange_bot_access_token.output.data.refresh_token}}`

---

## Sub-pattern B — Short-to-Long Exchange (Instagram, WhatsApp)

Initial token is short-lived. A single exchange call converts it to a long-lived token using a custom `grant_type`. No standard `refresh_token` — the exchange IS the renewal mechanism.

```yaml
# Inside oauth2 authentication entry:
# type: oauth2
# label: ...
# grantType: authorization_code
# setupFields: ...
# configFields: ...
authorization:
  type: oauth2
  authorizationUrl: https://provider.com/oauth/authorize
  authorizationParams:
    response_type: code
    client_id: $.credentials.clientId
    redirect_uri: ${apiHostUri}/connect/oauth2/{provider}/callback
    scope: '{{$.credentials.scopes ?? "{default scopes}"}}'
    state: stackone
  tokenUrl: https://provider.com/oauth/token
  token: $.credentials.accessToken
  includeBearer: true
refreshAuthentication:
  action:
    actionId: refresh_token_{provider}
    categories:
      - internal
    actionType: refresh_token
    label: Refresh Token
    description: Exchange {Provider} short-lived token for a long-lived token
    steps:
      - stepId: exchange_token
        description: Exchange current token for long-lived token
        stepFunction:
          functionName: request
          parameters:
            baseUrl: https://graph.provider.com
            url: /oauth/access_token
            method: get
            authorization:
              type: none
            args:
              - name: grant_type
                value: {provider_exchange_token}   # e.g. ig_exchange_token, fb_exchange_token
                in: query
              - name: client_secret
                value: $.credentials.clientSecret
                in: query
              - name: access_token
                value: $.credentials.accessToken
                in: query
      - stepId: map_token_response
        description: Map new long-lived token to credentials
        stepFunction:
          functionName: map_fields
          version: '2'
          parameters:
            fields:
              - targetFieldKey: accessToken
                expression: $.access_token
                type: string
              - targetFieldKey: expiresIn
                expression: $.expires_in
                type: number
            dataSource: $.steps.exchange_token.output.data
    result:
      data: $.steps.map_token_response.output.data
```

**Real examples:**

| Provider | grant_type | Endpoint | Method |
|---|---|---|---|
| Instagram | `ig_exchange_token` | `GET https://graph.instagram.com/access_token` | GET with query params |
| WhatsApp Business | `fb_exchange_token` | `GET https://graph.facebook.com/v24.0/oauth/access_token` | GET with query params |

---

## Sub-pattern C — Public-to-Permanent Exchange (Plaid)

A single-use `public_token` is obtained through a provider's frontend SDK (not the standard OAuth flow). It must be exchanged via an API call for a permanent `access_token` before any API requests can be made.

```yaml
# Typically implemented as a standalone action, not in refreshAuthentication
# Inside the action definition:
steps:
  - stepId: exchange_public_token
    description: Exchange single-use public token for permanent access token
    stepFunction:
      functionName: request
      parameters:
        url: /item/public_token/exchange
        method: post
        args:
          - name: client_id
            value: $.credentials.clientId
            in: body
          - name: secret
            value: $.credentials.secret
            in: body
          - name: public_token
            value: $.inputs.public_token
            in: body
result:
  data: $.steps.exchange_public_token.output.data
```

**Real example — Plaid** (`plaid/plaid.connector.s1.yaml`):
- Exchange endpoint: `POST /item/public_token/exchange`
- Credentials sent in headers: `PLAID-CLIENT-ID`, `PLAID-SECRET`
- The exchange action is called by the user during setup, not automatically by Falcon

> Pointing `result` straight at the request step is fine here only because this is a standalone
> action. Inside `refreshAuthentication` add a `map_fields` step and point `result` at that
> instead: on a failed refresh the request step's output is the provider's error body, and its
> keys would be saved as credentials. See **When a step fails** in `auth-oauth2-code-refresh`.

---

## Critical Rules

### MUST:
- ✅ Identify which sub-pattern the provider uses before implementing
- ✅ Use `condition: '{{missing($.credentials.refreshToken)}}'` on the exchange step in Sub-pattern A — skip if refresh token already exists
- ✅ Use JEXL fallback for `refresh_token` in Sub-pattern A: `{{$.credentials.refreshToken ?? $.steps.exchange_token.output.data.refresh_token}}`
- ✅ In Sub-pattern B `map_fields`, set `dataSource` to the exchange step output, not `$`
- ✅ In Sub-pattern A `map_fields`, set `dataSource: $` and use full step paths for expressions
- ✅ In `refreshAuthentication`, keep `map_fields` as the last step and point `result` at its output, as in `auth-oauth2-code-refresh` (see **When a step fails** there)

### MUST NOT:
- ❌ Use Sub-pattern A template for Sub-pattern B — they have different step counts and dataSource patterns
- ❌ Omit the `condition` on the exchange step in Sub-pattern A — without it, exchange runs on every refresh unnecessarily
- ❌ Assume the exchange endpoint uses POST — Instagram and WhatsApp use GET with query params

## Related Skills

- `auth-oauth2` — parent decision router
- `auth-oauth2-code-longterm` — use when token never expires and no exchange needed
- `auth-oauth2-code-refresh` — use for standard `grant_type: refresh_token` flows
- `auth-oauth2-client-credentials` — use for machine-to-machine flows
