# OAuth 2.0 — Auth Code + Standard Refresh Token

Use this skill when the provider uses the **authorization code grant** and the token response includes both `expires_in` and `refresh_token`.

## When to Use

✅ Use this pattern when the token response contains:
```json
{
  "access_token": "...",
  "token_type": "bearer",
  "expires_in": 3600,
  "refresh_token": "...",
  "scope": "..."
}
```

❌ Do NOT use if `refresh_token` is absent → use `auth-oauth2-code-longterm` instead.
❌ Do NOT use if an intermediate exchange endpoint is needed → use `auth-oauth2-code-exchange` instead.

**How to verify:** Check the provider's token endpoint response schema. Both `expires_in` and `refresh_token` must be present.

**Known examples:** Google Drive, HubSpot, Salesforce, Asana

---

## How Refresh Works

When the `access_token` expires, Falcon calls `refreshAuthentication`. The flow:

1. POST to the token endpoint with `grant_type: refresh_token` + the stored `refresh_token`
2. Provider returns a new `access_token` (and usually a new `refresh_token`)
3. `map_fields` maps the new values back to credentials

### When a step fails

Many providers rotate the refresh token: the grant returns a new one and kills the old one on the
spot. If anything after the grant fails, the new token exists only in that one run, and losing it
means the user has to reconnect from scratch. So a refresh action does not stop at the first
fatal step the way other actions do:

- The `map_fields` step still runs after a failed step, and whatever it maps is saved.
- It sees only the steps that succeeded. A failed request's output is the provider's error body
  (often an HTML error page), and that is hidden from it, so an expression reading the failed
  step resolves to nothing and the field keeps its stored value.
- Once one provider call has succeeded, every later request in the action is capped at 30s so a
  hang cannot outlast the Lambda. The grant itself is never capped, because slow grants do
  succeed and aborting one loses the rotated token.

The template below is already shaped for this. The rules under **Critical Rules** keep it that way.

---

## Template

> **The map key (`oauth2:` below) is the block's identifier, not a fixed keyword.** Gates
> A9/B6 match it against the researched `authMechanisms[].type` — name it after that type
> verbatim (e.g. `- oauth2_bot_token:` when research recorded `oauth2_bot_token`). A
> generic key on a specifically-typed mechanism fails the gate as "not configured" even
> though the block exists — a measured run's costliest repair round.

```yaml
authentication:
  - oauth2:
      type: oauth2
      label: OAuth 2.0
      grantType: authorization_code
      setupFields:
        - key: clientId
          label: Client ID
          type: text
          required: true
          secret: false
          placeholder: your-client-id
          description: Client ID from your {Provider} app registration
        - key: clientSecret
          label: Client Secret
          type: password
          required: true
          secret: true
          placeholder: your-client-secret
          description: Client Secret from your {Provider} app registration
        - key: scopes
          label: Scopes
          type: text
          required: false
          secret: false
          placeholder: "{default scopes}"
          description: Space-separated list of OAuth scopes to request
      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: Refresh {Provider} OAuth2 access token
          steps:
            - stepId: refresh_token_request
              description: Get new access token using refresh token
              stepFunction:
                functionName: request
                parameters:
                  baseUrl: https://provider.com
                  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: refresh_token
                      value: $.credentials.refreshToken
                      in: body
                    - name: client_id
                      value: $.credentials.clientId
                      in: body
                    - name: client_secret
                      value: $.credentials.clientSecret
                      in: body
            - stepId: map_token_response
              description: Map new tokens to credentials
              stepFunction:
                functionName: map_fields
                version: '2'
                parameters:
                  fields:
                    - targetFieldKey: accessToken
                      expression: $.access_token
                      type: string
                    - targetFieldKey: refreshToken
                      expression: $.refresh_token
                      type: string
                    - targetFieldKey: expiresIn
                      expression: $.expires_in
                      type: number
                  dataSource: $.steps.refresh_token_request.output.data
          result:
            data: $.steps.map_token_response.output.data
      environments:
        - key: production
          name: Production
      testActions:
        - action: {action_id}   # e.g. list_users, get_current_user, get_account
          required: true
```

### Key fields in `refreshAuthentication`

| Field | Value | Notes |
|---|---|---|
| `authorization.type` | `none` | Token endpoint uses body params, not Authorization header |
| `grant_type` | `refresh_token` | Always this value for token rotation |
| `refresh_token` | `$.credentials.refreshToken` | Stored from initial token exchange |
| `client_id` / `client_secret` | From credentials | Required by most providers |
| `dataSource` | `$.steps.refresh_token_request.output.data` | Map from the request step output |

> **Note:** Some providers use Basic Auth (`Authorization: Basic base64(clientId:clientSecret)`) on the token endpoint instead of body params. In that case, replace `authorization.type: none` with `type: basic` and remove `client_id`/`client_secret` from body args.

---

## Real Examples

### Google Drive

Requires `access_type: offline` and `prompt: consent` in `authorizationParams` to force issuance of a `refresh_token`. Without these, Google does not return a refresh token.

```yaml
# Inside oauth2 authentication entry:
# type: oauth2
# label: ...
# grantType: authorization_code
# setupFields: ...
authorization:
  type: oauth2
  authorizationUrl: https://accounts.google.com/o/oauth2/v2/auth
  authorizationParams:
    response_type: code
    client_id: $.credentials.clientId
    redirect_uri: ${apiHostUri}/connect/oauth2/googledrive/callback
    scope: '{{$.credentials.scopes ?? "https://www.googleapis.com/auth/drive"}}'
    access_type: offline
    prompt: consent
    include_granted_scopes: 'false'
    state: stackone
  tokenUrl: https://oauth2.googleapis.com/token
  token: $.credentials.accessToken
  includeBearer: true
refreshAuthentication:
  action:
    actionId: refresh_token_googledrive
    categories:
      - internal
    actionType: refresh_token
    label: Refresh Token
    description: Refresh Google Drive OAuth2 token
    steps:
      - stepId: refresh_token_request
        description: Get new access token
        stepFunction:
          functionName: request
          parameters:
            baseUrl: https://oauth2.googleapis.com
            url: /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: refresh_token
                value: $.credentials.refreshToken
                in: body
              - name: client_id
                value: $.credentials.clientId
                in: body
              - name: client_secret
                value: $.credentials.clientSecret
                in: body
      - stepId: map_token_response
        description: Map new tokens to credentials
        stepFunction:
          functionName: map_fields
          version: '2'
          parameters:
            fields:
              - targetFieldKey: accessToken
                expression: $.access_token
                type: string
              - targetFieldKey: refreshToken
                expression: $.refresh_token
                type: string
              - targetFieldKey: expiresIn
                expression: $.expires_in
                type: number
            dataSource: $.steps.refresh_token_request.output.data
    result:
      data: $.steps.map_token_response.output.data
```

> **Google note:** Google does not always return a new `refresh_token` on every refresh — it may return `null`. Map it anyway; Falcon retains the previous value if the new one is null.

---

## Critical Rules

### MUST:
- ✅ Always include both a `request` step and a `map_fields` step in `refreshAuthentication`
- ✅ Default to `authorization.type: none` on the refresh request (credentials in body); use `type: basic` when provider requires Basic Auth on the token endpoint
- ✅ Send `grant_type: refresh_token` in the request body
- ✅ Map `accessToken`, `refreshToken`, and `expiresIn` from the response
- ✅ Set `dataSource` to `$.steps.refresh_token_request.output.data` in `map_fields`
- ✅ Add provider-specific `authorizationParams` extras if required (e.g. `access_type: offline` for Google)
- ✅ Make the token request the first step and `map_fields` the last, so the rotated token is mapped whatever fails in between
- ✅ Point `result` at the `map_fields` output (`$.steps.map_token_response.output.data`). `result` is evaluated on a failed run too, with the failed step's output still in place
- ✅ Keep any calls between the grant and `map_fields` to the ones the credentials genuinely need (a `tenantId`, a `bhRestToken`). Each one is a chance to fail after the token has already rotated

### MUST NOT:
- ❌ Use `dataSource: $` in `map_fields` — reads root context, not the refresh response step output
- ❌ Omit `refreshToken` from `map_fields` — even if provider doesn't always return a new one
- ❌ Hardcode the token URL in `map_fields` — it belongs in the `request` step only
- ❌ Skip `Content-Type: application/x-www-form-urlencoded` — most token endpoints require it
- ❌ Map a whole step output to one credential (`expression: $.data`, or a bare `$.steps.x.output.data`). Map named fields (`$.access_token`). A whole-output mapping is how an error page once ended up saved as a token
- ❌ Point `result` straight at a request step's output. On a failed run that is the provider's error body, and its keys get saved as credentials
- ❌ Write to the provider's data inside a refresh action (installing an app, creating a record). Auth calls like a token or session-login endpoint are fine. A write can fail for its own reasons, like a permissions 403, and it breaks the refresh after the token has rotated

## Related Skills

- `auth-oauth2` — parent decision router
- `auth-oauth2-code-longterm` — use when token never expires and no `refresh_token`
- `auth-oauth2-code-exchange` — use when an intermediate exchange endpoint is required
- `auth-oauth2-client-credentials` — use for machine-to-machine flows
