Defines callbacks for looking up registrations, deployments, and managing nonces.
Your application implements this behaviour to connect Ltix to your persistence layer. See the Storage Adapters guide for a complete Ecto-backed example.
Returning your own structs
get_registration/3 and get_deployment/3 can return any struct that
implements Ltix.Registerable or Ltix.Deployable. Your struct is
passed through to the Ltix.LaunchContext after a successful launch,
so you can access your own fields (database IDs, tenant info, etc.)
without an extra query.
Every other callback receives the registration the same way you provided
it. Launch callbacks get the struct get_registration/3 returned, and
the signing callbacks get the struct you passed to the function that
needed a signature.
Options
Every callback receives a keyword list of options as its last argument.
It is the list you pass as :storage_adapter_options to the function
that calls your adapter, such as Ltix.handle_login/3, and [] when you
pass none. Use it for values your lookups need, such as a tenant.
Ltix.handle_login(params, launch_url, storage_adapter_options: [tenant: tenant])
@impl true
def get_registration(issuer, client_id, opts) do
MyApp.Lti.get_registration(opts[:tenant], issuer, client_id)
endLtix stores nothing between login and launch, so pass the same options to
Ltix.handle_callback/3.
Callback summary
| Callback | Called during | Purpose |
|---|---|---|
get_registration/3 | Login initiation and launch validation | Look up a platform by issuer and optional client_id |
get_deployment/3 | Launch validation | Look up a deployment by registration and deployment_id |
validate_nonce/4 | Launch validation | Record a nonce as used, and reject one already used |
key_id/2 | Service authentication and deep linking responses | Return the ID of the key that signs for a registration |
sign/4 | Service authentication and deep linking responses | Sign a message with that key |
Signing
key_id/2 and sign/4 are optional. Implement them when your tool
calls Advantage services or builds deep linking responses. Ltix builds
each JWT and calls your adapter for the signature, so the private key
stays wherever you keep it. Ltix.JWK.sign/2 signs with a key held in
your application.
@impl true
def key_id(registration, _opts) do
{:ok, MyApp.Lti.signing_key(registration).kid}
end
@impl true
def sign(_registration, key_id, message, _opts) do
case MyApp.Lti.get_key(key_id) do
nil -> {:error, :unknown_key}
jwk -> Ltix.JWK.sign(jwk, message)
end
endFailures
Return {:error, reason} when storage fails. The login or launch then
returns Ltix.Errors.Unknown.StorageError with that value in result. Ltix
errors are returned unchanged. A callback that returns any other
unexpected value, or a record that does not implement Ltix.Registerable or
Ltix.Deployable, also produces a StorageError.
Summary
Callbacks
@callback get_deployment( registration :: Ltix.Registerable.t(), deployment_id :: String.t(), opts :: keyword() ) :: {:ok, Ltix.Deployable.t()} | {:error, :not_found | term()}
Look up a deployment by registration and deployment_id.
The deployment_id is case-sensitive and assigned by the platform.
Return any struct that implements Ltix.Deployable. The library
validates it via the protocol and preserves your original struct
in the Ltix.LaunchContext. A deployment with a different
deployment_id fails the launch with Ltix.Errors.Unknown.StorageError.
Return {:error, :not_found} when no deployment matches, and the launch
returns Ltix.Errors.Invalid.DeploymentNotFound.
@callback get_registration( issuer :: String.t(), client_id :: String.t() | nil, opts :: keyword() ) :: {:ok, Ltix.Registerable.t()} | {:error, :not_found | term()}
Look up a platform registration by issuer and client_id.
Called during login initiation with the login request's issuer and
client_id, and again during launch validation with the ID Token's issuer
and audience. The client_id may be nil when the platform doesn't
include it in the login request. Your adapter must handle both cases.
When the platform sends a deployment ID, opts[:deployment_id] has
it. It comes from the login request's lti_deployment_id during login
initiation and from the ID Token during launch validation. Ltix has not
verified it at either point, and it replaces a :deployment_id you pass
in :storage_adapter_options.
Return any struct that implements Ltix.Registerable. The library
extracts the Ltix.Registration it needs via the protocol and
preserves your original struct in the Ltix.LaunchContext.
Return {:error, :not_found} when no registration matches, and the login
or launch returns Ltix.Errors.Invalid.RegistrationNotFound.
@callback key_id(registration :: Ltix.Registerable.t(), opts :: keyword()) :: {:ok, String.t()} | {:error, term()}
Return the ID of the key that signs for a registration.
The ID becomes the kid of each JWT Ltix builds for the registration, and
platforms use it to find the public key in your key set. Ltix passes it
back to sign/4.
@callback sign( registration :: Ltix.Registerable.t(), key_id :: String.t(), message :: binary(), opts :: keyword() ) :: {:ok, binary()} | {:error, term()}
Sign a message with the key key_id/2 returned.
Return {:ok, signature} with the RSASSA-PKCS1-v1_5 SHA-256 (RS256)
signature of message, as raw bytes. Ltix.JWK.sign/2 returns this for a
key held in your application.
Sign with the key whose ID is key_id, and return {:error, reason} for a key
ID you can't sign with.
@callback validate_nonce( nonce :: String.t(), registration :: Ltix.Registerable.t(), expires_at :: DateTime.t(), opts :: keyword() ) :: :ok | {:error, :nonce_already_used | term()}
Record a nonce from a verified ID Token as used.
Called during launch validation, after the token's signature and claims
are verified. Return :ok the first time a nonce is recorded, and
{:error, :nonce_already_used} every time after that. The launch then
returns Ltix.Errors.Security.NonceReused.
Recording has to be atomic. When two launches with the same nonce arrive
at once, only one of them gets :ok.
Keep each record until expires_at. Ltix rejects the token after that
time, so you can delete the record then.