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 registration

  • Ltix.StorageAdapter - behaviour your app implements to look up registrations, deployments, and manage nonces

  • Ltix.LaunchContext - the validated output of a successful launch, containing the parsed claims, registration, and deployment

  • Ltix.LaunchClaims - structured data parsed from the ID Token (roles, context, resource link, and more)

Configuration

config :ltix,
  storage_adapter: MyApp.LtiStorage

handle_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 implementing Ltix.StorageAdapter. Looked up at runtime, so it works with releases.

Optional

  • :allow_anonymous - when true, allow launches without a sub claim in the ID Token. Defaults to false.

  • :max_token_age - seconds after the platform issued the ID Token that the launch is still accepted, or :infinity to accept any age. Defaults to 3600.

  • :local_network_access - the addresses platform requests may connect to besides public ones. :local adds private network addresses, and :loopback adds 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_options need :loopback, because Ltix can't check the addresses they connect to.

  • :json_library - JSON encoder/decoder module. Detected at compile time, using JSON (Elixir 1.18+/OTP 27+) if available, then Jason. Only set this if you need a different library. With JSON or Jason, a token that repeats a claim has the last value. With another library, it has the value that library's decode/1 returns.

  • :req_options - default options passed to Req.request/2 for 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 the Ltix.JWT.KeySet to get platform public keys from. Defaults to Ltix.JWT.KeySet. The key set must be started in your supervision tree.

  • :default_key_size - RSA key size in bits for Ltix.JWK.generate/1 when no :key_size is given. Defaults to 4096.

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)
end

In 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
end

Advantage 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)
end

See 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

handle_callback(params, state, opts \\ [])

@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 implementing Ltix.StorageAdapter (defaults to application config)
  • :storage_adapter_options - keyword list passed to each storage adapter callback (defaults to [])
  • :allow_anonymous - allow launches without a sub claim (defaults to application config, then false)
  • :req_options - options passed to the HTTP client for JWKS fetching
  • :key_set - name of the Ltix.JWT.KeySet to get platform public keys from (defaults to application config, then Ltix.JWT.KeySet)
  • :claim_parsers - custom claim parsers (see Ltix.LaunchClaims.from_json/2)
  • :role_parsers - custom role parsers (see Ltix.LaunchClaims.Role.parse/2)
  • :clock_skew - seconds of tolerance for the token's exp, iat, and nbf (defaults to 5)
  • :max_token_age - seconds after the platform issued the ID Token that the launch is still accepted, or :infinity to accept any age (defaults to application config, then 3600)
  • :local_network_access - :local or :loopback to fetch the platform's keys from a private network address or this machine (defaults to application config, then :none)

handle_callback!(params, state, opts \\ [])

@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)

handle_login(params, redirect_uri, opts \\ [])

@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 implementing Ltix.StorageAdapter (defaults to application config)
  • :storage_adapter_options - keyword list passed to each storage adapter callback (defaults to [])

handle_login!(params, redirect_uri, opts \\ [])

@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)