
Canvas、Moodle、Blackboard のような LMS(学習管理システム)向けの連携を作ろうとしたことがあるなら、その痛みを知っているはずだ。具体的には、**LTI 1.3(Learning Tools Interoperability)**の痛みだ。
OIDC ログイン、JWKS キーセット、RSA 署名、秘密鍵の複雑なダンスが絡んでくる。安全であることを意図した仕組みだが、「Canvas に Web アプリを埋め込みたい」だけの開発者にとっては、悪夢のように感じられることがある。
これが私が行き着いたアーキテクチャだ。セキュリティ層には Node.js、フロントエンドには Next.js を使い、1つのモノリスとしてではなく、意図的に分割している。
🔗 GitHub リポジトリ: github.com/paurushrai/lti-1.3-node-nextjs-template
🧐 LTI 1.3 とは何で、なぜ気にすべきなのか
LTI(Learning Tools Interoperability)は、外部ツール(あなたのアプリのような)が LMS(Canvas のような)と通信できるようにする標準規格だ。
かつて LTI 1.1 は簡単だった。「secret key」と「consumer key」を共有するだけだ。だがそれは安全ではなかった。LTI 1.3 は、モダンな情報セキュリティモデルへと移行する。
- ログインフローのための OIDC(OpenID Connect)。
- データ交換のための JWT(JSON Web Tokens)。
- 共有シークレットの代わりの公開鍵・秘密鍵暗号(RSA256)。
これにより非常に安全になるが、ゼロから実装するのはより難しくなる。
🏗 アーキテクチャ
これを解決するために、モノリスは作らない。セキュリティプロバイダーとアプリケーション UI を分離する。
技術スタック
- バックエンド(セキュリティガード): Node.js + Express +
ltijs(Cvmcosta による、LTI 1.3 の標準的な Node.js ライブラリ)。 - フロントエンド(体験): Next.js 14+(App Router)+ React + Tailwind CSS。
- データベース: PostgreSQL(暗号鍵とプラットフォーム登録情報を保存)。
- インフラ: Docker Compose。
問題:「サードパーティ Cookie」の終末
LTI ツールにまつわる大きな課題は、それが LMS 内の <iframe> の中で動くということだ。モダンなブラウザ(Safari、Chrome シークレットモード)はデフォルトでサードパーティ Cookie をブロックする。
もしあなたの Web アプリがセッション Cookie(connect.sid)に依存しているなら?Safari では失敗する。
解決策:「LTIaaS」トークンフロー
このプロジェクトは、Cookie の制約を回避するために「ステートレス」なトークンパターンを実装している。
- LMS の起動: LMS が自分たちの Node.js バックエンドにリクエストを送る。
- ハンドシェイク: バックエンドが署名を検証する。有効であれば、短命で使い捨ての ltik(LTI key) を生成する。
- リダイレクト: バックエンドは iframe を Next.js フロントエンドへリダイレクトし、URL にトークンを渡す。
https://myapp.com/app?ltik=eyJhb... - ハイドレーション: Next.js アプリが URL からこのトークンを取得する。
- API 呼び出し: フロントエンドは、以後のすべての API 呼び出しで、この トークンを
Authorizationヘッダーの Bearer トークンとして使う。
Cookie は一切不要!
🚀 セットアップの方法(ステップバイステップ)
これをローカルマシンで5分で動かす方法はこうだ。
1. 前提条件
Docker がインストールされている必要がある。また ngrok(または同様のトンネリングサービス)も必要だ。LTI ツールは HTTPS で配信されなければならないからだ。単純に localhost を使うことはできない。
2. リポジトリをクローンする
git clone https://github.com/paurushrai/lti-1.3-node-nextjs-template.git
cd lti-1.3-node-nextjs-template3. 環境を設定する
環境変数のサンプルファイルをコピーする。
cp lti-server/.env.example lti-server/.envランダムで安全なキーを生成し、.env の中の LTI_KEY に貼り付ける。
4. トンネルを起動する
ポート3000で ngrok を起動する(自分たちの Node サーバーが動いている場所だ)。
ngrok http 3000https の URL をコピーする(例:https://my-tool.ngrok-free.app)。
5. アプリを起動する
lti-server/.env をもう一度開き、UI_URL を自分の ngrok URL に設定する。
さあ、Docker ですべてを起動する。
docker-compose up --buildこれで次が手に入る。
- LTI サーバー:
http://localhost:4000 - Next.js クライアント:
http://localhost:3000 - Postgres: 自動的に接続済み。
🔌 LMS への接続(「マジック」リンク)
昔は、公開鍵と URL を手動でコピー&ペーストする必要があった。このプロジェクトは LTI Dynamic Registration をサポートしている。
- 自分の登録用 URL をコピーする。
https://<your-ngrok-domain>/lti/register - 自分の LMS(例:Canvas の管理画面 → Developer Keys)を開く。
- **「Dynamic Registration」**を選択する。
- URL を貼り付けて Register をクリックする。
完了。 💥 ツールと LMS が自動的に公開鍵を交換する。ツールはこれでインストール済みだ。
🧪 フローをテストする
- 自分の LMS のコース内に、汎用の課題またはモジュール項目を作成する。
- 「External Tool」を選択し、新しいツールを選ぶ。
- 起動する。
- iframe の中に Next.js アプリが読み込まれるのが見える。
- こう表示される。「Welcome, [Your Name]」と、あなたのロール(Instructor/Student)。
アプリは正常に次のことをやり遂げている。
- 起動を認証した。
- LTI の起動データから、あなたのユーザープロファイルをデコードした。
- それを安全にフロントエンドへ渡した。
セキュリティ層と UI、あなたなら同じように分けるか?
プロトコルロジック(Node/ltijs)をアプリケーションロジック(Next.js)から分離したことが、この構成を現実的なものにした。セキュリティ層は地味で監査しやすいままであり、UI は OIDC や JWKS について何も知る必要のない、普通の Next.js アプリのままでいられる。
コードは GitHub に置いてある。フォークして、ハンドシェイクをすでに処理してくれるものから始めたいなら: lti-1.3-node-nextjs-template。この2つの層をこうやって分けたことで、助けになるどころか邪魔になったというケースに出くわした人がいるのか、気になっている。


