From 003648c7e7aa2ab5cf23da57ed2d0c6bc1099e51 Mon Sep 17 00:00:00 2001
From: Zachary Hancock <zhancock@edx.org>
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 92a28b8..2ec4f4a 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://<your_ngrok>.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 <LMS_URL>/api/lti_consumer/v1/public_keysets/<BLOCK_LOCATION>
-    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 06577f3..90b6e27 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 4c6ebc4..dce26c0 100644
--- a/lti_consumer/models.py
+++ b/lti_consumer/models.py
@@ -24,7 +24,7 @@ from lti_consumer.lti_1p3.consumer import LtiAdvantageConsumer, LtiProctoringCon
 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 @@ class LtiConfiguration(models.Model):
             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 @@ class LtiConfiguration(models.Model):
             )
         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 350e51c..e7a3e75 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),
     )
 
-- 
GitLab