# `ADK.Auth.OAuth2`
[🔗](https://github.com/zeroasterisk/adk-elixir/blob/main/lib/adk/auth/oauth2.ex#L1)

Core OAuth2 HTTP operations: authorization URL generation, auth code exchange,
and token refresh.

Uses `Req` for HTTP. All functions are stateless — they operate on credential
structs and return updated structs. The `CredentialManager` orchestrates
when to call these.

## Token Exchange Flow

    # 1. Build authorization URL (redirect user here)
    url = ADK.Auth.OAuth2.authorization_url(%{
      auth_endpoint: "https://accounts.google.com/o/oauth2/v2/auth",
      client_id: "my-client-id",
      redirect_uri: "https://myapp.com/auth/callback",
      scopes: ["openid", "profile"],
      state: "random-state-string"
    })

    # 2. After redirect, exchange code → tokens
    {:ok, updated_cred} = ADK.Auth.OAuth2.exchange_code(credential, opts)

    # 3. Later, refresh expired token
    {:ok, refreshed_cred} = ADK.Auth.OAuth2.refresh_token(credential, opts)

## Token Expiry

Tokens include `expires_at` (Unix timestamp). Use `expired?/1` or
`expires_soon?/1` to check before use. The `CredentialManager` handles
this automatically.

# `token_response`

```elixir
@type token_response() :: %{
  access_token: String.t(),
  token_type: String.t() | nil,
  expires_in: integer() | nil,
  refresh_token: String.t() | nil,
  scope: String.t() | nil,
  id_token: String.t() | nil
}
```

# `authorization_url`

```elixir
@spec authorization_url(keyword() | map()) :: String.t()
```

Build an OAuth2 authorization URL to redirect the user to.

Required options:
- `:auth_endpoint` — the authorization endpoint URL
- `:client_id` — OAuth2 client ID
- `:redirect_uri` — callback URL

Optional options:
- `:scopes` — list of requested scopes (default: `[]`)
- `:state` — CSRF state string (default: generated)
- `:access_type` — `"offline"` to request refresh token (Google-specific)
- `:extra_params` — map of additional query parameters

# `client_credentials`

```elixir
@spec client_credentials(
  ADK.Auth.Credential.t(),
  keyword()
) :: {:ok, ADK.Auth.Credential.t()} | {:error, term()}
```

Perform a client credentials grant (server-to-server, no user).

The credential must have `client_id`, `client_secret`, and `token_endpoint`.
Scopes from the credential are used if present.

Options:
- `:scopes` — override credential scopes
- `:http_opts` — extra options passed to `Req.post/2`

# `exchange_code`

```elixir
@spec exchange_code(
  ADK.Auth.Credential.t(),
  keyword()
) :: {:ok, ADK.Auth.Credential.t()} | {:error, term()}
```

Exchange an authorization code for access + refresh tokens.

The credential must have:
- `auth_code` — the code received from the OAuth callback
- `client_id`, `client_secret` — your OAuth app credentials
- `token_endpoint` — the token URL

Returns `{:ok, updated_credential}` with `access_token`, `refresh_token`,
and `expires_at` populated, and `auth_code` cleared.

Options:
- `:redirect_uri` — must match the one used in the authorization URL
- `:http_opts` — extra options passed to `Req.post/2`

# `expired?`

```elixir
@spec expired?(ADK.Auth.Credential.t()) :: boolean()
```

Returns true if the credential's access token has expired.

Returns false if no `expires_at` is set (assumes still valid).

# `expires_soon?`

```elixir
@spec expires_soon?(ADK.Auth.Credential.t(), non_neg_integer()) :: boolean()
```

Returns true if the credential will expire within the buffer window
(default: 5 minutes). Proactive refresh to avoid races.

# `needs_exchange?`

```elixir
@spec needs_exchange?(ADK.Auth.Credential.t()) :: boolean()
```

Returns true if the credential needs exchange (has auth_code but no access_token).

# `refresh_token`

```elixir
@spec refresh_token(
  ADK.Auth.Credential.t(),
  keyword()
) :: {:ok, ADK.Auth.Credential.t()} | {:error, term()}
```

Refresh an expired access token using the refresh token.

The credential must have `refresh_token`, `client_id`, `client_secret`,
and `token_endpoint`.

Options:
- `:scopes` — request specific scopes (optional)
- `:http_opts` — extra options passed to `Req.post/2`

# `refreshable?`

```elixir
@spec refreshable?(ADK.Auth.Credential.t()) :: boolean()
```

Returns true if refresh is possible (credential has refresh_token + endpoint).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
