From 003648c7e7aa2ab5cf23da57ed2d0c6bc1099e51 Mon Sep 17 00:00:00 2001 From: Zachary Hancock Date: Tue, 10 Jan 2023 15:46:59 -0500 Subject: [PATCH] feat: support separate different base urls for UI flow and API callbacks (#319) Allows independent configuration of the base URL used for LTI API requests and LTI browser flow. This primarily aids local development because we no longer have to tunnel the entire LMS in order to test against the IMS tools. --- README.rst | 40 ++++++++++++------------------ lti_consumer/lti_1p3/README.md | 2 +- lti_consumer/models.py | 6 ++--- lti_consumer/utils.py | 45 ++++++++++++++++++++++------------ 4 files changed, 49 insertions(+), 44 deletions(-) diff --git a/README.rst b/README.rst index 92a28b85..2ec4f4a7 100644 --- a/README.rst +++ b/README.rst @@ -184,39 +184,36 @@ needs to know the LMS's one. Instructions: -1. Set up a local tunnel tunneling the LMS (using `ngrok` or a similar tool) to get a URL accessible from the internet. -2. Create a new course, and add the `lti_consumer` block to the advanced modules list. -3. In the course, create a new unit and add the LTI block. +#. Set up a local tunnel (using `ngrok` or a similar tool) to get a URL accessible from the internet. +#. Add the following settings to `edx-platform/lms/envs/private.py` and `edx-platform/cms/envs/private.py`: + + * LTI_BASE="http://localhost:18000" + * LTI_API_BASE="http://.ngrok.io" + +#. Create a new course, and add the `lti_consumer` block to the advanced modules list. +#. In the course, create a new unit and add the LTI block. * Set ``LTI Version`` to ``LTI 1.3``. * Set the ``Tool Launch URL`` to ``https://lti-ri.imsglobal.org/lti/tools/`` -4. In Studio, you'll see a few parameters being displayed in the preview: +#. In Studio, you'll see a few parameters being displayed in the preview: .. code:: Client ID: f0532860-cb34-47a9-b16c-53deb077d4de Deployment ID: 1 # Note that these are LMS URLS - Keyset URL: http://localhost:18000/api/lti_consumer/v1/public_keysets/block-v1:OpenCraft+LTI101+2020_T2+type@lti_consumer+block@efc55c7abb87430883433bfafb83f054 - Access Token URL: http://localhost:18000/api/lti_consumer/v1/token/block-v1:OpenCraft+LTI101+2020_T2+type@lti_consumer+block@efc55c7abb87430883433bfafb83f054 + Keyset URL: http://1234.ngrok.io/api/lti_consumer/v1/public_keysets/88e45ecbd-7cce-4fa0-9537-23e9f7288ad9 + Access Token URL: http://1234.ngrok.io/api/lti_consumer/v1/token/8e45ecbd-7cce-4fa0-9537-23e9f7288ad9 OIDC Callback URL: http://localhost:18000/api/lti_consumer/v1/launch/ -5. Add the tunnel URL to the each of these URLs as it'll need to be accessed by the tool (hosted externally). - -.. code:: - - # This is /api/lti_consumer/v1/public_keysets/ - https://647dd2e1.ngrok.io/api/lti_consumer/v1/public_keysets/block-v1:OpenCraft+LTI101+2020_T2+type@lti_consumer+block@996c72b16070434098bc598bd7d6dbde - - -6. Set up a tool in the IMS Global reference implementation (https://lti-ri.imsglobal.org/lti/tools/). +#. Set up a tool in the IMS Global reference implementation (https://lti-ri.imsglobal.org/lti/tools/). * Click on ``Add tool`` at the top of the page (https://lti-ri.imsglobal.org/lti/tools). * Add the parameters and URLs provided by the block, and generate a private key on https://lti-ri.imsglobal.org/keygen/index and paste it there (don't close the tab, you'll need the public key later). -7. Go back to Studio, and edit the block adding its settings (you'll find them by scrolling down https://lti-ri.imsglobal.org/lti/tools/ until you find the tool you just created): +#. Go back to Studio, and edit the block adding its settings (you'll find them by scrolling down https://lti-ri.imsglobal.org/lti/tools/ until you find the tool you just created): .. code:: @@ -224,15 +221,8 @@ Instructions: Tool Initiate Login URL: https://lti-ri.imsglobal.org/lti/tools/[tool_id]/login_initiations Tool Public key: Public key from key page. -8. Publish block, log into LMS and navigate to the LTI block page. -9. Click ``Send Request`` and verify that the LTI launch was successful. - -.. admonition:: Testing using ``ngrok`` - - When launching LTI 1.3 requests through ``ngrok``, make sure your LMS is serving session cookies marked as - ``Secure`` and with the ``SameSite`` attribute set to ``None``. You can do this by changing ``SESSION_COOKIE_SECURE: true`` - and ``DCS_SESSION_COOKIE_SAMESITE: None`` in your ``lms.yml`` configuration files. Note that this will break logins - for locally accessed URLs in the devstack. +#. Publish block, log into LMS and navigate to the LTI block page. +#. Click ``Send Request`` and verify that the LTI launch was successful. LTI Advantage Features diff --git a/lti_consumer/lti_1p3/README.md b/lti_consumer/lti_1p3/README.md index 06577f3d..90b6e27f 100644 --- a/lti_consumer/lti_1p3/README.md +++ b/lti_consumer/lti_1p3/README.md @@ -54,7 +54,7 @@ def _get_lti1p3_consumer(): lti_oidc_url=lti_1p3_oidc_url, lti_launch_url=lti_1p3_launch_url, # Platform and deployment configuration - iss=get_lms_base(), + iss=get_lti_api_base(), client_id=lti_1p3_client_id, deployment_id="1", # Platform key diff --git a/lti_consumer/models.py b/lti_consumer/models.py index 4c6ebc42..dce26c07 100644 --- a/lti_consumer/models.py +++ b/lti_consumer/models.py @@ -24,7 +24,7 @@ from lti_consumer.lti_1p3.key_handlers import PlatformKeyHandler from lti_consumer.plugin import compat from lti_consumer.utils import ( - get_lms_base, + get_lti_api_base, get_lti_ags_lineitems_url, get_lti_deeplinking_response_url, get_lti_nrps_context_membership_url, @@ -488,7 +488,7 @@ def _get_lti_1p3_consumer(self): block = compat.load_enough_xblock(self.location) consumer = consumer_class( - iss=get_lms_base(), + iss=get_lti_api_base(), lti_oidc_url=block.lti_1p3_oidc_url, lti_launch_url=block.lti_1p3_launch_url, client_id=self.lti_1p3_client_id, @@ -504,7 +504,7 @@ def _get_lti_1p3_consumer(self): ) elif self.config_store == self.CONFIG_ON_DB: consumer = consumer_class( - iss=get_lms_base(), + iss=get_lti_api_base(), lti_oidc_url=self.lti_1p3_oidc_url, lti_launch_url=self.lti_1p3_launch_url, client_id=self.lti_1p3_client_id, diff --git a/lti_consumer/utils.py b/lti_consumer/utils.py index 350e51c2..e7a3e75d 100644 --- a/lti_consumer/utils.py +++ b/lti_consumer/utils.py @@ -26,22 +26,37 @@ def _(text): return text -def get_lms_base(): +def get_lti_api_base(): """ - Returns LMS base url to be used as issuer on OAuth2 flows - and in various LTI URLs. For local testing it is often necessary - to override the normal LMS base with a proxy such as ngrok, use - the setting LTI_LMS_BASE_URL_OVERRIDE in your LMS settings if - necessary. + Returns base url to be used as issuer on OAuth2 flows + and in various LTI API calls. If LTI_API_BASE is set this will + override the default LTI_BASE url for these URLs only. TODO: This needs to be improved and account for Open edX sites and organizations. One possible improvement is to use `contentstore.get_lms_link_for_item` and strip the base domain name. """ - if hasattr(settings, 'LTI_LMS_BASE_URL_OVERRIDE'): - return settings.LTI_LMS_BASE_URL_OVERRIDE + if hasattr(settings, 'LTI_API_BASE'): + return settings.LTI_API_BASE + elif hasattr(settings, 'LTI_BASE'): + return settings.LTI_BASE else: + # Eventually we should move away from supporting this setting as it is incorrect + # in applications that are not the LMS. Keeping this around for backward support. + return settings.LMS_ROOT_URL + + +def get_lti_view_base(): + """ + Returns base url to be used when generating view and redirect urls + as part of the LTI launch flow. + """ + if hasattr(settings, 'LTI_BASE'): + return settings.LTI_BASE + else: + # Eventually we should move away from supporting this setting as it is incorrect + # in applications that are not the LMS. Keeping this around for backward support. return settings.LMS_ROOT_URL @@ -52,7 +67,7 @@ def get_lms_lti_keyset_link(config_id): :param config_id: the config_id of the LtiConfiguration object """ return "{lms_base}/api/lti_consumer/v1/public_keysets/{config_id}".format( - lms_base=get_lms_base(), + lms_base=get_lti_api_base(), config_id=str(config_id), ) @@ -64,7 +79,7 @@ def get_lms_lti_launch_link(): :param location: the location of the block """ return "{lms_base}/api/lti_consumer/v1/launch/".format( - lms_base=get_lms_base(), + lms_base=get_lti_view_base(), ) @@ -75,7 +90,7 @@ def get_lms_lti_access_token_link(config_id): :param config_id: the config_id of the LtiConfiguration object """ return "{lms_base}/api/lti_consumer/v1/token/{config_id}".format( - lms_base=get_lms_base(), + lms_base=get_lti_api_base(), config_id=str(config_id), ) @@ -90,7 +105,7 @@ def get_lti_ags_lineitems_url(lti_config_id, lineitem_id=None): """ url = "{lms_base}/api/lti_consumer/v1/lti/{lti_config_id}/lti-ags".format( - lms_base=get_lms_base(), + lms_base=get_lti_api_base(), lti_config_id=str(lti_config_id), ) @@ -107,7 +122,7 @@ def get_lti_deeplinking_response_url(lti_config_id): :param lti_config_id: LTI configuration id """ return "{lms_base}/api/lti_consumer/v1/lti/{lti_config_id}/lti-dl/response".format( - lms_base=get_lms_base(), + lms_base=get_lti_api_base(), lti_config_id=str(lti_config_id), ) @@ -120,7 +135,7 @@ def get_lti_deeplinking_content_url(lti_config_id, launch_data): :param launch_data: (lti_consumer.data.Lti1p3LaunchData): a class containing data necessary for an LTI 1.3 launch """ url = "{lms_base}/api/lti_consumer/v1/lti/{lti_config_id}/lti-dl/content".format( - lms_base=get_lms_base(), + lms_base=get_lti_api_base(), lti_config_id=str(lti_config_id), ) url += "?" @@ -142,7 +157,7 @@ def get_lti_nrps_context_membership_url(lti_config_id): """ return "{lms_base}/api/lti_consumer/v1/lti/{lti_config_id}/memberships".format( - lms_base=get_lms_base(), + lms_base=get_lti_api_base(), lti_config_id=str(lti_config_id), )