Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LTI Deep Linking 対応 #4

Closed
10 tasks done
cccties opened this issue Oct 7, 2020 · 17 comments · Fixed by #980
Closed
10 tasks done

LTI Deep Linking 対応 #4

cccties opened this issue Oct 7, 2020 · 17 comments · Fixed by #980
Assignees
Labels
Deep Linking enhancement 新機能実装またはリクエスト
Milestone

Comments

@cccties
Copy link

cccties commented Oct 7, 2020

LMS からは現在マイクロコンテンツへのリンクとして事前に登録された学習コンテンツにジャンプするしかナビゲーションが存在しないが、昨年標準化された LTI DL を使っていくことで権限や操作内容に応じたアクセスポイントの切替などナビゲーションの改善が出来るハズ。

https://www.imsglobal.org/spec/lti-dl/v2p0

まだ仕様を読めておらず、現状何が出来なくてどう変えることが出来るのかという詳細を把握しきれていないが、ひとまず将来やりたいこととして enhancement issue に記録


TODO:

@cccties cccties added the enhancement 新機能実装またはリクエスト label Oct 7, 2020
@cccties cccties added this to the Future milestone Oct 7, 2020
ties-makimura added a commit that referenced this issue Jan 28, 2022
@dynamis
Copy link

dynamis commented Dec 16, 2022

deep link に関する仕様メモ:

https://www.ssken.gr.jp/MAINSITE/event/2019/20191024-edu/lecture-02/SSKEN_edu2019_TokiwaYuji_presentation_20191015.pdf

https://www.imsglobal.org/spec/lti-dl/v2p0
image

show the form して LMS 側で選択可能な UI を提供する / リンク先側での操作を不要にできることを活かすお話。

Deep linking with LTI 1.3

Deep Linking 1.3

  • Users create content selectors with ad-hoc links
  • Instead of a basic launch, users can create a link selector for either Quicklink or Insert Stuff (addition extensions planned)
  • All auth and security data is maintained by the tool's deployment
  • Content selectors continue to be accessed the save way plugins have been in the past
  • LTI file picker for OneDrive users that can be used as a Quicklink to a file

@YouheiNozaki YouheiNozaki self-assigned this Apr 26, 2023
@kou029w
Copy link

kou029w commented Jun 12, 2023

試してみました:

パッチ

diff --git a/server/models/ltiResourceLinkRequest.ts b/server/models/ltiResourceLinkRequest.ts
index e8ba2e7c..0328bf6b 100644
--- a/server/models/ltiResourceLinkRequest.ts
+++ b/server/models/ltiResourceLinkRequest.ts
@@ -3,7 +3,6 @@ import type { FromSchema } from "json-schema-to-ts";
 export const LtiResourceLinkRequestSchema = {
   title: "LTI Resource Link Request",
   type: "object",
-  required: ["id"],
   properties: {
     id: { title: "LTI Resource Link ID", type: "string" },
     title: { title: "Title", type: "string" },
diff --git a/server/models/session.ts b/server/models/session.ts
index aa14c030..278020fa 100644
--- a/server/models/session.ts
+++ b/server/models/session.ts
@@ -18,7 +18,7 @@ export type SessionSchema = {
   ltiVersion: LtiVersionSchema;
   ltiUser: LtiUserSchema;
   ltiRoles: LtiRolesSchema;
-  ltiResourceLinkRequest: LtiResourceLinkRequestSchema;
+  ltiResourceLinkRequest?: LtiResourceLinkRequestSchema;
   ltiContext: LtiContextSchema;
   ltiLaunchPresentation?: LtiLaunchPresentationSchema;
   ltiAgsEndpoint?: LtiAgsEndpointSchema;
@@ -36,7 +36,6 @@ export const sessionSchema = {
     "ltiVersion",
     "ltiUser",
     "ltiRoles",
-    "ltiResourceLinkRequest",
     "ltiContext",
     "user",
     "systemSettings",
diff --git a/server/services/init.ts b/server/services/init.ts
index 189392c7..8d6be9f7 100644
--- a/server/services/init.ts
+++ b/server/services/init.ts
@@ -12,15 +12,17 @@ const frontendUrl = `${FRONTEND_ORIGIN}${FRONTEND_PATH}`;
 /** 起動時の初期化プロセス */
 async function init({ session }: FastifyRequest) {
   const systemSettings = getSystemSettings();
-  const ltiResourceLink = await findLtiResourceLink({
-    consumerId: session.oauthClient.id,
-    id: session.ltiResourceLinkRequest.id,
-  });
+  const ltiResourceLink = session.ltiResourceLinkRequest?.id
+    ? await findLtiResourceLink({
+        consumerId: session.oauthClient.id,
+        id: session.ltiResourceLinkRequest.id,
+      })
+    : null;
 
   if (ltiResourceLink) {
     await upsertLtiResourceLink({
       ...ltiResourceLink,
-      title: session.ltiResourceLinkRequest.title ?? ltiResourceLink.title,
+      title: session.ltiResourceLinkRequest?.title ?? ltiResourceLink.title,
       contextTitle: session.ltiContext.title ?? ltiResourceLink.contextTitle,
       contextLabel: session.ltiContext.label ?? ltiResourceLink.contextLabel,
     });
diff --git a/server/validators/ltiClaims.ts b/server/validators/ltiClaims.ts
index 617ba79a..a8278bb5 100644
--- a/server/validators/ltiClaims.ts
+++ b/server/validators/ltiClaims.ts
@@ -1,5 +1,6 @@
 import {
   Equals,
+  IsIn,
   IsNotEmpty,
   IsOptional,
   IsString,
@@ -37,8 +38,10 @@ export class LtiClaims {
         ),
     });
   }
-  @Equals("LtiResourceLinkRequest")
-  "https://purl.imsglobal.org/spec/lti/claim/message_type"!: "LtiResourceLinkRequest";
+  @IsIn(["LtiResourceLinkRequest", "LtiDeepLinkingRequest"])
+  "https://purl.imsglobal.org/spec/lti/claim/message_type"!:
+    | "LtiResourceLinkRequest"
+    | "LtiDeepLinkingRequest";
   @Equals("1.3.0")
   "https://purl.imsglobal.org/spec/lti/claim/version"!: "1.3.0";
   @IsNotEmpty()
@@ -46,9 +49,9 @@ export class LtiClaims {
   "https://purl.imsglobal.org/spec/lti/claim/deployment_id"!: string;
   @IsNotEmpty()
   "https://purl.imsglobal.org/spec/lti/claim/target_link_uri"!: string;
-  @IsNotEmpty()
+  @IsOptional()
   @ValidateNested()
-  "https://purl.imsglobal.org/spec/lti/claim/resource_link"!: ResourceLinkClaim;
+  "https://purl.imsglobal.org/spec/lti/claim/resource_link"?: ResourceLinkClaim;
   @IsNotEmpty()
   @IsString({ each: true })
   "https://purl.imsglobal.org/spec/lti/claim/roles"!: string[];
@@ -78,9 +81,9 @@ class ResourceLinkClaim {
   constructor(props?: Partial<ResourceLinkClaim>) {
     Object.assign(this, props);
   }
-  @IsNotEmpty()
+  @IsOptional()
   @IsString()
-  id!: string;
+  id?: string;
   @IsOptional()
   @IsString()
   title?: string;

id_token

{
  "nonce": "OI7cwo57cTe_sz0usHLUk7nL0kYqbKqyBvwDrvgHs8s",
  "iat": 1686556178,
  "exp": 1686556238,
  "iss": "http://localhost:8081",
  "aud": "By4PWqdQVnQx7SA",
  "https://purl.imsglobal.org/spec/lti/claim/deployment_id": "1",
  "https://purl.imsglobal.org/spec/lti/claim/target_link_uri": "http://localhost:8080",
  "sub": "2",
  "https://purl.imsglobal.org/spec/lti/claim/lis": {
    "person_sourcedid": "",
    "course_section_sourcedid": ""
  },
  "https://purl.imsglobal.org/spec/lti/claim/roles": [
    "http://purl.imsglobal.org/vocab/lis/v2/institution/person#Administrator",
    "http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor",
    "http://purl.imsglobal.org/vocab/lis/v2/system/person#Administrator"
  ],
  "https://purl.imsglobal.org/spec/lti/claim/context": {
    "id": "2",
    "label": "C1",
    "title": "コース1",
    "type": [
      "CourseSection"
    ]
  },
  "https://purl.imsglobal.org/spec/lti/claim/message_type": "LtiDeepLinkingRequest",
  "https://purl.imsglobal.org/spec/lti/claim/launch_presentation": {
    "locale": "en"
  },
  "https://purl.imsglobal.org/spec/lti/claim/ext": {
    "lms": "moodle-2"
  },
  "https://purl.imsglobal.org/spec/lti/claim/tool_platform": {
    "product_family_code": "moodle",
    "version": "2022112802",
    "guid": "e616f7a43757ade063281213a522f1f6",
    "name": "New Site",
    "description": "New Site"
  },
  "https://purl.imsglobal.org/spec/lti/claim/version": "1.3.0",
  "https://purl.imsglobal.org/spec/lti-ags/claim/endpoint": {
    "scope": [
      "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem",
      "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly",
      "https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly",
      "https://purl.imsglobal.org/spec/lti-ags/scope/score"
    ],
    "lineitems": "http://localhost:8081/mod/lti/services.php/2/lineitems?type_id=1"
  },
  "https://purl.imsglobal.org/spec/lti/claim/custom": {
    "context_memberships_url": "http://localhost:8081/mod/lti/services.php/CourseSection/2/bindings/1/memberships"
  },
  "https://purl.imsglobal.org/spec/lti-nrps/claim/namesroleservice": {
    "context_memberships_url": "http://localhost:8081/mod/lti/services.php/CourseSection/2/bindings/1/memberships",
    "service_versions": [
      "1.0",
      "2.0"
    ]
  },
  "https://purl.imsglobal.org/spec/lti-dl/claim/deep_linking_settings": {
    "accept_types": [
      "ltiResourceLink"
    ],
    "accept_presentation_document_targets": [
      "frame",
      "iframe",
      "window"
    ],
    "accept_copy_advice": false,
    "accept_multiple": true,
    "accept_unsigned": false,
    "auto_create": false,
    "can_confirm": false,
    "deep_link_return_url": "http://localhost:8081/mod/lti/contentitem_return.php?course=2&id=1&sesskey=tthwwUyAfa",
    "title": "リンク",
    "text": ""
  }
}

LtiResourceLinkRequest との差分:


決まっていない点

  • resource_link をバインドする方法
    • URL or LtiResourceLinkRequest としてパラメータを受け取り、それを元にリンクを決める
      • Deep Linkingによる選択の段階では Resource Link は存在しない
        • Deep Linking 後に Resource Link は作成される
        • 作成するResource Link IDを指定する手段があるかは要調査
      • Deep Linkingによって選択後、提供するブックを選択・変更する場合、どう扱うか?
        • A. Deep Linkingを優先する・Deep Linking以外でのリンクの変更を禁止
          • この場合、リンクを変更するには Deep Linking で再び選択するか、URL or カスタムパラメータを手動で変更 or 削除して選択しなおす
        • B. リンクからアクセスして変更する従来どおりの操作を優先する・Deep Linkingでのリンクの変更を禁止
          • この場合、Deep Linking によって決まるパラメータを無視することになる

@kou029w
Copy link

kou029w commented Jun 12, 2023

  • それを使って LtiResourceLinkRequest として受け取れるのかな?

LtiDeepLinkingResponse の中で "https://purl.imsglobal.org/spec/lti-dl/claim/content_items" に "custom" プロパティを加える。
"https://purl.imsglobal.org/spec/lti/claim/custom" で受け取れる。

例:
LtiDeepLinkingResponse

client.requestObject などによって JWT を生成
リクエストボディに JWT={生成したJWT … 署名したLtiDeepLinkingResponse} を指定
"deep_link_return_url" に content-type:application/x-www-form-urlencoded で POST すること

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImYwMTU1NjBhYWZiNThmYThjOGMzOGM5N2FmMGJiZDYzIn0.eyJpc3MiOiJNbkxRV3Q2czNLZ2NHMWoiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjgwODEiLCJub25jZSI6Im9ibTE3YXM5aTcxM3VjNzQ1bzcxdGlxcjMiLCJodHRwczovL3B1cmwuaW1zZ2xvYmFsLm9yZy9zcGVjL2x0aS9jbGFpbS9kZXBsb3ltZW50X2lkIjoiMiIsImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvbHRpL2NsYWltL21lc3NhZ2VfdHlwZSI6Ikx0aURlZXBMaW5raW5nUmVzcG9uc2UiLCJodHRwczovL3B1cmwuaW1zZ2xvYmFsLm9yZy9zcGVjL2x0aS9jbGFpbS92ZXJzaW9uIjoiMS4zLjAiLCJodHRwczovL3B1cmwuaW1zZ2xvYmFsLm9yZy9zcGVjL2x0aS1kbC9jbGFpbS9tc2ciOiJTdWNjZXNzZnVsbHkgUmVnaXN0ZXJlZCIsImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvbHRpLWRsL2NsYWltL2NvbnRlbnRfaXRlbXMiOlt7InR5cGUiOiJsdGlSZXNvdXJjZUxpbmsiLCJ0aXRsZSI6Ikx0aWpzIERlbW8iLCJjdXN0b20iOnsibmFtZSI6IlJlc291cmNlMiIsInZhbHVlIjoidmFsdWUyIn19XSwiaWF0IjoxNjg2NTY2NDE4LCJleHAiOjE2ODY1NjY0Nzh9.iILK-ZyG63vtaCOZj8MV-jSb-Hl2XGSDPHbFn2001IVrA6ZJexSvcG5pPWjZUbfTsJl1PkzBE4g65Rut72E6fjMbLxON3t-ghXmpdM1v_CohSBamksESlRuidzy12qOOz7xHdqosaiDV2YmRLbMRf97LWN_HAHtOFb80F5KK1mRMSSHdekvOt4FUYTUeVLfaQBC1eComDY2kFPLThbfiR--g1T50UYPCPxJa-pynwwW2s2jh1xV_Q9_cNwvC0qWtNJf4QeSIQO2uaZUg9HS0jgMGrQEVnU-L-3IRUF6Uv9uyHeOQmh3iPtXZZDSVB37sQWGzTRNdHiIsec_keF3QfJO8HdmOLRhJdRm_XBrE5FRLCKWuM3qKJL-ZFZ3oOH_-LesPBZ1OT7HQXx6u9OxUIlcilJeq1j3rdGBeQaBSrDHg7jR7kaoaXkr6uJ9JvymqYrXFshrxq8j7qi5qvNwfzwQ05a9UKil0-gHDxzsiCEUYv5mPQYRrfm_WiAYtKTfbC-QVPFXgn4LjyGQ-2C0aUYuWDTWn3XV5h1jaQJhlt74-dtKs_se4l05ZUc5eLqGZfrDFY_ig_tVGF1qcvr0HtQTL-JJWupUBDZKKuIQwfM5whN7aVT8rwbZizTGkUl1qw2uT2jXA1cmfkUygOF56oeI9S3VpOoHtsa4gykZDFvc

抜粋

  "https://purl.imsglobal.org/spec/lti-dl/claim/content_items": [
    {
      "type": "ltiResourceLink",
      "title": "Ltijs Demo",
      "custom": {
        "name": "Resource2",
        "value": "value2"
      }
    }
  ],

LtiResourceLinkRequest

eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ijg3YzdiZDE5MzBjZWM1ODczNTFiIn0.eyJub25jZSI6Im1tZHViZXVscmNvNzZkZHRscHhhcDNmM3ciLCJpYXQiOjE2ODY1NjY0MjAsImV4cCI6MTY4NjU2NjQ4MCwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgxIiwiYXVkIjoiTW5MUVd0NnMzS2djRzFqIiwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9sdGkvY2xhaW0vZGVwbG95bWVudF9pZCI6IjIiLCJodHRwczovL3B1cmwuaW1zZ2xvYmFsLm9yZy9zcGVjL2x0aS9jbGFpbS90YXJnZXRfbGlua191cmkiOiJodHRwczovL3Bhc3NpbmctbG9va3NtYXJ0LWdvdmVybmluZy12aWN0b3JpYW4udHJ5Y2xvdWRmbGFyZS5jb20iLCJzdWIiOiIyIiwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9sdGkvY2xhaW0vbGlzIjp7InBlcnNvbl9zb3VyY2VkaWQiOiIiLCJjb3Vyc2Vfc2VjdGlvbl9zb3VyY2VkaWQiOiIifSwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9sdGkvY2xhaW0vcm9sZXMiOlsiaHR0cDovL3B1cmwuaW1zZ2xvYmFsLm9yZy92b2NhYi9saXMvdjIvaW5zdGl0dXRpb24vcGVyc29uI0FkbWluaXN0cmF0b3IiLCJodHRwOi8vcHVybC5pbXNnbG9iYWwub3JnL3ZvY2FiL2xpcy92Mi9tZW1iZXJzaGlwI0luc3RydWN0b3IiLCJodHRwOi8vcHVybC5pbXNnbG9iYWwub3JnL3ZvY2FiL2xpcy92Mi9zeXN0ZW0vcGVyc29uI0FkbWluaXN0cmF0b3IiXSwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9sdGkvY2xhaW0vY29udGV4dCI6eyJpZCI6IjIiLCJsYWJlbCI6IkMxIiwidGl0bGUiOiJcdTMwYjNcdTMwZmNcdTMwYjkxIiwidHlwZSI6WyJDb3Vyc2VTZWN0aW9uIl19LCJodHRwczovL3B1cmwuaW1zZ2xvYmFsLm9yZy9zcGVjL2x0aS9jbGFpbS9tZXNzYWdlX3R5cGUiOiJMdGlSZXNvdXJjZUxpbmtSZXF1ZXN0IiwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9sdGkvY2xhaW0vcmVzb3VyY2VfbGluayI6eyJ0aXRsZSI6Ikx0aWpzIERlbW8iLCJkZXNjcmlwdGlvbiI6IiIsImlkIjoiMiJ9LCJodHRwczovL3B1cmwuaW1zZ2xvYmFsLm9yZy9zcGVjL2x0aS9jbGFpbS9sYXVuY2hfcHJlc2VudGF0aW9uIjp7ImxvY2FsZSI6ImVuIiwiZG9jdW1lbnRfdGFyZ2V0IjoiZnJhbWUiLCJyZXR1cm5fdXJsIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgxL21vZC9sdGkvcmV0dXJuLnBocD9jb3Vyc2U9MiZsYXVuY2hfY29udGFpbmVyPTUmaW5zdGFuY2VpZD0yJnNlc3NrZXk9MkMzSGFFazhZViJ9LCJodHRwczovL3B1cmwuaW1zZ2xvYmFsLm9yZy9zcGVjL2x0aS9jbGFpbS9leHQiOnsibG1zIjoibW9vZGxlLTIifSwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9sdGkvY2xhaW0vdG9vbF9wbGF0Zm9ybSI6eyJwcm9kdWN0X2ZhbWlseV9jb2RlIjoibW9vZGxlIiwidmVyc2lvbiI6IjIwMjIxMTI4MDIiLCJndWlkIjoiZTYxNmY3YTQzNzU3YWRlMDYzMjgxMjEzYTUyMmYxZjYiLCJuYW1lIjoiTmV3IFNpdGUiLCJkZXNjcmlwdGlvbiI6Ik5ldyBTaXRlIn0sImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvbHRpL2NsYWltL3ZlcnNpb24iOiIxLjMuMCIsImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvbHRpL2NsYWltL2N1c3RvbSI6eyJuYW1lIjoiUmVzb3VyY2UyIiwidmFsdWUiOiJ2YWx1ZTIifX0.VYK5sfav63k_rxzfk3EPPc8GoF5TvAXbhlezP-XQ8Gz-c5HFtdoz3_4fXQV2jdopfww4R32XzJx8BetmBYwyTzGty9DP3SbmbFbk2eb_bHOoLc2d6uQAjfeST1CWbL9_DydNws2LpvMnxy1d0vzYUA06-eXrlIXitIPtiMAx9smQZVaCQWrQ_lZS8h8o68FHKzHNIgvvHJ2kSxoVsXa2t7BZtIvFN5nZMpzLQSD2uKX49bbwJzOYEYwQBzvQJG3FbNWf9wShg0IXERgPEp7Kb3JNlHEcWIeRBw7APJ78MoDc7Hx1A5rkQBYz1p_DBgp6hjCPN9_rygIwNXbX3OunHQ

別の案

同じドメインであれば "url" を指定してツールのログイン初期化エンドポイントにクエリーを足すなど別のURLを指定することができる。
ここが指定可能。
image

@kou029w
Copy link

kou029w commented Jun 13, 2023

  • 作成するResource Link IDを指定する手段があるかは要調査

LTI-AGS 2.0 と組み合わせることで可能かどうか調べてみた。
→ 不可能

3.2.8 resourceLinkId and binding a line item to a resource link
A line item MAY be attached to a resource link by including a 'resourceLinkId' in the payload. The resource link MUST exist in the context where the line item is created, and MUST be a link owned by the same tool. If not, the line item creation MUST fail with a response code of Not Found 404.

The platform MAY remove the line items attached to a resource link if the resource link itself is removed.

https://www.imsglobal.org/spec/lti-ags/v2p0#resourcelinkid-and-binding-a-line-item-to-a-resource-link

https://www.imsglobal.org/spec/lti-ags/v2p0/openapi/#/default/LineItem.GET

あらかじめ "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly" を許可することで LineItem 取得エンドポイントを介して Resource Link ID の取得が可能なようにも読めた。なので、実際にMoodleで試してみた。
→ LineItem 取得エンドポイントは LtiDeepLinkingRequest には存在せず。

Deep Linking での id_token

{
  "nonce": "kAB0PGujNiBM56k7yCoKyHfxUbKerkyEKBVcXjFn5ro",
  "iat": 1686651087,
  "exp": 1686651147,
  "iss": "http://localhost:8081",
  "aud": "By4PWqdQVnQx7SA",
  "https://purl.imsglobal.org/spec/lti/claim/deployment_id": "1",
  "https://purl.imsglobal.org/spec/lti/claim/target_link_uri": "http://localhost:8080",
  "sub": "2",
  "https://purl.imsglobal.org/spec/lti/claim/lis": {
    "person_sourcedid": "",
    "course_section_sourcedid": ""
  },
  "https://purl.imsglobal.org/spec/lti/claim/roles": [
    "http://purl.imsglobal.org/vocab/lis/v2/institution/person#Administrator",
    "http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor",
    "http://purl.imsglobal.org/vocab/lis/v2/system/person#Administrator"
  ],
  "https://purl.imsglobal.org/spec/lti/claim/context": {
    "id": "2",
    "label": "C1",
    "title": "コース1",
    "type": [
      "CourseSection"
    ]
  },
  "https://purl.imsglobal.org/spec/lti/claim/message_type": "LtiDeepLinkingRequest",
  "https://purl.imsglobal.org/spec/lti/claim/launch_presentation": {
    "locale": "en"
  },
  "https://purl.imsglobal.org/spec/lti/claim/ext": {
    "lms": "moodle-2"
  },
  "https://purl.imsglobal.org/spec/lti/claim/tool_platform": {
    "product_family_code": "moodle",
    "version": "2022112802",
    "guid": "e616f7a43757ade063281213a522f1f6",
    "name": "New Site",
    "description": "New Site"
  },
  "https://purl.imsglobal.org/spec/lti/claim/version": "1.3.0",
  "https://purl.imsglobal.org/spec/lti-ags/claim/endpoint": {
    "scope": [
      "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem",
      "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly",
      "https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly",
      "https://purl.imsglobal.org/spec/lti-ags/scope/score"
    ],
    "lineitems": "http://localhost:8081/mod/lti/services.php/2/lineitems?type_id=1"
  },
  "https://purl.imsglobal.org/spec/lti/claim/custom": {
    "context_memberships_url": "http://localhost:8081/mod/lti/services.php/CourseSection/2/bindings/1/memberships"
  },
  "https://purl.imsglobal.org/spec/lti-nrps/claim/namesroleservice": {
    "context_memberships_url": "http://localhost:8081/mod/lti/services.php/CourseSection/2/bindings/1/memberships",
    "service_versions": [
      "1.0",
      "2.0"
    ]
  },
  "https://purl.imsglobal.org/spec/lti-dl/claim/deep_linking_settings": {
    "accept_types": [
      "ltiResourceLink"
    ],
    "accept_presentation_document_targets": [
      "frame",
      "iframe",
      "window"
    ],
    "accept_copy_advice": false,
    "accept_multiple": true,
    "accept_unsigned": false,
    "auto_create": false,
    "can_confirm": false,
    "deep_link_return_url": "http://localhost:8081/mod/lti/contentitem_return.php?course=2&id=1&sesskey=q3BpOMrSgL",
    "title": "chibichilo",
    "text": ""
  }
}

@kou029w
Copy link

kou029w commented Jun 14, 2023

  • Deep Linkingによって選択後、提供するブックを選択・変更する場合、どう扱うか?
    • A. Deep Linkingを優先する・Deep Linking以外でのリンクの変更を禁止
      • この場合、リンクを変更するには Deep Linking で再び選択するか、URL or カスタムパラメータを手動で変更 or 削除して選択しなおす

まずはこの方針で進めるのがよさそうです。
なお、もし仮にDeep Linkingが有効化されていない・選択していないケース(→ URL or カスタムパラメータが設定されていないケース)ならば、従来どおりリンクからアクセスして操作できることを期待 (後方互換の観点)。

@kou029w
Copy link

kou029w commented Jun 14, 2023

いくつかの機能の提案についてはIssueを分割しました (本件とは別スコープ)

@kou029w
Copy link

kou029w commented Jun 14, 2023

LtiDeepLinkingResponceの一部:

  "https://purl.imsglobal.org/spec/lti-dl/claim/content_items": [
    {
      "type": "ltiResourceLink",
      "url": ここで指定したURLがLtiResourceLinkRequestのクレームにあるtarget_link_uriとして設定されるハズ
    }
  ],

LtiResourceLinkRequest

id_token

  "https://purl.imsglobal.org/spec/lti/claim/target_link_uri": "http://localhost:8080/book?bookId=1",

これを、セッションに書き込み→このURLにクライアントがアクセスする

@kou029w
Copy link

kou029w commented Jun 20, 2023

LtiDeepLinkingResponce

Tool-Originating Messages によって行なう(と想像)。
https://www.imsglobal.org/spec/security/v1p0/#tool-originating-messages

JWTのclaimによって身元や有効期限が表明され認証を行なう。仕様を確認してみる。

iss
: REQUIRED. Issuer Identifier for the Issuer of the message i.e. the Tool. It must be the OAuth 2.0 client_id of the Tool (this MAY be provided to it by the Platform upon registration of the Tool).

aud
: REQUIRED. Audience(s) for whom this Tool JWT is intended. It MUST contain the case-sensitive URL used by the Platform to identify itself as an Issuer in platform-originating Messages. In the common special case when there is one audience, the aud value MAY be a single case-sensitive string.

exp
: REQUIRED. Expiration time on or after which the Platform MUST NOT accept the Tool JWT for processing. When processing this parameter, the Platform MUST verify that the time expressed in this Claim occurs after the current date/time. Implementers MAY provide for some small leeway, usually no more than a few minutes, to account for clock skew. This Claim's value MUST be a JSON number representing the number of seconds offset from 1970-01-01T00:00:00Z (UTC). See [RFC3339] for details regarding date/times in general and UTC in particular.

iat
: REQUIRED. Time at which the Issuer generated the Tool JWT. Its value is a JSON number representing the number of seconds offset from 1970-01-01T00:00:00Z (UTC) until the generation time.

nonce
: REQUIRED. String value used to associate a Tool session with a Tool JWT, and to mitigate replay attacks. The nonce value is a case-sensitive string.

azp
: OPTIONAL. Authorized party - the party to which the Tool JWT was issued. If present, it MUST contain the same value as in the aud Claim. The azp value is a case-sensitive string containing a String or URI value.

exp の部分の参考値を調べてみる。
Ltijsの例:

https://github.com/Cvmcosta/ltijs/blob/6a7bcc622c0123a3dd70e95a884deb12409a0ad8/src/Provider/Services/DeepLinking.js#L113

expiresIn: 60 とあり、有効期間60秒(定数)だと分かった。usually no more than a few minutes としてこのくらいの値が妥当なのかな。

実験してみます。

@kou029w
Copy link

kou029w commented Jun 20, 2023

実験方法

  • 下記の実証用パッチを適応
  • 開発用サーバーを起動して、DLの選択画面にアクセス
    • ターミナルのログから Deep Link Return URL、LTI Deployment ID を確認
      • これらの値を使ってJWTの生成・フォームの生成を行ない、クライアント側からPOSTを行なう

実証用パッチ:

diff --git a/pages/books/index.tsx b/pages/books/index.tsx
index aed7c0e7..c47254fa 100644
--- a/pages/books/index.tsx
+++ b/pages/books/index.tsx
@@ -83,6 +83,8 @@ function Index() {
 
   return (
     <>
+      {/* TODO: 実験目的。後で削除 */}
+      <a href="http://localhost:8080/api/v2/lti/demo">/api/v2/lti/demo</a>
       <Books linkedBook={linkedBook} {...handlers} />
       {previewContent?.type === "book" && (
         <BookPreviewDialog {...dialogProps} book={previewContent}>
diff --git a/server/config/app.ts b/server/config/app.ts
index 01cb54c0..8093ef97 100644
--- a/server/config/app.ts
+++ b/server/config/app.ts
@@ -35,17 +35,18 @@ async function app(fastify: FastifyInstance, options: Options) {
     routePrefix: `${basePath}/swagger`,
   });
 
-  await fastify.register(helmet, {
-    contentSecurityPolicy: {
-      directives: {
-        defaultSrc: ["'self'"],
-        imgSrc: ["'self'", "data:"],
-        scriptSrc: ["'self'"].concat(fastify.swaggerCSP.script),
-        styleSrc: ["'self'"].concat(fastify.swaggerCSP.style),
-      },
-    },
-    frameguard: false,
-  });
+  // TODO: デバッグ目的。本番環境では必須。あとで戻す。
+  // await fastify.register(helmet, {
+  //   contentSecurityPolicy: {
+  //     directives: {
+  //       defaultSrc: ["'self'"],
+  //       imgSrc: ["'self'", "data:"],
+  //       scriptSrc: ["'self'"].concat(fastify.swaggerCSP.script),
+  //       styleSrc: ["'self'"].concat(fastify.swaggerCSP.style),
+  //     },
+  //   },
+  //   frameguard: false,
+  // });
 
   await Promise.all([
     fastify.register(cors, {
diff --git a/server/config/routes/lti.ts b/server/config/routes/lti.ts
index 2fcc2ede..4352281f 100644
--- a/server/config/routes/lti.ts
+++ b/server/config/routes/lti.ts
@@ -9,6 +9,10 @@ import * as ltiKeys from "$server/services/ltiKeys";
 import * as ltiClients from "$server/services/ltiClients";
 import * as linkSearch from "$server/services/linkSearch";
 import * as ltiMembersService from "$server/services/ltiMembers";
+import { SignJWT, importJWK } from "jose";
+import { generators } from "openid-client";
+import { createPrivateKey } from "$server/utils/ltiv1p3/jwk";
+import findClient from "$server/utils/ltiv1p3/findClient";
 
 export async function launch(fastify: FastifyInstance) {
   const path = "/lti/launch";
@@ -98,3 +102,54 @@ export async function ltiMembers(fastify: FastifyInstance) {
     Body: ltiMembersService.Body;
   }>(path, { schema: method.put, ...hooks.put }, handler(update));
 }
+
+export async function demo(fastify: FastifyInstance) {
+  fastify.get("/lti/demo", async (req, res) => {
+    // TODO: session には LtiDeepLinkingSettings がないけど少なくとも deepLinkReturnUrl を参照できるようにしておきたい
+    // TODO: session には LtiDeploymentId がないけど必要
+    // TODO: 「LTI Resource Linkの更新」ではなくDL更新処理にしてみる実験
+    //        ホントは別のAPIに分けるか実装に合わせて説明を変更するなどしておくべき
+    const url =
+      "http://localhost:8081/mod/lti/contentitem_return.php?course=2&id=1&sesskey=99IpUHWa4o";
+    const alg = "RS256";
+    const privateKey = await createPrivateKey();
+    const client = await findClient(req.session.oauthClient.id);
+    const jwt = await new SignJWT({
+      nonce: generators.nonce(),
+      "https://purl.imsglobal.org/spec/lti/claim/deployment_id": "1", // ← 例。ホントはセッションから取り出したい
+      "https://purl.imsglobal.org/spec/lti/claim/message_type":
+        "LtiDeepLinkingResponse",
+      "https://purl.imsglobal.org/spec/lti/claim/version": "1.3.0",
+      "https://purl.imsglobal.org/spec/lti-dl/claim/msg":
+        "Successfully Registered",
+      "https://purl.imsglobal.org/spec/lti-dl/claim/content_items": [
+        {
+          type: "ltiResourceLink",
+          url: "http://localhost:8080/book?bookId=99999", // ← 例
+        },
+      ],
+    })
+      .setProtectedHeader({ alg, kid: privateKey?.kid })
+      // @ts-expect-error TODO: undefined であればエラーを返す
+      .setIssuer(client.metadata.client_id)
+      // @ts-expect-error TODO: undefined であればエラーを返す
+      .setAudience(client.issuer.metadata.issuer)
+      .setIssuedAt()
+      .setExpirationTime("60s")
+      .sign(await importJWK({ alg, ...privateKey }));
+
+    console.log(jwt);
+
+    void res.header("content-type", "text/html; charset=utf-8");
+    await res.send(
+      `
+<form style="display: none;" action="${url}" method="POST">
+<input type="hidden" name="JWT" value="${jwt}" />
+</form>
+<script>
+document.querySelector("form").submit()
+</script>
+`
+    );
+  });
+}
diff --git a/server/services/ltiCallback.ts b/server/services/ltiCallback.ts
index 5ac17125..ef659d15 100644
--- a/server/services/ltiCallback.ts
+++ b/server/services/ltiCallback.ts
@@ -44,6 +44,22 @@ export async function post(req: FastifyRequest<{ Body: Props }>) {
       nonce: req.session.oauthClient.nonce,
     });
     const claims = token.claims();
+
+    // TODO: 実験目的。後で削除
+    console.debug(
+      "url",
+      claims[
+        "https://purl.imsglobal.org/spec/lti-dl/claim/deep_linking_settings"
+        // @ts-expect-error TODO: LtiClaimsの他のクレームと同様にバリデーションを行なうべき
+      ]?.deep_link_return_url
+    );
+
+    // TODO: 実験目的。後で削除
+    console.debug(
+      "deploymentId",
+      claims["https://purl.imsglobal.org/spec/lti/claim/deployment_id"]
+    );
+
     const ltiClaims = new LtiClaims(claims as Partial<LtiClaims>);
     await validateOrReject(ltiClaims);
     const session: Omit<SessionSchema, "user" | "systemSettings"> = {

実験結果

Screencast.from.2023.06.20.20.02.21.webm

分かったこと

  • LtiDeepLinkingResponse
    • Tool-Originating Messages によって行なう具体的な流れ

@kou029w
Copy link

kou029w commented Jun 23, 2023

@YouheiNozaki
LtiDeepLinkingResponse や Tool-Originating Messages の仕組み理解して実装方針決めてコードを書いていくことができそうでしょうか?
難しい or 一部説明があればできそう or 問題ない/実装中 など、フィードバックもらえればそれに合わせます。
意外と難しいのかもなのでそのあたりはお気軽にご相談ください 🙏

@kou029w
Copy link

kou029w commented Jun 23, 2023

意外と難しいのかも

本件は分割・分担したほうが良さそうな気もしました。TODOの部分でそれぞれ課題の分割してそれぞれ個別のIssuesとして起票してもらうことできますか? いくつか片付けるのを私も手伝わせてください。よろしくです 🙏 > @YouheiNozaki さん

@YouheiNozaki
Copy link

YouheiNozaki commented Jun 23, 2023

ありがとうございます!

そうですね。正直認証周りの仕様読み解くの結構難しいです。。。

TODOの部分でそれぞれ課題の分割してそれぞれ個別のIssuesとして起票してもらうことできますか?

いただいたデモをベースにタスク分けや問題の切り分けしたりはできると思うので、後ほど対応します🙏

どちらにしろ、少し実装相談等、お話したいので来週頭あたりにミーティングお願いしたいです🙏
土日にできる範囲で実装進められるところはやっていこうかと思います。

@YouheiNozaki
Copy link

こちらタスク分割しました!

@kou029w
Copy link

kou029w commented Jun 28, 2023

ミーティングお願いしたいです

はい、大丈夫です 🙆‍♂️
いつにしましょうかね?

@YouheiNozaki
Copy link

すいません、明日はちょっと予定があるので、来週頭にセットさせていただきます🙏
よろしくお願い致します!

@YouheiNozaki
Copy link

メモ:

実装のゴール

  • 選択をしたリンクに設定する
  • ゴールは決まっていない
     - 例えば「リンクを提供」を押したら、ブックのURLが提供される見たいなイメージ←これで良さそう

進め方

  • Deep Linkingの連携は渡邉さんが担当
  • 待ち時間が発生する箇所あり
  • 野崎はUI部分の仕様含め、実装を進める
  • ブランチはlit-deep-linkで進める。

@kou029w
Copy link

kou029w commented Sep 8, 2023

@YouheiNozaki

#980 (comment)
#980 (comment)

この2つのコメントについて確認のほどよろしくお願いします 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Deep Linking enhancement 新機能実装またはリクエスト
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants