Ltix.StorageAdapter behaviour (Ltix v0.2.0)

Copy Markdown View Source

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

Ltix stores nothing between login and launch, so pass the same options to Ltix.handle_callback/3.

Callback summary

CallbackCalled duringPurpose
get_registration/3Login initiation and launch validationLook up a platform by issuer and optional client_id
get_deployment/3Launch validationLook up a deployment by registration and deployment_id
validate_nonce/4Launch validationRecord a nonce as used, and reject one already used
key_id/2Service authentication and deep linking responsesReturn the ID of the key that signs for a registration
sign/4Service authentication and deep linking responsesSign 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
end

Failures

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

Look up a deployment by registration and deployment_id.

Look up a platform registration by issuer and client_id.

Return the ID of the key that signs for a registration.

Sign a message with the key key_id/2 returned.

Record a nonce from a verified ID Token as used.

Callbacks

get_deployment(registration, deployment_id, opts)

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

get_registration(issuer, client_id, opts)

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

key_id(registration, opts)

(optional)
@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.

sign(registration, key_id, message, opts)

(optional)
@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.

validate_nonce(nonce, registration, expires_at, opts)

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