# OAuth 2.0 — Auth Code + Long-lived Token

Use this skill when the provider uses the **authorization code grant** and issues a **non-expiring access token** — no `expires_in`, no `refresh_token` in the token response.

## When to Use

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

❌ Do NOT use if the response includes `expires_in`, `expires_on`, or `refresh_token` → use `auth-oauth2-code-refresh` instead.

**How to verify:** Check the provider's token endpoint response schema. If `expires_in` and `refresh_token` are absent or explicitly documented as unsupported, use this pattern.

**Known examples:** Vimeo, Aha!

---

## Critical Rule

`refreshAuthentication` is **always required** in Falcon OAuth2 — even when the token never expires. Use the `map_fields` pass-through: re-maps existing credentials back to themselves with no endpoint call.

**Omitting `refreshAuthentication` entirely will break OAuth2.**

---

## 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`, so name it after that
> type verbatim (e.g. `- oauth2_bot_token:`).

```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: Pass-through for {Provider} long-lived OAuth2 token (does not expire)
          steps:
            - stepId: map_token_response
              description: Re-map existing credentials — token does not expire
              stepFunction:
                functionName: map_fields
                version: '2'
                parameters:
                  fields:
                    - targetFieldKey: accessToken
                      expression: $.credentials.accessToken
                      type: string
                    - targetFieldKey: scopes
                      expression: $.credentials.scopes
                      type: string
                    # Add any other fields returned in the initial token response
                  dataSource: $
          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
```

### `authorizationParams` notes

| Field | Value | Notes |
|---|---|---|
| `response_type` | `code` | Always `code` for authorization code flow |
| `client_id` | `$.credentials.clientId` | From `setupFields` |
| `redirect_uri` | `${apiHostUri}/connect/oauth2/{provider}/callback` | StackOne callback URL |
| `scope` | JEXL with fallback | Default = provider's default scopes (space-separated). User can override via the `scopes` setupField. |
| `state` | `stackone` | Always include for CSRF protection |

### `map_fields` pass-through notes

- Map **every field** returned in the provider's initial token response
- Common fields: `accessToken`, `scopes`, `user`, `userId`, `teamId`
- Do not map fields that were not in the token response
- `dataSource: $` — reads from the root credentials object, not a step output

---

## Real Examples

### Vimeo

Token response: `access_token`, `token_type`, `scope`, `user`. No `expires_in`. No `refresh_token` (explicitly documented as unsupported).

```yaml
# Inside oauth2 authentication entry:
# type: oauth2
# label: ...
# grantType: authorization_code
# setupFields: ...
authorization:
  type: oauth2
  authorizationUrl: https://api.vimeo.com/oauth/authorize
  authorizationParams:
    response_type: code
    client_id: $.credentials.clientId
    redirect_uri: ${apiHostUri}/connect/oauth2/vimeo/callback
    # NOTE: `upload` scope excluded — pending approval from Vimeo
    scope: '{{$.credentials.scopes ?? "public private purchased create edit delete interact video_files stats promo_codes"}}'
    state: stackone
  tokenUrl: https://api.vimeo.com/oauth/access_token
  token: $.credentials.accessToken
  includeBearer: true
refreshAuthentication:
  action:
    actionId: refresh_token_vimeo
    categories:
      - internal
    actionType: refresh_token
    label: Refresh Token
    description: Pass-through for Vimeo long-lived OAuth2 token (does not expire)
    steps:
      - stepId: map_token_response
        description: Re-map existing credentials — token does not expire
        stepFunction:
          functionName: map_fields
          version: '2'
          parameters:
            fields:
              - targetFieldKey: accessToken
                expression: $.credentials.accessToken
                type: string
              - targetFieldKey: scopes
                expression: $.credentials.scopes
                type: string
              - targetFieldKey: user
                expression: $.credentials.user
                type: string
            dataSource: $
    result:
      data: $.steps.map_token_response.output.data
```

### Aha!

Dynamic `authorizationUrl` and `tokenUrl` built from `subdomain` configField. Uses `tokenParams` to send `client_id`, `client_secret`, and `redirect_uri` in the token exchange body. Maps a very large `expiresIn` value as a workaround signal for non-expiry.

> **Note:** Aha! places `subdomain` in `setupFields` because it is tied to the OAuth app registration (T1-scoped), not per end-user. Typically subdomain belongs in `configFields` — only use `setupFields` when it's part of the OAuth app identity.

```yaml
# Inside oauth2 authentication entry:
# type: oauth2
# label: ...
# grantType: authorization_code
setupFields:
  - key: subdomain
    label: Aha! Subdomain
    type: text
    required: true
    secret: false
    placeholder: yourcompany
    description: Your Aha! subdomain (e.g. acme from acme.aha.io)
  - key: clientId
    label: Client ID
    type: text
    required: true
    secret: false
  - key: clientSecret
    label: Client Secret
    type: password
    required: true
    secret: true
authorization:
  type: oauth2
  authorizationUrl: https://${credentials.subdomain}.aha.io/oauth/authorize
  authorizationParams:
    response_type: code
    client_id: $.credentials.clientId
    redirect_uri: '${apiHostUri}/connect/oauth2/aha/callback'
  tokenUrl: https://${credentials.subdomain}.aha.io/oauth/token
  tokenParams:
    grant_type: authorization_code
    client_id: $.credentials.clientId
    client_secret: $.credentials.clientSecret
    redirect_uri: '${apiHostUri}/connect/oauth2/aha/callback'
  token: $.credentials.accessToken
  includeBearer: true
refreshAuthentication:
  action:
    actionId: refresh_token_aha
    categories:
      - internal
    actionType: refresh_token
    label: Refresh Token
    description: Pass-through for Aha! long-lived OAuth2 token (does not expire)
    steps:
      - stepId: map_token_response
        description: Re-map existing credentials — token does not expire
        stepFunction:
          functionName: map_fields
          version: '2'
          parameters:
            fields:
              - targetFieldKey: accessToken
                expression: $.credentials.accessToken
                type: string
              - targetFieldKey: expiresIn
                expression: '{{3600000000}}'
                type: number
            dataSource: $
    result:
      data: $.steps.map_token_response.output.data
```

> **Aha! note:** `expiresIn: 3600000000` is a workaround — a very large number to signal effectively-never expiry. Not all providers need this; include only if the framework requires `expiresIn` to be set.

---

## Critical Rules

### MUST:
- ✅ Always include `refreshAuthentication` — even for non-expiring tokens
- ✅ Use `actionType: refresh_token` and `functionName: map_fields`
- ✅ Set `dataSource: $` (reads from credentials root, not a step)
- ✅ Map every field returned in the initial token response
- ✅ Use `state: stackone` in `authorizationParams`
- ✅ Set scope default to the provider's default scopes (space-separated)

### MUST NOT:
- ❌ Omit `refreshAuthentication` — breaks Falcon OAuth2
- ❌ Add an HTTP request step inside `refreshAuthentication` — no endpoint call needed
- ❌ Use `refreshUrl` or `tokenUrl` inside `refreshAuthentication`
- ❌ Map fields not present in the token response

## Related Skills

- `auth-oauth2` — parent decision router
- `auth-oauth2-code-refresh` — use when `expires_in` + `refresh_token` are present
- `auth-oauth2-code-exchange` — use when an intermediate exchange endpoint is required
- `auth-oauth2-client-credentials` — use for machine-to-machine flows
