Closed cccties closed 1 year ago
deep link に関する仕様メモ:
https://www.imsglobal.org/spec/lti-dl/v2p0
show the form して LMS 側で選択可能な UI を提供する / リンク先側での操作を不要にできることを活かすお話。
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
試してみました:
パッチ
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 との差分:
決まっていない点
- それを使って 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を指定することができる。 ここが指定可能。
- 作成する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/openapi/#/default/LineItem.GET
あらかじめ "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly" を許可することで LineItem 取得エンドポイントを介して Resource Link ID の取得が可能なようにも読めた。なので、実際にMoodleで試してみた。 → LineItem 取得エンドポイントは LtiDeepLinkingRequest には存在せず。
- Deep Linkingによって選択後、提供するブックを選択・変更する場合、どう扱うか?
- A. Deep Linkingを優先する・Deep Linking以外でのリンクの変更を禁止
- この場合、リンクを変更するには Deep Linking で再び選択するか、URL or カスタムパラメータを手動で変更 or 削除して選択しなおす
まずはこの方針で進めるのがよさそうです。 なお、もし仮にDeep Linkingが有効化されていない・選択していないケース(→ URL or カスタムパラメータが設定されていないケース)ならば、従来どおりリンクからアクセスして操作できることを期待 (後方互換の観点)。
いくつかの機能の提案についてはIssueを分割しました (本件とは別スコープ)
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にクライアントがアクセスする
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の例:
expiresIn: 60 とあり、有効期間60秒(定数)だと分かった。usually no more than a few minutes としてこのくらいの値が妥当なのかな。
実験してみます。
実験方法
実証用パッチ:
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
分かったこと
@YouheiNozaki LtiDeepLinkingResponse や Tool-Originating Messages の仕組み理解して実装方針決めてコードを書いていくことができそうでしょうか? 難しい or 一部説明があればできそう or 問題ない/実装中 など、フィードバックもらえればそれに合わせます。 意外と難しいのかもなのでそのあたりはお気軽にご相談ください :pray:
意外と難しいのかも
本件は分割・分担したほうが良さそうな気もしました。TODOの部分でそれぞれ課題の分割してそれぞれ個別のIssuesとして起票してもらうことできますか? いくつか片付けるのを私も手伝わせてください。よろしくです :pray: > @YouheiNozaki さん
ありがとうございます!
そうですね。正直認証周りの仕様読み解くの結構難しいです。。。
TODOの部分でそれぞれ課題の分割してそれぞれ個別のIssuesとして起票してもらうことできますか?
いただいたデモをベースにタスク分けや問題の切り分けしたりはできると思うので、後ほど対応します🙏
どちらにしろ、少し実装相談等、お話したいので来週頭あたりにミーティングお願いしたいです🙏 土日にできる範囲で実装進められるところはやっていこうかと思います。
こちらタスク分割しました!
ミーティングお願いしたいです
はい、大丈夫です :ok_man: いつにしましょうかね?
すいません、明日はちょっと予定があるので、来週頭にセットさせていただきます🙏 よろしくお願い致します!
メモ:
実装のゴール
進め方
@YouheiNozaki
https://github.com/npocccties/chibichilo/pull/980#issuecomment-1711109566 https://github.com/npocccties/chibichilo/pull/980#issuecomment-1711383651
この2つのコメントについて確認のほどよろしくお願いします :pray:
LMS からは現在マイクロコンテンツへのリンクとして事前に登録された学習コンテンツにジャンプするしかナビゲーションが存在しないが、昨年標準化された LTI DL を使っていくことで権限や操作内容に応じたアクセスポイントの切替などナビゲーションの改善が出来るハズ。
https://www.imsglobal.org/spec/lti-dl/v2p0
まだ仕様を読めておらず、現状何が出来なくてどう変えることが出来るのかという詳細を把握しきれていないが、ひとまず将来やりたいこととして enhancement issue に記録
TODO: