# Investigate Auth — OAuth2

## Purpose

Extract every field needed to generate a Falcon `oauth2` auth config for one specific grant type. Called once per OAuth2 grant type — if a provider supports both `authorization_code` and `client_credentials`, this skill is invoked twice with different `grant_type` inputs.

---

## Input

| Parameter | Required | Values |
|-----------|----------|--------|
| `provider` | ✅ | Provider slug |
| `auth_docs_url` | ✅ | URL of the auth documentation page |
| `grant_type` | ✅ | One of: `authorization_code` \| `authorization_code_pkce` \| `implicit` \| `password_credentials` \| `client_credentials` |

---

## Fetch Strategy

Follow the shared fallback chain in `../shared/auth-investigation-common.md → Fetch strategy` (WebFetch → crwl → Playwright → GitHub search, with soft-404 detection).

GitHub search queries for this skill (step 4 — always run):
```
WebSearch("<provider> oauth2 authorization_code client_id site:github.com")
WebSearch("<provider> oauth refresh_token connector site:github.com")
WebSearch("<provider> oauth scope list site:github.com")
WebSearch("<provider> oauth developer app setup redirect_uri site:github.com")
```

---

## What to Extract

### Grant type behaviour — extract fields relevant to the given `grant_type`:

| `grant_type` | Flow description | Key URLs needed |
|---|---|---|
| `authorization_code` | User authorizes via browser redirect, code exchanged for token | `authorization_url`, `token_url` |
| `authorization_code_pkce` | Same as authorization_code + PKCE code challenge/verifier | `authorization_url`, `token_url`, `code_challenge_method` |
| `implicit` | Token returned directly from authorization endpoint, no code exchange | `authorization_url` only |
| `password_credentials` | User supplies username + password directly to token endpoint | `token_url` only |
| `client_credentials` | App posts `client_id` + `client_secret` to get token, no user redirect | `token_url` only |

### Scopes — always attempt to fetch, even if not on the main auth page:

Most OAuth2 providers document scopes. Run the full 4-step search strategy in `../shared/auth-investigation-common.md → Scope extraction search strategy` before setting `scopes: []` — and note the absence in `quirks` if nothing is found.

### Refresh token behavior (skip for `implicit` and `client_credentials`):

- Does the provider rotate the refresh value on each renewal call?
- What is the documented expiry duration (seconds)?
- Are the client id/secret sent in the request body or as a Basic authorization header?

### Setup fields — what the user must provide when connecting:

- `authorization_code` / `authorization_code_pkce` / `implicit`: always `clientId`, `clientSecret`
- `password_credentials`: always `clientId`, `clientSecret` + user `username`, `password`
- `client_credentials`: always `clientId`, `clientSecret`
- Sometimes extra: `subdomain`, `accountId`, `instanceUrl`, etc.

Capture the properties in `../shared/auth-investigation-common.md → Credential field properties` for every field (`key`, `label`, `type`, `required`, `secret`, `placeholder`, `description`, `where_to_find`).

### Developer app setup (skip for `client_credentials` and `password_credentials`):

- Where to create the app
- What redirect URI to configure: `https://api.stackone.com/connect/oauth2/<provider>/callback`
- Steps summary

---

## Output File

**Path:** `.connector-dev/<provider>/research/auth/auth_oauth2_<grant_type>.json`

e.g. `.connector-dev/asana/research/auth/auth_oauth2_authorization_code.json`

```json
{
  "grant_type": "authorization_code",
  "supported": true,
  "label": "OAuth 2.0",
  "auth_docs_url": "<auth docs URL fetched>",

  "authorization_code": {
    "authorization_url": "https://provider.com/oauth/authorize",
    "authorization_params": {
      "response_type": "code",
      "scope_param_name": "scope",
      "scope_separator": " ",
      "client_id_param": "client_id",
      "redirect_uri_param": "redirect_uri",
      "state_param": "state",
      "extra_params": []
    },
    "token_url": "https://provider.com/oauth/token",
    "token_body_format": "form",
    "include_bearer": true
  },

  "authorization_code_pkce": null,
  "implicit": null,
  "password_credentials": null,
  "client_credentials": null,

  "refresh_token": {
    "supported": true,
    "token_url": "https://provider.com/oauth/token",
    "body_format": "form",
    "sends_client_credentials_in": "body",
    "sends_redirect_uri": true,
    "issues_new_refresh_token": true,
    "access_token_expiry_seconds": 3600,
    "response_fields": {
      "access_token_path": "$.access_token",
      "refresh_token_path": "$.refresh_token",
      "expires_in_path": "$.expires_in"
    },
    "notes": null
  },

  "setup_fields": [
    {
      "key": "clientId",
      "label": "Client ID",
      "type": "text",
      "required": true,
      "secret": false,
      "placeholder": "4A98C887DABEBDF0BD8EF7D",
      "description": "Client ID from your provider developer app registration.",
      "where_to_find": "Developer Portal > Apps > Your App > Auth > Client ID"
    },
    {
      "key": "clientSecret",
      "label": "Client Secret",
      "type": "password",
      "required": true,
      "secret": true,
      "placeholder": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      "description": "Client Secret from your provider developer app.",
      "where_to_find": "Developer Portal > Apps > Your App > Auth > Client Secret"
    }
  ],

  "scopes": [
    // [] if provider has no documented scopes
    {
      "name": "read:users",
      "description": "Read user profile data",
      "required": true,
      "category": "users",
      "tier_restriction": null
    }
  ],

  "scope_strategy": "required_plus_optional",  // null if no scopes

  "base_url": {
    "is_dynamic": false,
    "static_url": "https://api.provider.com/v2",
    "dynamic_expression": null
  },

  "developer_app_setup": {
    "redirect_uri": "https://api.stackone.com/connect/oauth2/<provider>/callback",
    "steps_summary": "Go to developer.provider.com → Create App → Auth tab → add redirect URI → copy Client ID and Secret",
    "docs_url": "<developer portal URL>"
  },

  "environments": [
    { "key": "production", "name": "Production" }
  ],

  "rate_limit": {
    "requests_per_second": null,
    "requests_per_minute": null,
    "retry_after_header": null,
    "notes": null
  },

  "quirks": [],
  "github_findings": []
}
```

### Grant type block rules:

| `grant_type` input | Populate block | Set all others to |
|---|---|---|
| `authorization_code` | `authorization_code` | `null` |
| `authorization_code_pkce` | `authorization_code_pkce` (include `code_challenge_method`) | `null` |
| `implicit` | `implicit` (`authorization_url` only, no `token_url`) | `null` |
| `password_credentials` | `password_credentials` (`token_url` + user credential fields) | `null` |
| `client_credentials` | `client_credentials` (`token_url` + client credential fields) | `null` |

**Additional per-grant rules:**
- `implicit`: set `refresh_token.supported: false` — implicit flow does not issue refresh tokens
- `client_credentials`: set `refresh_token.supported: false` and `developer_app_setup: null`
- `password_credentials`: add `username` and `password` to `setup_fields`
- `authorization_code_pkce`: add `code_challenge_method` (`S256` or `plain`) to the block
- `scope_strategy` values: `required_plus_optional` (HubSpot), `single_list` (most), `static_in_config` (Adobe-style)
- `sends_client_credentials_in`: `"body"` or `"header"` (Basic auth header)
- Always include the StackOne redirect URI in `developer_app_setup.redirect_uri`

---

## Anti-Patterns

Shared anti-patterns (scope handling, `where_to_find`, `placeholder`, `secret` typing) live in `../shared/auth-investigation-common.md → Shared anti-patterns`. OAuth2-specific:

- ❌ Populate more than one grant type block — only the block matching `grant_type` input is non-null
- ❌ Leave `authorization_url` null for `authorization_code` or `implicit` flows
- ❌ Set `scopes: []` without noting it in `quirks` — always flag when no scopes were found
- ❌ Omit `grant_type` from the output JSON — must match the input value exactly
- ❌ Set `refresh_token.supported: true` for `implicit` or `client_credentials` grant types
- ❌ Guess `issues_new_refresh_token` — only set `true` if provider docs explicitly state it
