# `Ltix.OAuth.Client`
[🔗](https://github.com/DecoyLex/ltix/blob/main/lib/ltix/oauth/client.ex#L1)

Authenticated OAuth session for LTI Advantage service calls.

Holds an `Ltix.OAuth.AccessToken` with the scopes it was granted, the
registration and deployment it was requested for, and the service
endpoints it calls, and provides explicit refresh. Pass this struct to
service functions like `Ltix.MembershipsService.list_members/2`.

`registration` is the struct you authenticated with, which can be your own
struct implementing `Ltix.Registerable`.

## Refreshing

    client = Ltix.OAuth.Client.refresh!(client)

## Reusing tokens across contexts

A token is valid across contexts (courses, launches) on the same
registration. A platform may also limit it to the deployment it was
requested for. Reuse a cached token with different endpoints.

    {:ok, client_b} = Ltix.OAuth.Client.with_endpoints(client, %{
      Ltix.MembershipsService => course_b_endpoint
    })

Or cache `client.token` and build a client from it later.

    {:ok, client} = Ltix.OAuth.Client.from_access_token(cached_token,
      registration: registration,
      deployment: deployment,
      endpoints: %{Ltix.MembershipsService => endpoint}
    )

# `t`

```elixir
@type t() :: %Ltix.OAuth.Client{
  deployment: Ltix.Deployable.t() | String.t() | nil,
  endpoints: %{required(module()) =&gt; term()},
  local_network_access: :none | :local | :loopback | nil,
  registration: Ltix.Registerable.t(),
  req_options: keyword(),
  storage_adapter: module() | nil,
  storage_adapter_options: keyword(),
  token: Ltix.OAuth.AccessToken.t()
}
```

# `expired?`

```elixir
@spec expired?(t()) :: boolean()
```

Check whether the client's token has expired, the same way as
`Ltix.OAuth.AccessToken.expired?/1`.

## Examples

    iex> token = %Ltix.OAuth.AccessToken{expires_at: DateTime.add(DateTime.utc_now(), 3600)}
    iex> Ltix.OAuth.Client.expired?(%Ltix.OAuth.Client{token: token})
    false

# `force_refresh`

```elixir
@spec force_refresh(t()) :: {:ok, t()} | {:error, Exception.t()}
```

Force a refresh regardless of expiry.
Useful for testing or for refreshing before making a batch of calls.

# `force_refresh!`

```elixir
@spec force_refresh!(t()) :: t()
```

Same as `force_refresh/1` but raises on error.

# `from_access_token`

```elixir
@spec from_access_token(Ltix.OAuth.AccessToken.t(), keyword()) ::
  {:ok, t()} | {:error, Exception.t()}
```

Build a client from a cached `AccessToken`.

The token may lack some of the scopes the endpoints need.

## Options

* `:registration` (`t:term/0`) - Required. Any struct implementing `Ltix.Registerable`.

* `:endpoints` (`t:map/0`) - Required. Map of service modules to endpoint structs.

* `:deployment` (`t:term/0`) - The deployment the token is for, as a struct implementing `Ltix.Deployable` or a deployment ID. Token requests include it.

* `:req_options` (`t:keyword/0`) - Options passed through to `Req.request/2`. The default value is `[]`.

* `:local_network_access` (`nil` | `:none` | `:local` | `:loopback`) - Addresses platform requests may connect to besides public ones. `:local`
  adds private network addresses, and `:loopback` adds those and this
  machine. Defaults to `config :ltix, :local_network_access`, then `:none`.

* `:storage_adapter` (`t:module/0`) - Module implementing `Ltix.StorageAdapter`, which signs token requests. Defaults to the application configuration.

* `:storage_adapter_options` (`t:keyword/0`) - Options passed to the storage adapter's signing callbacks. The default value is `[]`.

* `:require_all_scopes` (`t:boolean/0`) - Return a `ScopeMismatch` error unless every scope the endpoints need was granted. The default value is `false`.

# `from_access_token!`

```elixir
@spec from_access_token!(Ltix.OAuth.AccessToken.t(), keyword()) :: t()
```

Same as `from_access_token/2` but raises on error.

# `has_scope?`

```elixir
@spec has_scope?(t(), String.t()) :: boolean()
```

Check whether the client's token was granted a specific scope.

## Examples

    iex> token = %Ltix.OAuth.AccessToken{granted_scopes: MapSet.new(["scope:read"])}
    iex> client = %Ltix.OAuth.Client{token: token}
    iex> Ltix.OAuth.Client.has_scope?(client, "scope:read")
    true
    iex> Ltix.OAuth.Client.has_scope?(client, "scope:write")
    false

# `refresh`

```elixir
@spec refresh(t()) :: {:ok, t()} | {:error, Exception.t()}
```

If expired, re-acquire the token using the stored registration and endpoints.
Otherwise, return the client unchanged.

Re-derives requested scopes from endpoints via each service's `scopes/1`
callback, so a transient partial grant does not become permanent.

# `refresh!`

```elixir
@spec refresh!(t()) :: t()
```

Same as `refresh/1` but raises on error.

# `request`

```elixir
@spec request(t(), module(), keyword()) ::
  {:ok, Req.Response.t()} | {:error, Exception.t()}
```

Send an authenticated request to a platform service.

Use this to call a platform from your own `Ltix.AdvantageService`. `opts`
are `Req` request options such as `:url`, `:method`, `:headers`, `:params`,
and `:json`. The request has the client's access token as its bearer token,
and it gets the same checks as every request Ltix sends, including HTTPS and
the client's `:local_network_access` setting.

Returns `{:ok, response}` for a successful status. A 401 or 403 returns
`Ltix.Errors.Security.AccessDenied`, and any other status returns
`Ltix.Errors.Unknown.TransportError`.

In tests, stub the request with `Req.Test.stub(service, ...)`.

## Examples

    {:ok, %Req.Response{body: body}} =
      Ltix.OAuth.Client.request(client, MyApp.AttendanceService,
        url: endpoint.attendance_url,
        headers: [{"accept", "application/json"}]
      )

# `request_pages`

```elixir
@spec request_pages(t(), module(), keyword()) ::
  {:ok, Enumerable.t()} | {:error, Exception.t()}
```

Send an authenticated GET request and follow the `rel="next"` links of the
responses.

Takes the options of `request/3`. `:params` apply to the first page only.

Fetches the first page before returning. Returns `{:ok, stream}` with
`{:ok, body}` for each page, or `{:error, error}` if the first page fails.
Later pages are fetched as the stream is consumed. A later failure emits one
`{:error, error}` and ends the stream.

## Examples

    {:ok, pages} =
      Ltix.OAuth.Client.request_pages(client, MyApp.AttendanceService,
        url: endpoint.attendance_url,
        params: [limit: 50]
      )

    Enum.flat_map(pages, fn {:ok, body} -> body["records"] end)

# `require_any_scope`

```elixir
@spec require_any_scope(t(), [String.t()]) :: :ok | {:error, Exception.t()}
```

Require any one of the given scopes.

Returns `:ok` if at least one scope from the list was granted.

# `require_scope`

```elixir
@spec require_scope(t(), String.t()) :: :ok | {:error, Exception.t()}
```

Require a specific scope, returning an error if not granted.

# `require_unexpired`

```elixir
@spec require_unexpired(t()) :: :ok | {:error, Exception.t()}
```

Require a token that hasn't expired, returning
`Ltix.Errors.Security.AccessTokenExpired` if it has.

## Examples

    iex> token = %Ltix.OAuth.AccessToken{expires_at: DateTime.add(DateTime.utc_now(), 3600)}
    iex> Ltix.OAuth.Client.require_unexpired(%Ltix.OAuth.Client{token: token})
    :ok

# `with_endpoints`

```elixir
@spec with_endpoints(t(), %{required(module()) =&gt; term()}, keyword()) ::
  {:ok, t()} | {:error, Exception.t()}
```

Replace the endpoints of an existing client.

The token remains the same, so it may lack some of the scopes the new endpoints need.

## Options

* `:require_all_scopes` (`t:boolean/0`) - Return a `ScopeMismatch` error unless every scope the endpoints need was granted. The default value is `false`.

# `with_endpoints!`

```elixir
@spec with_endpoints!(t(), %{required(module()) =&gt; term()}, keyword()) :: t()
```

Same as `with_endpoints/3` but raises on error.

---

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