# `Ltix.StorageAdapter`
[🔗](https://github.com/DecoyLex/ltix/blob/main/lib/ltix/storage_adapter.ex#L1)

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](storage-adapters.md) guide
for a complete Ecto-backed example.

## Returning your own structs

`c:get_registration/3` and `c: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 `c: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

| Callback | Called during | Purpose |
|---|---|---|
| `c:get_registration/3` | Login initiation and launch validation | Look up a platform by issuer and optional client_id |
| `c:get_deployment/3` | Launch validation | Look up a deployment by registration and deployment_id |
| `c:validate_nonce/4` | Launch validation | Record a nonce as used, and reject one already used |
| `c:key_id/2` | Service authentication and deep linking responses | Return the ID of the key that signs for a registration |
| `c:sign/4` | Service authentication and deep linking responses | Sign a message with that key |

## Signing

`c:key_id/2` and `c: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`.

# `get_deployment`

```elixir
@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`

```elixir
@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`
*optional* 

```elixir
@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 `c:sign/4`.

# `sign`
*optional* 

```elixir
@callback sign(
  registration :: Ltix.Registerable.t(),
  key_id :: String.t(),
  message :: binary(),
  opts :: keyword()
) :: {:ok, binary()} | {:error, term()}
```

Sign a message with the key `c: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`

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
