README.md

# Lti 1p3

[![Hex.pm](https://img.shields.io/hexpm/v/lti_1p3)](https://hex.pm/packages/lti_1p3)
[![GitHub](https://img.shields.io/github/license/Simon-Initiative/lti_1p3?color=blue)](https://github.com/Simon-Initiative/lti_1p3/blob/master/LICENSE)
[![Build & Test](https://github.com/Simon-Initiative/lti_1p3/actions/workflows/main.yml/badge.svg)](https://github.com/Simon-Initiative/lti_1p3/actions/workflows/main.yml)
[![Coverage Status](https://coveralls.io/repos/github/Simon-Initiative/lti_1p3/badge.svg?branch=master)](https://coveralls.io/github/Simon-Initiative/lti_1p3?branch=master)

An Elixir library for LTI 1.3 Platforms and Tools.

This library implements the [Learning Tools Interoperability (LTI) 1.3 Specification](http://www.imsglobal.org/spec/lti/v1p3/) for Tool and Platform integrations in Elixir. You can use this library to develop an LTI 1.3 Tool or Platform (or both). The data persistence layer is "pluggable" and can be configured according to the [Data Providers](#data-providers) section below.

## Installation

The package can be installed by adding `lti_1p3` to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:lti_1p3, "~> 0.1.0"}
  ]
end
```

Documentation can be found at [https://hexdocs.pm/lti_1p3](https://hexdocs.pm/lti_1p3).

## Getting Started

### Config

Add the following to `config/config.exs`:
```elixir
use Mix.Config

# ... existing config

config :lti_1p3,
  provider: Lti_1p3.DataProviders.MemoryProvider

# ... import_config

```

The provider configured here is the default in-memory persistence provider which means any registrations or deployments created will be lost when your app is stopped or restarted. To persist data across restarts you will need to specify a durable provider such as the [EctoProvider](https://github.com/Simon-Initiative/lti_1p3_ecto_provider) or implement a custom data provider using the DataProvider behavior. Refer to the [Data Providers](#data-providers) section below for more details.

### Jwk

Whether you are planning on implementing a tool or a platform, you must create and expose a public Jwk regardless. This Jwk should be available from an endpoint to be used by the other party for verification of tokens which were signed using the private key counterpart. Note that both tool and platform will have their own separate and distinct Jwks, however if your app happens to be both a tool and platform you can simply reuse the same Jwk for both. **NEVER** expose or share the private key which is generated by this function. The security of the LTI handshake depends on it's secrecy.

```elixir
# Create an active Jwk. Typically this is done once at startup, in a database seed script
# or when keys are rotated by the tool. This will be reused across registration creations
%{private_key: private_key} = Lti_1p3.KeyGenerator.generate_key_pair()
{:ok, jwk} = Lti_1p3.create_jwk(%Lti_1p3.Jwk{
  pem: private_key,
  typ: "JWT",
  alg: "RS256",
  kid: "some-unique-kid",
  active: true,
})
```

Create an endpoint to expose all public Jwks. For example, using a Phoenix controller:

```elixir
defmodule MyAppWeb.LtiController do
  use MyAppWeb, :controller

  ...

  def jwks(conn, _params) do
    keys = Lti_1p3.get_all_public_keys()

    conn
    |> json(keys)
  end

  ...

end
```

If you are using Phoenix, don't forget to add the endpoint to your `router.ex`:

```elixir
    get "/.well-known/jwks.json", LtiController, :jwks
```

These keys can be considered site-wide metadata and as such can reside in the `.well-known` path per [RFC 5785](https://tools.ietf.org/html/rfc5785).

### LTI 1.3 Tool Example

If you are unfamiliar with LTI 1.3, please refer to the the [LTI 1.3 Launch Overview](./docs/lti_1p3_overview.md).

Before a launch can be performed, a platform must be registered with your tool by creating a **Jwk**, **Registration** and **Deployment**. A Registration represents the details provided by a platform administrator. For the simplest case, if your tool only needs to integrate with a single platform this can be hard coded in at startup or in a simple database seed script. For the more common case, if your tool needs to support multiple runtime-configurable platform integrations, this registration process will most likely be implemented in something more akin to a web form, such as using a Phoenix controller.

```elixir
# this jwk is the same jwk we generated in the section above
{:ok, jwk} = Lti_1p3.get_active_jwk()

# Create a Registration, Details are typically provided by the platform administrator for this registration.
{:ok, registration} = Lti_1p3.Tool.create_registration(%Lti_1p3.Tool.Registration{
  issuer: "https://platform.example.edu",
  client_id: "1000000000001",
  key_set_url: "https://platform.example.edu/.well-known/jwks.json",
  auth_token_url: "https://platform.example.edu/access_tokens",
  auth_login_url: "https://platform.example.edu/authorize_redirect",
  auth_server: "https://platform.example.edu",
  tool_jwk_id: jwk.id,
})

# Create a Deployment. Essentially this a unique identifier for a specific registration launch point,
# for which there can be many for a single registration. This will also typically be provided by a
# platform administrator.
{:ok, _deployment} = Lti_1p3.Tool.create_deployment(%Lti_1p3.Tool.Deployment{
  deployment_id: "some-deployment-id",
  registration_id: registration.id,
})

```

Your tool implementation will need to have 2 tool-specific endpoints for handling LTI requests. The first will be a `login` endpoint, which will issue a login request back to the platform. The second will be a `launch` endpoint, which will validate the lti launch details and if successful, cache the LTI params from the request and display the resource. The details of both of these steps is outlined in the [LTI 1.3 Launch Overview](./docs/lti_1p3_overview.md). You will need to provide both of these endpoint urls to the platform as part of their registration process for your tool.

The first endpoint, `login`, uses the `Lti_1p3.Tool.OidcLogin` module to validate the request and return a state key and redirect_uri. For example:

```elixir
defmodule MyAppWeb.LtiController do
  use MyAppWeb, :controller

  def login(conn, params) do
    case Lti_1p3.OidcLogin.oidc_login_redirect_url(params) do
      {:ok, state, redirect_url} ->
        conn
        |> put_session("state", state)
        |> redirect(external: redirect_url)

      {:error, %{reason: :invalid_registration, msg: _msg, issuer: issuer, client_id: client_id}} ->
        handle_invalid_registration(conn, issuer, client_id)

      {:error, %{reason: _reason, msg: msg}} ->
        render(conn, "lti_error.html", msg: msg)
    end
  end

  ...

end
```

Notice how the returned state is stored in the session so that it can be used later in the launch request. The user is then redirected to the returned redirect_url. In the case where an error is returned, a map with the reason code, error message, and any additional data associated with the specific error is returned and can be handled accordingly.

The second endpoint, `launch`, uses the `Lti_1p3.Tool.LaunchValidation` module to validate the launch and cache the lti params. For example:

```elixir
defmodule MyAppWeb.LtiController do
  use MyAppWeb, :controller

  ...

  def launch(conn, params) do
    session_state = Plug.Conn.get_session(conn, "state")
    case Lti_1p3.Tool.LaunchValidation.validate(params, session_state) do
      {:ok, lti_params, key} ->
        # store key in the session so that the cached lti_params can be retrieved in later requests
        conn = conn
          |> Plug.Conn.put_session(:lti_1p3_key, key)

        handle_valid_lti_1p3_launch(conn, lti_params)

      {:error, %{reason: :invalid_registration, msg: _msg, issuer: issuer, client_id: client_id}} ->
        handle_invalid_registration(conn, issuer, client_id)

      {:error, %{reason: :invalid_deployment, msg: _msg, registration_id: registration_id, deployment_id: deployment_id}} ->
        handle_invalid_deployment(conn, registration_id, deployment_id)

      {:error, %{reason: _reason, msg: msg}} ->
        render(conn, "lti_error.html", reason: msg)
    end
  end

  ...

end
```

If successful, `validate` returns the LTI params from the request as well as a `key` for the cached lti params, which can be used to retrieve the the LTI params associated with the user's latest launch by using the `Lti_1p3.Tool` module. This key is guaranteed to be unique to the platform, user, and context_id and reproducible given the same values. This means when a different set of LTI params for the same platform, user, and context_id are received, the generated key will be identical and the corresponding cached params will be updated to the new values.

Note, this example of storing the `lti_1p3_key` assumes only a single platform will use the tool. If you expect more that one platform to use your tool
concurrently, you may want to build out a more rich structure of storing these lti param keys in the session, such as including the full universal scope consisting of a platform identifier, user identifier and context identifier all in the session to guarantee the correct lti params can be loaded for a particular platform, user, and context.

```elixir
%Lti_1p3.Tool.LtiParams{params: lti_params} = Lti_1p3.Tool.get_lti_params_by_sub(sub)
```

If you are using Phoenix, don't forget to add these endpoints to your `router.ex`. The LTI 1.3 specification says the `login` request can be sent as either a `GET` or `POST`, so we must support both methods.

```elixir
    post "/login", LtiController, :login
    get "/login", LtiController, :login
    post "/launch", LtiController, :launch
```

> **Additional Note:**
> As modern browsers continue to limit the ability of iFrames to set cookies from within a page from another domain (which is typically how an LTI resource is displayed on a platform by default) it becomes more unreliable to use cookie-based session storage for things like `state` and `lti_1p3_sub` key. If you run into issues related to session data not being stored consistently across requests, please verify that the cookie is actually being set in the browser and also try initiating the launch into a new tab instead of in an iframe.

### LTI 1.3 Platform Example

If you are unfamiliar with LTI 1.3, please refer to the the [LTI 1.3 Launch Overview](./docs/lti_1p3_overview.md).

Before your platform can initiate a launch request, you must first create a **Platform Instance** with the details provided by the tool publisher or developer. A platform instance represents an integration with a specific tool and can be created using the `Lti_1p3.Platform` module. Typically a platform will provide some sort of web form to create these for every tool the platform will launch into.

```elixir
{:ok, platform_instance} = Lti_1p3.Platform.create_platform_instance(%PlatformInstance{
  name: "Some Example Tool",
  target_link_uri: "https://tool.example.edu/launch",
  client_id: "1000000000001",
  login_url: "https://tool.example.edu/login",
  keyset_url: "https://tool.example.edu/.well-known/jwks.json",
  redirect_uris: "https://tool.example.edu/launch",
})
```

The choice of client_id here is somewhat arbitrary and can simply be an incrementing integer or guid-based. The only constraint is that it must be unique. This client_id will be provided to the tool as part of it's configuration details.

Your platform implementation will need to have an `authorize_redirect` endpoint for handling platform-specific LTI requests which will verify the current user logged in is the same user who initiated the request using the login_hint and then use the `Lti_1p3.AuthorizationRedirect` module to authorize the LTI details by verifying the LTI details provided by the tool and if successful, render a form that will post the final LTI request and params to the tool. For example:

```elixir
defmodule MyAppWeb.LtiController do
  use MyAppWeb, :controller

  ...

  def authorize_redirect(conn, params) do
    issuer = "https://platform.example.edu"
    deployment_id = "some-deployment-id"

    # current user can be any map or struct that has an id: %{id: user_id}
    current_user = conn.assigns[:current_user]

    case Lti_1p3.AuthorizationRedirect.authorize_redirect(params, current_user, issuer, deployment_id) do
      {:ok, redirect_uri, state, id_token} ->
        conn
        |> render("post_redirect.html", redirect_uri: redirect_uri, state: state, id_token: id_token)
      
      {:error, %{reason: _reason, msg: msg}} ->
          render(conn, "lti_error.html", reason: msg)
    end
  end
end
```

Example of `post_redirect.html`, a self-submitting POST form with state and id_token containing the final LTI params:
```html
<!doctype html>
<html lang="en">
    <head>
        <title>You are being redirected...</title>
    </head>
    <body>
      <div>
          You are being redirected...
      </div>
        <form name="post_redirect" action="<%= @redirect_uri %>" method="post">
            <input type="hidden" name="state" value="<%= @state %>">
            <input type="hidden" name="id_token" value="<%= @id_token %>">

            <noscript>
              <input type="submit" value="Click here to continue">
            </noscript>
        </form>

        <script type="text/javascript">
          window.onload=function(){
            document.getElementsByName('post_redirect')[0].style.display = 'none';
            document.forms["post_redirect"].submit();
          }
        </script>
    </body>
</html>
```

## Data Providers

Data providers are implementations of the `DataProvider` behavior which provide data persistance for the library. In most cases, the non-durable MemoryProvider or persistent [EctoProvider](https://github.com/Simon-Initiative/lti_1p3_ecto_provider) will be sufficient.

### Existing Data Providers

| Name        | Module    | Description                 |
| ------------|-----------|-----------------------------|
| Memory Provider (Default) | `Lti_1p3.DataProviders.MemoryProvider` | An Elixir agent-based, non-durable in-memory store |
| Ecto Provider | `Lti_1p3.DataProviders.EctoProvider` | An Ecto-based, persistent store (External Dependency: https://github.com/Simon-Initiative/lti_1p3_ecto_provider) |

To use a specific data provider, simply install the provider dependency and set the module you would like to use as your `provider` in `config.config.ex`.

```elixir
use Mix.Config

# ... existing config

config :lti_1p3,
  provider: Lti_1p3.DataProviders.EctoProvider

# ... import_config
```


### Custom Data Provider

Depending on your persistence setup, you may want to implement your own custom data provider using the `DataProvider` behavior which can also be set in `config/config.ex`.

```elixir
use Mix.Config

# ... existing config

config :lti_1p3,
  provider: MyApp.DataProviders.CustomProvider

# ... import_config
```
## Full LTI 1.3 Implementation Example

This library was built for the purposes of supporting the Open Learning Initiative's next generation learning platform, [Torus](https://github.com/Simon-Initiative/oli-torus). For a complete implementation of all the concepts discussed here and usage of this library, take a look at the open source repository on Github, specifically [lti_controller.ex](https://github.com/Simon-Initiative/oli-torus/blob/master/lib/oli_web/controllers/lti_controller.ex).