Ltix handles the LTI 1.3 OIDC launch flow for tool applications. It is built around 4 main components.
Ltix.Registration- what the tool stores about a platform (issuer, client_id, endpoints). Created during out-of-band registrationLtix.StorageAdapter- behaviour your app implements to look up registrations, deployments, and manage noncesLtix.LaunchContext- the validated output of a successful launch, containing the parsed claims, registration, and deploymentLtix.LaunchClaims- structured data parsed from the ID Token (roles, context, resource link, and more)
Configuration
config :ltix,
storage_adapter: MyApp.LtiStoragehandle_login/3 takes :storage_adapter and :storage_adapter_options,
and handle_callback/3 takes both of those, :allow_anonymous, :max_token_age,
:local_network_access, :req_options, and :key_set, to override the
setting for a single call. Unknown or invalid options raise
Ltix.Errors.Invalid.InvalidOption.
Required
:storage_adapter- module implementingLtix.StorageAdapter. Looked up at runtime, so it works with releases.
Optional
:allow_anonymous- whentrue, allow launches without asubclaim in the ID Token. Defaults tofalse.:max_token_age- seconds after the platform issued the ID Token that the launch is still accepted, or:infinityto accept any age. Defaults to3600.:local_network_access- the addresses platform requests may connect to besides public ones.:localadds private network addresses, and:loopbackadds those and this machine. Defaults to:none. The functions that send platform requests take the same option. The:finch,:adapter,:unix_socket, and proxy options in:req_optionsneed:loopback, because Ltix can't check the addresses they connect to.:json_library- JSON encoder/decoder module. Detected at compile time, usingJSON(Elixir 1.18+/OTP 27+) if available, thenJason. Only set this if you need a different library. WithJSONorJason, a token that repeats a claim has the last value. With another library, it has the value that library'sdecode/1returns.:req_options- default options passed toReq.request/2for all HTTP calls (JWKS fetching, OAuth token requests, service calls). Useful for setting timeouts, middleware, or test adapters.config :ltix, req_options: [receive_timeout: 10_000]:key_set- name of theLtix.JWT.KeySetto get platform public keys from. Defaults toLtix.JWT.KeySet. The key set must be started in your supervision tree.:default_key_size- RSA key size in bits forLtix.JWK.generate/1when no:key_sizeis given. Defaults to4096.
Launch claim parsers
Custom claim and role parsers are configured under the
Ltix.LaunchClaims key.
config :ltix, Ltix.LaunchClaims,
claim_parsers: %{
"https://example.com/custom" => MyApp.CustomClaimParser
},
role_parsers: %{
"https://example.com/roles/" => MyApp.CustomRoleParser
}See Custom Claim Parsers and Custom Role Parsers for details.
Handling Launches
The LTI launch flow requires two endpoints. In your login endpoint,
call handle_login/3 with the platform's initiation params and your
launch URL.
def login(conn, params) do
launch_url = url(conn, ~p"/lti/launch")
{:ok, %{redirect_uri: url, state: state}} =
Ltix.handle_login(params, launch_url)
conn
|> put_session(:lti_state, state)
|> redirect(external: url)
endIn your launch endpoint, call handle_callback/3 with the POST
params and the stored state.
def launch(conn, params) do
state = get_session(conn, :lti_state)
{:ok, context} = Ltix.handle_callback(params, state)
# context.claims has the parsed launch data
# context.claims.target_link_uri is where to redirect
# context.claims.roles tells you who the user is
endAdvantage Services
After a successful launch, call platform services like roster queries and grade passback. Authenticate with the platform's token endpoint, then call service functions.
{:ok, client} = Ltix.MembershipsService.authenticate(context)
{:ok, roster} = Ltix.MembershipsService.list_members(client)
Enum.each(roster, fn member ->
IO.puts("#{member.name}: #{inspect(member.roles)}")
end)Post grades back to the platform's gradebook.
{:ok, client} = Ltix.GradeService.authenticate(context)
:ok = Ltix.GradeService.post_score(client, score)See the Advantage Services guide for OAuth details, token lifecycle, and multi-service authentication.
Deep Linking
When a platform sends an LtiDeepLinkingRequest launch, the same
handle_callback/3 returns a %LaunchContext{}. Branch on the
message type and build a response.
{:ok, context} = Ltix.handle_callback(params, state)
case context.claims.message_type do
"LtiDeepLinkingRequest" ->
{:ok, link} = Ltix.DeepLinking.ContentItem.LTIResourceLink.new(
url: "https://tool.example.com/activity/1",
title: "Quiz 1"
)
{:ok, response} = Ltix.DeepLinking.build_response(context, [link])
# POST response.jwt to response.return_url
"LtiResourceLinkRequest" ->
redirect(conn, external: context.claims.target_link_uri)
endSee the Deep Linking guide for content item types, line items, and platform constraints.
Summary
Functions
Handle an authentication response and validate the ID Token.
Same as handle_callback/3, but returns the launch context or raises
the error.
Handle a platform's login initiation and build an authorization redirect.
Same as handle_login/3, but returns the redirect or raises the error.
Functions
@spec handle_callback(params :: map(), state :: String.t(), opts :: keyword()) :: {:ok, Ltix.LaunchContext.t()} | {:error, Exception.t()}
Handle an authentication response and validate the ID Token.
Returns {:ok, %LaunchContext{}} on success with parsed claims, registration,
and deployment. The state parameter should be the value stored in the
session during handle_login/3.
Use context.claims.target_link_uri for the final redirect destination.
Options
:storage_adapter- module implementingLtix.StorageAdapter(defaults to application config):storage_adapter_options- keyword list passed to each storage adapter callback (defaults to[]):allow_anonymous- allow launches without asubclaim (defaults to application config, thenfalse):req_options- options passed to the HTTP client for JWKS fetching:key_set- name of theLtix.JWT.KeySetto get platform public keys from (defaults to application config, thenLtix.JWT.KeySet):claim_parsers- custom claim parsers (seeLtix.LaunchClaims.from_json/2):role_parsers- custom role parsers (seeLtix.LaunchClaims.Role.parse/2):clock_skew- seconds of tolerance for the token'sexp,iat, andnbf(defaults to5):max_token_age- seconds after the platform issued the ID Token that the launch is still accepted, or:infinityto accept any age (defaults to application config, then3600):local_network_access-:localor:loopbackto fetch the platform's keys from a private network address or this machine (defaults to application config, then:none)
@spec handle_callback!(params :: map(), state :: String.t(), opts :: keyword()) :: Ltix.LaunchContext.t()
Same as handle_callback/3, but returns the launch context or raises
the error.
Takes the same options as handle_callback/3.
context = Ltix.handle_callback!(params, state)
@spec handle_login(params :: map(), redirect_uri :: String.t(), opts :: keyword()) :: {:ok, %{redirect_uri: String.t(), state: String.t()}} | {:error, Exception.t()}
Handle a platform's login initiation and build an authorization redirect.
The redirect_uri is the tool's launch URL where the platform will
POST the authentication response.
Returns {:ok, %{redirect_uri: url, state: state}} on success. Store
state in the user's session for CSRF verification, then redirect the
user agent to redirect_uri.
Options
:storage_adapter- module implementingLtix.StorageAdapter(defaults to application config):storage_adapter_options- keyword list passed to each storage adapter callback (defaults to[])
@spec handle_login!(params :: map(), redirect_uri :: String.t(), opts :: keyword()) :: %{ redirect_uri: String.t(), state: String.t() }
Same as handle_login/3, but returns the redirect or raises the error.
Takes the same options as handle_login/3.
%{redirect_uri: url, state: state} = Ltix.handle_login!(params, launch_url)