Custom Claim Parsers

Copy Markdown View Source

Ltix parses standard OIDC and LTI claims from the launch JWT automatically. Unusable optional metadata becomes nil, while valid fields remain available. Identity and required launch fields remain validated. Any unrecognized claims are put in the extensions map as raw values.

context.claims.extensions
#=> %{"https://myplatform.example.com/claim/analytics" => %{"session_id" => "abc123"}}

Register custom claim parsers to transform these raw values into structured data.

Writing a claim parser

A claim parser is a module implementing the Ltix.LaunchClaims.Parser behavior. Its parse/1 callback takes the raw claim value and returns {:ok, parsed} or {:error, reason}.

defmodule MyApp.Lti.AnalyticsClaim do
  use Ltix.LaunchClaims.Parser

  defstruct [:session_id, :tracking_enabled]

  @impl Ltix.LaunchClaims.Parser
  def parse(%{"session_id" => session_id} = raw) do
    {:ok,
     %__MODULE__{
       session_id: session_id,
       tracking_enabled: Map.get(raw, "tracking_enabled", false)
     }}
  end

  def parse(_), do: {:error, "missing session_id in analytics claim"}
end

A 1-arity function with the same contract works too, such as fn raw -> {:ok, raw} end.

Registering parsers

Claim parsers are registered as a map of claim key to parser module or function. The key is the full claim name as it appears in the JWT (usually a namespaced URI for extension claims).

Via application config (recommended for parsers that apply globally)

# config/config.exs
config :ltix, Ltix.LaunchClaims,
  claim_parsers: %{
    "https://myplatform.example.com/claim/analytics" => MyApp.Lti.AnalyticsClaim
  }

Via handle_callback/3 (for per-call control)

Ltix.handle_callback(params, state,
  claim_parsers: %{
    "https://myplatform.example.com/claim/analytics" => MyApp.Lti.AnalyticsClaim
  }
)

Via LaunchClaims.from_json/2 (for direct parsing)

Ltix.LaunchClaims.from_json(claims,
  claim_parsers: %{
    "https://myplatform.example.com/claim/analytics" => MyApp.Lti.AnalyticsClaim
  }
)

Per-call parsers merge with application config, with per-call taking priority for overlapping keys.

To replace a standard parser, register its claim key. The parsed value appears in the corresponding LaunchClaims field.

Accessing parsed claims

Parsed extension claims remain in the extensions map, and their values are your parsed values.

{:ok, context} = Ltix.handle_callback(params, state)

context.claims.extensions["https://myplatform.example.com/claim/analytics"]
#=> %MyApp.Lti.AnalyticsClaim{session_id: "abc123", tracking_enabled: true}

Missing claims

A parser runs only for claims present in the token. An absent extension claim is left out of extensions, and an absent standard claim leaves its LaunchClaims field nil. A claim sent as JSON null is passed to the parser as nil.

Error handling

When a parser returns {:error, reason}, the launch fails with Ltix.Errors.Invalid.InvalidClaim, whose message is the reason (a string as given, an exception's message, or any other term as inspect/1 shows it). A reason that is already an Ltix error is returned unchanged.

{:error, %Ltix.Errors.Invalid.InvalidClaim{} = error} = Ltix.handle_callback(params, state)

error.claim
#=> "https://myplatform.example.com/claim/analytics"

error.message
#=> "missing session_id in analytics claim"

Registering multiple parsers

You can register claim parsers and role parsers together in application config.

config :ltix, Ltix.LaunchClaims,
  role_parsers: %{
    "https://myplatform.example.com/roles/" => MyApp.Lti.PlatformRoleParser
  },
  claim_parsers: %{
    "https://myplatform.example.com/claim/analytics" => MyApp.Lti.AnalyticsClaim,
    "https://myplatform.example.com/claim/proctoring" => MyApp.Lti.ProctoringClaim
  }

Next steps