Rails で LTI 認証を実装する

# LTI 認証とは

LTI認証という規格があります。
LTIとはLearning Tools Interoperability という単語の頭文字で、要するに学習支援ツール間で認証情報をやり取りする際の規格という意味です。

認証技術の中でも分野が特定されているかなりニッチな技術なので、知らない方も多いんではないでしょうか。僕も教育工学を専門にする前はLTIという単語すら知りませんでした。

以下にIMS GLOBAL から公式に出されているLTI認証の説明図をのっけて置きます。

![](https://www.imsglobal.org/sites/default/files/LTI%20Advantage%20UPDATED%20Graphic%20Sept%208%202020_0.svg)

個人的には、結局OAuthの一種と理解しているのですが、我々が開発するアプリケーションを Learning Tools, 認証の基盤となる大本のツールを Learning Platform として、Learning Tools は Learning Platform の拡張機能を提供する代わりに、認証情報を Learning Platform からもらいます。これによってユーザーは様々なアプリをユーザーを切り替えることなしに、使い分けることができるという利点があるわけです。このとき、Learning Toolsのほうを LTI Tool Provider, Learning Platformのほうを LTI Consumer と呼んだりもします。

ただ、問題はこれをどう実装するかです。実際、ニッチな技術ゆえにドキュメンテーションに乏しく、ネット上を探しても実装例はあまり公開されていません。そこで今回、この記事では Ruby on Rails を使った LTI 認証の実際について紹介したいと思います。

# 実装例

### ライブラリのインストール

まずは、必要なライブラリをインストールします。

```
$ bundle add devise
$ bundle add ims-lti
$ bundle add oauth
```

Rails を使うメリットは lti 認証のためのライブラリがあるところです。今回は `ims-lti` を使って実装します。

### 認証キーの登録

`config`配下に`lti_settings.yml` ファイルを作成します。
内容は以下の内容を記載してください（コンシューマーキー、LTIシークレットは適宜安全な文字列を使用してください）。

```:config/lti_settings.yml
production:
  __consumer_key__: '__lti_secret__'

development:
  __consumer_key__: '__lti_secret__'
```

作成したら、これを読み込むための設定を`config/application.rb`に追加します。

```:config/application.rb
config.lti_settings = Rails.application.config_for(:lti_settings)
```

### コントローラーの作成

`ltis_controller`を作って以下の内容を記載しましょう

```rb

require 'oauth/request_proxy/action_controller_request'

class LtisController < ApplicationController
  skip_before_action :verify_authenticity_token, only: :launch

  def lti_launch
    # 受け取った consumer key が config/lti_settings.yml の中にあるかどうかを確認
    if not Rails.configuration.lti_settings[params[:oauth_consumer_key]]
      render :launch_error, status: 401
      return
    end 
    shared_secret = Rails.configuration.lti_settings[params[:oauth_consumer_key]]
    authenticator = IMS::LTI::Services::MessageAuthenticator.new(request.url, request.request_parameters, shared_secret)
    
    #Check if the signature is valid
    if not authenticator.valid_signature?
      render :launch_error, status: 401
      return
    end
    #check if the message is too old
    if DateTime.strptime(request.request_parameters['oauth_timestamp'],'%s') < 5.minutes.ago
      render :launch_error, status: 401
      return
    end
    
    # LTI 情報をsessionに保存
    session_data = {
      "fullname" => authenticator&.message&.lis_person_name_full,
      "email" => authenticator&.message&.lis_person_contact_email_primary,
      "user_id" => authenticator&.message&.user_id,
      "context_id" => authenticator&.message&.context_id,
      "context_title" => authenticator&.message&.context_title,
      "tool_consumer_instance_name" => authenticator&.message&.tool_consumer_instance_name
    }
    print(session_data)
    session['lti-authenticator'] = session_data
    sign_in_and_redirect(User.first)
  end
end
```

`MessageAuthenticator` にわたってきたリクエストと事前にこちらで保持しているキーの情報を渡すことで簡単に認証ができます。認証が成功したら、`devise` ライブラリを使ってsign_inすることで、アプリ側での認証を実装しています。

最後に`lti_launch`を起動するためのエンドポイントを定義しておきましょう。

```:config/routes.rb
  match 'lti/launch' => 'ltis#lti_launch', via: [:get, :post], as: :lti_launch
```

### Learning Platform 側での設定

ここまでできたら、あとは Learning Platform 側で事前に `lti_settings.yml` に記載した認証情報を書いて指定したエンドポイントに遷移するように設定すれば完成です。このやり方は個々のLearning Platformによって異なるので割愛します。

### （補足）LTI 遷移時に Rails で cross origin error が出たら

`config/application.rb` に以下の内容を追記してください

```:config/application.rb
    config.lti_settings = Rails.application.config_for(:lti_settings)
    config.action_dispatch.default_headers['Referrer-Policy'] = 'unsafe-url'
    config.action_controller.forgery_protection_origin_check = false
    config.action_controller.allow_forgery_protection = false
```

### 参考文献

[Rails LTI Tool Provider](http://splice.cs.vt.edu/lti/tutorials/rails)
[IMS LTI](https://github.com/instructure/ims-lti)