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

Deep Linking - Fails for Blackboard #237

Open
tomitrescak opened this issue Sep 26, 2024 · 12 comments
Open

Deep Linking - Fails for Blackboard #237

tomitrescak opened this issue Sep 26, 2024 · 12 comments
Assignees
Labels
bug Something isn't working

Comments

@tomitrescak
Copy link

tomitrescak commented Sep 26, 2024

This more of a questions.
Igot everything working but Deep Linking.

For some reason, when I submit the deep lnked form, it generates "Tool (Invalid Link)" In Balckboard fro me that I cannot click on. Any idea what could be the issue?

{
  iss: "e1a9f302-6c64-4ecd-904b-5eddae93c10f",
  aud: "https://blackboard.com",
  nonce: "2067uke47q5vtlb6x3gzgj40i",
  "https://purl.imsglobal.org/spec/lti/claim/deployment_id": "6c97b9c2-35df-41ca-9ac9-b23xxxx26981",
  "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/data": "_48076_1::_9637154_1::-1::false::false::_1216_1::5eda69725xxxx26a94d29c1453be1a5d::false::false",
  "https://purl.imsglobal.org/spec/lti-dl/claim/content_items": [
    {
      type: "ltiResourceLink",
      title: "Ltijs Demo",
      custom: {
        cookoo: "resource2",
      },
    },
  ],
}

This is the jwt token generated by ltijs.

This is in Blckboard:

image

@tomitrescak tomitrescak added the bug Something isn't working label Sep 26, 2024
@siddrcrelias
Copy link

Hello, @tomitrescak is this solved...I was able to implement LTI in my org. and we even have customers using it on Avendoo and Moodle...let me know if you need help with this :)

@tomitrescak
Copy link
Author

No, the deep linking is evading us. Linking of type “link” works but linking “ltitoollink” always leads to the “invalid link” above. I have no idea how to debug it. It seems like configuration issue or me not sending correct data. But it just does not work.

@tomitrescak
Copy link
Author

Hello, @tomitrescak is this solved...I was able to implement LTI in my org. and we even have customers using it on Avendoo and Moodle...let me know if you need help with this :)

Are you using any custom parameters? Would you be so kind to show me screeshot of your blackboard config and the code for deep linking route? This is mine:

router.post("/deeplink", async (req, res) => {
  try {
    const resource = req.body;
    const items = [
      {
        type: "ltiResourceLink",
        title: "My Tool Link",
      },
    ];

    const form = await lti.DeepLinking.createDeepLinkingForm(
      res.locals.token,
      items,
      { message: "Successfully Registered" }
    );
    if (form) return res.send(form);
    return res.sendStatus(500);
  } catch (err) {
    console.log(err.message);
    return res.status(500).send(err.message);
  }
});

@siddrcrelias
Copy link

siddrcrelias commented Oct 23, 2024

Hello @tomitrescak
Sorry for seeing this message late, let me send you some links
https://github.com/siddrc/ltijs-nestjs-server - this is the implementation that I am using at my company, some parts have been skipped but this should give you an idea on how to achieve onboarding various customers , doing deeplinking, assigning different tools to each customer and launching the deep linking UI interface

I also have a medium article on this, so that no one has to break their head on how to implement LTIv1.3
https://medium.com/@debu2in/implementing-ltiv1-3-using-ltijs-1ab38ab87567

I have a discord as well siddharthroyc_97017 , this will notify me on mobile..and I can help sooner...I would ask you to go through the code and the article, if the answer below does not help, or may be reach me out on discord.

Ok, so you are asking if I am using any custom parameters for deep-linking- I think not because the LMS stores the link to the tool provided by me, but when the course is launched by the LMS I get a kind of id from the LMS and I use it to launch the course.

For all my testing purposes I have used Moodle. For custom parameters, what I would do is may be call some endpoint to store those parameters somewhere just before the deeplinking request and retrieve them and use it when the course is launched -- dont know if this helps.. but may I ask why do you need custom parameters.

@siddrcrelias
Copy link

Here are some screenshots for Deeplinking,
On Moodle/LMS what we do is we create a course shell, with minimal data and then link our content to it
image

upon clicking Select Content we show a UI which has like checkboxes and shows courses and the submit button

image

image

submitDeeplinkRequest
image

On the Server

@Post()
 async deeplink(
   @Req() req: Request,
   @Res() res: Response,
   @Body() deeplinkDto: DeepLinkingDto,
 ) {
   try {
     if (
       Array.isArray(deeplinkDto.courses) &&
       deeplinkDto.courses.length > 0
     ) {
       const token = res.locals.token as Token;
       const deepLinkedCourses = deeplinkDto.courses.map((course) => {
         return {
           type: 'ltiResourceLink',
           title: course.courseName,
           custom: {
             name: course.courseName,
             value: course.courseId,
           },
         };
       });
       const deepLinkingMessage =
         await lti.DeepLinking.createDeepLinkingMessage(
           token,
           deepLinkedCourses,
           { message: 'Successfully Registered' },
         );
       const responseJson = {
         deepLinkingMessage,
         lmsEndpoint:
           token.platformContext.deepLinkingSettings.deep_link_return_url,
       };
       if (deepLinkingMessage) res.send(responseJson);
       else throw new Error('No Deep Linking Message created.');
     } else throw new Error('No Courses selected for deep linking.');
   } catch (err) {
     console.log(`${err.message} - ${err.stack}`);
     res.status(500).send(err.message);
   }
 }

We make a JSON message using lti.DeepLinking.createDeepLinkingMessage and return back to the deep linking ui component.
Finally, then my deep linking UI component submits it to the LMS and voila!

image

@tomitrescak
Copy link
Author

tomitrescak commented Oct 24, 2024

Hello, this is very helpful and amazing article! I have a couple of questions:

  • when I enable ltiaas: true my whole app breaks as the ltik is not somehow generated. what did you do to make it work? (I made it work by parsing res.locals.ltik)
  • do you have a sceeenshot of config of the deeplinking config, I am mainly interested in custom parameters

Thanks a lot

@siddrcrelias
Copy link

siddrcrelias commented Oct 24, 2024

Custom Parameters I did not use but I think this is something like key value pair seperated with a ; or a & or something - this again might be LMS dependent, because I remember seeing some websites on the internet mentioning this since deep linking request originates from the LMS ( essentially from its LTI deeplinking module ) first then send it out to the deeplinking URL
I remember seeing something like KEY1=VALUE&KEY2=VALUE2 , I don't know I am just shooting in the dark there. I presume since it says custom, it should work in a simple intuitive way.

Here is the screenshot of my config for your reference
Screenshot 2024-10-24 at 15 22 17

Screenshot 2024-10-24 at 15 17 01

..also thanks for the first point highlight..I will try it out...

@KetulCodiste
Copy link

@siddrcrelias I read your medium article. Can you please tell me how to get the LTI_key. I am using node js with express. I am currently using the following code to create the public and private key to get the jwk:

const crypto = require("crypto");
const fs = require("fs");

const generateKeys = () => {
	// Generate RSA key pair
	const { privateKey, publicKey } = crypto.generateKeyPairSync("rsa", {
		modulusLength: 2048,
		publicKeyEncoding: {
			type: "spki",
			format: "pem",
		},
		privateKeyEncoding: {
			type: "pkcs8",
			format: "pem",
		},
	});

	// Generate a key ID
	const keyId = crypto.randomBytes(16).toString("hex");

	// Convert public key to base64url format for JWK
	const pubKeyBuffer = Buffer.from(publicKey);
	const modulus = pubKeyBuffer
		.toString("base64")
		.replace(/\+/g, "-")
		.replace(/\//g, "_")
		.replace(/=/g, "");

	// Create JWK
	const jwk = {
		kty: "RSA",
		kid: keyId,
		n: modulus,
		e: "AQAB",
		alg: "RS256",
		use: "sig",
	};

	// Save keys to files
	fs.writeFileSync("private.key", privateKey);
	fs.writeFileSync("public.key", publicKey);
	fs.writeFileSync("jwk.json", JSON.stringify(jwk, null, 2));

	console.log("Keys generated successfully!");
	console.log("\nYour JWK for Canvas configuration:");
	console.log(JSON.stringify(jwk, null, 2));
};

generateKeys();

It will create the 3 files in the root folder. public.key and private.key and jwk.json. I want to use Canvas LMS and there is an option while creating the the developer key, Public JWK. I am using this generated jwk to set there and using the public file at line in lti.setup

staticPath: path.join(__dirname, "public"),

using the following code to setup the lti and register the platform.

const encryptionKey = crypto
      .createHash("sha256")
      .update(process.env.ENCRYPTION_KEY || "your-secure-encryption-key")
      .digest("base64")
      .substr(0, 32);

    console.log("Using encryption key:", encryptionKey);
    console.log("path.join(__dirname, public),", path.join(__dirname, "public"))
    await lti.setup(
      encryptionKey, // Move private key to env variable
      {
        url: process.env.MONGODB_URI, // Move MongoDB URI to env variable
        connection: {
          retryWrites: true,
          w: "majority",
        },
      },
      {
        staticPath: path.join(__dirname, "public"),
        cookies: {
          secure: true,
          sameSite: "None",
        },
        devMode: true,
        tokenMaxAge: 60,
        hostName: new URL(process.env.TOOL_URL).hostname,
        proxy: true,
        serverAddon: (app) => {
          app.enable("trust proxy");
          return app;
        },
      }
    );

    const app = lti.app;


    const port = process.env.PORT || 4001;
    await lti.deploy({ port });

    await lti.registerPlatform({
      url: process.env.PLATFORM_URL,
      name: "Canvas LMS",
      clientId: process.env.CLIENT_ID,
      authenticationEndpoint: `${process.env.PLATFORM_URL}/api/lti/authorize_redirect`,
      accesstokenEndpoint: `${process.env.PLATFORM_URL}/login/oauth2/token`,
      authConfig: {
        method: "JWK_SET",
        key: `${process.env.PLATFORM_URL}/api/lti/security/jwks`,
      },
      
    });

while doing the deeplinking with the following code:

const deepLinkingMessage = await lti.DeepLinking.createDeepLinkingMessage(
          token,
          contentItems,
          {
            message: "Quiz successfully selected!",
            log: "Quiz selection completed successfully",
            errlog: "Error occurred during quiz selection",
          }
        );

, when i submit the form with the deepLinkingMessage i got, i am getting the following error in canvas lms browser:

{
"errors": {
"jwt": [
{
"attribute": "jwt",
"type": "JWT verification failure",
"message": "JWT verification failure"
}
]
}
}

when i verified the deepLinkingMessage in jwt.io, it is saying that it is a "Invalid Signature". I am not sure at which step I am going wrong

@siddrcrelias
Copy link

siddrcrelias commented Nov 18, 2024

@KetulCodiste hello, ok tbh I did not create the keys myself ,this library itself creates it
and this is done in this repo itself. I think the url is /lti/keys

As to regarding your question, I am not sure I understand you correctly.
LTI_KEY is the env variable which we set and you can decide its value to be whatever you wish,
and if you mean ltik, then you can check res.locals as well like the screenshot below

image

And in all fairness I don't think you need to do anything regarding the keys other than using the given url, coz this library itself generates the key ( I don't know how it does it tbh ) , then we set the urls inside the LMS and as soon you launch a course or send a deep linking LTI request from LMS , LMS sends some signed data to your LTI1.3 application and then LTI middleware verifies it upon receiving and that's it you take control once the request is validated.

Now the LTI request can be either a deep linking request or a course launch request. And all this is based of Oauthv2 so..🤷🏻‍♂️

@KetulCodiste
Copy link

Thank you for your response, @siddrcrelias . You're right—we don't need to do anything to retrieve the JWK keys, as LTI provides them. I’ve successfully implemented deep linking.

Now, I’m exploring how to serve the frontend. My current setup includes:

Backend: Node.js with Express.
Frontend: React.js.
The frontend and backend are hosted on different domains.

The website includes several CRUD operations, and I want to replicate those functionalities when deep linking is launched. I have a few questions regarding this:

1. LTI Integration with Existing Backend:
Is it possible to integrate LTI into my existing Node.js backend with Express, or would I need a different setup?

2. Serving the Frontend:
Should I create separate templates of my React.js frontend web pages in the backend for LTI use, or can I use the existing React.js code directly?

3. Authentication with JWT:
On my website, I’m managing authentication using JWT tokens. Can I use JWT for authentication when handling deep linking? If so, how can this be achieved?

I’d appreciate your insights on these points!

@siddrcrelias
Copy link

siddrcrelias commented Nov 23, 2024

Hi @KetulCodiste , sorry for replying late...was busy with some stuff..
Let me breakdown my response for this ...

  1. LTI Integration with Existing Backend:
    Is it possible to integrate LTI into my existing Node.js backend with Express, or would I need a different setup?

Well to be honest with you you can do this in the same backend, you will need a different middleware/filter to intercept LTI requests and validate and pass it ahead.
What I did was :

  1. My company offers content via Custom LMS (lets call this A) and via LTI (lets call this B)
  2. I made a B which works as a LTI v1.3 content provider and handles only the LTI logic, LTI auth logic, has code related only to LTI stuff
  3. That way I can keep adding features to A which have nothing to do with B
  4. App A has a different auth logic
  5. App B auth logic is handled by LTI

In my head I like to keep things separate , till there is a pressing need to merge them or if I see there is a lot of duplication.
Here I have kept them seperate coz they both cater to different audiences
A caters to audience who use A as a LMS
B caters to audience who already have a LMS and want just our content on their LMS.

  1. Serving the Frontend:
    Should I create separate templates of my React.js frontend web pages in the backend for LTI use, or can I use the existing React.js code directly?

Here again I made a separate FE but I used the same components from A, just to minimize my work. I did not use the react files given in ltijs coz they are suppose to be a kind of reference. I kept a simple html in my BE code which basically redirects to the FE project and I did this because I wanted to de-couple my FE and BE and I did not want to do a build of the FE each time , replace it in the BE code

  1. Authentication with JWT:
    On my website, I’m managing authentication using JWT tokens. Can I use JWT for authentication when handling deep linking? If so, how can this be achieved?

I am sorry I do not quite follow this, the entire auth part is already handled by LTIjs itself, you don't need to do anything other than figure out your spot in all this to how to place your content. When you deep link, you deep link a course from your server or infra to the customer's LMS.
When the customer clicks on that link in their LMS , it then generates a LTI course launch request which has its own signed data and is sent to your LTI server, at this point ltijs uses the keys to check if all data is valid does the oauthv2 mumbo jumbo and forwards the request, this is the spot where you come in and serve your content that's it , all the heavy lifting auth. is already done, there is no need of passing one more JWT tbh :)

Your present website must be generating JWT after validating the user based on some checks and that mechanism but LTIjs already does that for you, that is why we exchange keys , certs and token endpoints in the beginning so that LTIjs can do the heavy lifting :)

I hope the answers help :)

@siddrcrelias
Copy link

hey @tomitrescak was your issue resolved ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

4 participants