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}
)
Summary
Functions
Check whether the client's token has expired, the same way as
Ltix.OAuth.AccessToken.expired?/1.
Force a refresh regardless of expiry. Useful for testing or for refreshing before making a batch of calls.
Same as force_refresh/1 but raises on error.
Build a client from a cached AccessToken.
Same as from_access_token/2 but raises on error.
Check whether the client's token was granted a specific scope.
If expired, re-acquire the token using the stored registration and endpoints. Otherwise, return the client unchanged.
Same as refresh/1 but raises on error.
Send an authenticated request to a platform service.
Send an authenticated GET request and follow the rel="next" links of the
responses.
Require any one of the given scopes.
Require a specific scope, returning an error if not granted.
Require a token that hasn't expired, returning
Ltix.Errors.Security.AccessTokenExpired if it has.
Replace the endpoints of an existing client.
Same as with_endpoints/3 but raises on error.
Types
@type t() :: %Ltix.OAuth.Client{ deployment: Ltix.Deployable.t() | String.t() | nil, endpoints: %{required(module()) => 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() }
Functions
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
@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.
Same as force_refresh/1 but raises on error.
@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(term/0) - Required. Any struct implementingLtix.Registerable.:endpoints(map/0) - Required. Map of service modules to endpoint structs.:deployment(term/0) - The deployment the token is for, as a struct implementingLtix.Deployableor a deployment ID. Token requests include it.:req_options(keyword/0) - Options passed through toReq.request/2. The default value is[].:local_network_access(nil|:none|:local|:loopback) - Addresses platform requests may connect to besides public ones.:localadds private network addresses, and:loopbackadds those and this machine. Defaults toconfig :ltix, :local_network_access, then:none.:storage_adapter(module/0) - Module implementingLtix.StorageAdapter, which signs token requests. Defaults to the application configuration.:storage_adapter_options(keyword/0) - Options passed to the storage adapter's signing callbacks. The default value is[].:require_all_scopes(boolean/0) - Return aScopeMismatcherror unless every scope the endpoints need was granted. The default value isfalse.
@spec from_access_token!(Ltix.OAuth.AccessToken.t(), keyword()) :: t()
Same as from_access_token/2 but raises on error.
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
@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.
Same as refresh/1 but raises on error.
@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"}]
)
@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)
@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.
@spec require_scope(t(), String.t()) :: :ok | {:error, Exception.t()}
Require a specific scope, returning an error if not granted.
@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
@spec with_endpoints(t(), %{required(module()) => 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(boolean/0) - Return aScopeMismatcherror unless every scope the endpoints need was granted. The default value isfalse.
Same as with_endpoints/3 but raises on error.