Ltix.OAuth.Client (Ltix v0.1.0)

Copy Markdown View Source

Authenticated OAuth session for LTI Advantage service calls.

Holds an access token, tracks which scopes were granted, and provides explicit refresh. Pass this struct to service functions like Ltix.MembershipsService.get_members/2.

Checking expiry

client =
  if Ltix.OAuth.Client.expired?(client) do
    Ltix.OAuth.Client.refresh!(client)
  else
    client
  end

Reusing tokens across contexts

A token is valid across contexts (courses, launches) on the same registration. Reuse a cached token with different endpoints:

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

Or build from a previously cached AccessToken:

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

Summary

Functions

Check whether the client's token has expired.

Build a client from a cached AccessToken.

Check whether the client was granted a specific scope.

Re-acquire the token using the stored registration and endpoints.

Same as refresh/1 but raises on error.

Require any one of the given scopes.

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

Swap endpoints on an existing client.

Types

t()

@type t() :: %Ltix.OAuth.Client{
  access_token: String.t(),
  endpoints: %{required(module()) => term()},
  expires_at: DateTime.t(),
  registration: Ltix.Registration.t(),
  req_options: keyword(),
  scopes: MapSet.t(String.t())
}

Functions

expired?(client)

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

Check whether the client's token has expired.

Uses a 60-second buffer to avoid using a token that is about to expire.

Examples

iex> client = %Ltix.OAuth.Client{
...>   access_token: "tok",
...>   expires_at: DateTime.add(DateTime.utc_now(), 3600),
...>   scopes: MapSet.new(),
...>   registration: nil,
...>   req_options: []
...> }
iex> Ltix.OAuth.Client.expired?(client)
false

from_access_token(token, opts)

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

Build a client from a cached AccessToken.

Validates endpoints and checks that the token's granted scopes cover the required scopes for all endpoints.

Options

  • :registration (required) - the Ltix.Registration for refresh
  • :endpoints (required) - map of service modules to endpoint structs
  • :req_options - options passed through to Req.request/2 (default: [])

from_access_token!(token, opts)

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

Same as from_access_token/2 but raises on error.

has_scope?(client, scope)

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

Check whether the client was granted a specific scope.

Examples

iex> client = %Ltix.OAuth.Client{
...>   access_token: "tok",
...>   expires_at: DateTime.utc_now(),
...>   scopes: MapSet.new(["scope:read"]),
...>   registration: nil,
...>   req_options: []
...> }
iex> Ltix.OAuth.Client.has_scope?(client, "scope:read")
true
iex> Ltix.OAuth.Client.has_scope?(client, "scope:write")
false

refresh(client)

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

Re-acquire the token using the stored registration and endpoints.

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

refresh!(client)

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

Same as refresh/1 but raises on error.

require_any_scope(client, scopes)

@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(client, scope)

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

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

with_endpoints(client, endpoints)

@spec with_endpoints(t(), %{required(module()) => term()}) ::
  {:ok, t()} | {:error, Exception.t()}

Swap endpoints on an existing client.

Validates the new endpoints and checks that the client's granted scopes cover the required scopes. The token remains the same.

with_endpoints!(client, endpoints)

@spec with_endpoints!(t(), %{required(module()) => term()}) :: t()

Same as with_endpoints/2 but raises on error.