Skip to content
Snippets Groups Projects
Unverified Commit 003648c7 authored by Zachary Hancock's avatar Zachary Hancock Committed by GitHub
Browse files

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.
parent f3eca6be
No related branches found
No related tags found
No related merge requests found
...@@ -184,39 +184,36 @@ needs to know the LMS's one. ...@@ -184,39 +184,36 @@ needs to know the LMS's one.
Instructions: Instructions:
1. Set up a local tunnel tunneling the LMS (using `ngrok` or a similar tool) to get a URL accessible from the internet. #. Set up a local tunnel (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. #. Add the following settings to `edx-platform/lms/envs/private.py` and `edx-platform/cms/envs/private.py`:
3. In the course, create a new unit and add the LTI block.
* 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 ``LTI Version`` to ``LTI 1.3``.
* Set the ``Tool Launch URL`` to ``https://lti-ri.imsglobal.org/lti/tools/`` * 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:: .. code::
Client ID: f0532860-cb34-47a9-b16c-53deb077d4de Client ID: f0532860-cb34-47a9-b16c-53deb077d4de
Deployment ID: 1 Deployment ID: 1
# Note that these are LMS URLS # 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 Keyset URL: http://1234.ngrok.io/api/lti_consumer/v1/public_keysets/88e45ecbd-7cce-4fa0-9537-23e9f7288ad9
Access Token URL: http://localhost:18000/api/lti_consumer/v1/token/block-v1:OpenCraft+LTI101+2020_T2+type@lti_consumer+block@efc55c7abb87430883433bfafb83f054 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/ 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). #. Set up a tool in the IMS Global reference implementation (https://lti-ri.imsglobal.org/lti/tools/).
.. 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/).
* Click on ``Add tool`` at the top of the page (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). * 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:: .. code::
...@@ -224,15 +221,8 @@ Instructions: ...@@ -224,15 +221,8 @@ Instructions:
Tool Initiate Login URL: https://lti-ri.imsglobal.org/lti/tools/[tool_id]/login_initiations Tool Initiate Login URL: https://lti-ri.imsglobal.org/lti/tools/[tool_id]/login_initiations
Tool Public key: Public key from key page. Tool Public key: Public key from key page.
8. Publish block, log into LMS and navigate to the LTI block page. #. Publish block, log into LMS and navigate to the LTI block page.
9. Click ``Send Request`` and verify that the LTI launch was successful. #. 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.
LTI Advantage Features LTI Advantage Features
......
...@@ -54,7 +54,7 @@ def _get_lti1p3_consumer(): ...@@ -54,7 +54,7 @@ def _get_lti1p3_consumer():
lti_oidc_url=lti_1p3_oidc_url, lti_oidc_url=lti_1p3_oidc_url,
lti_launch_url=lti_1p3_launch_url, lti_launch_url=lti_1p3_launch_url,
# Platform and deployment configuration # Platform and deployment configuration
iss=get_lms_base(), iss=get_lti_api_base(),
client_id=lti_1p3_client_id, client_id=lti_1p3_client_id,
deployment_id="1", deployment_id="1",
# Platform key # Platform key
......
...@@ -24,7 +24,7 @@ from lti_consumer.lti_1p3.consumer import LtiAdvantageConsumer, LtiProctoringCon ...@@ -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.lti_1p3.key_handlers import PlatformKeyHandler
from lti_consumer.plugin import compat from lti_consumer.plugin import compat
from lti_consumer.utils import ( from lti_consumer.utils import (
get_lms_base, get_lti_api_base,
get_lti_ags_lineitems_url, get_lti_ags_lineitems_url,
get_lti_deeplinking_response_url, get_lti_deeplinking_response_url,
get_lti_nrps_context_membership_url, get_lti_nrps_context_membership_url,
...@@ -488,7 +488,7 @@ class LtiConfiguration(models.Model): ...@@ -488,7 +488,7 @@ class LtiConfiguration(models.Model):
block = compat.load_enough_xblock(self.location) block = compat.load_enough_xblock(self.location)
consumer = consumer_class( consumer = consumer_class(
iss=get_lms_base(), iss=get_lti_api_base(),
lti_oidc_url=block.lti_1p3_oidc_url, lti_oidc_url=block.lti_1p3_oidc_url,
lti_launch_url=block.lti_1p3_launch_url, lti_launch_url=block.lti_1p3_launch_url,
client_id=self.lti_1p3_client_id, client_id=self.lti_1p3_client_id,
...@@ -504,7 +504,7 @@ class LtiConfiguration(models.Model): ...@@ -504,7 +504,7 @@ class LtiConfiguration(models.Model):
) )
elif self.config_store == self.CONFIG_ON_DB: elif self.config_store == self.CONFIG_ON_DB:
consumer = consumer_class( consumer = consumer_class(
iss=get_lms_base(), iss=get_lti_api_base(),
lti_oidc_url=self.lti_1p3_oidc_url, lti_oidc_url=self.lti_1p3_oidc_url,
lti_launch_url=self.lti_1p3_launch_url, lti_launch_url=self.lti_1p3_launch_url,
client_id=self.lti_1p3_client_id, client_id=self.lti_1p3_client_id,
......
...@@ -26,22 +26,37 @@ def _(text): ...@@ -26,22 +26,37 @@ def _(text):
return text return text
def get_lms_base(): def get_lti_api_base():
""" """
Returns LMS base url to be used as issuer on OAuth2 flows Returns base url to be used as issuer on OAuth2 flows
and in various LTI URLs. For local testing it is often necessary and in various LTI API calls. If LTI_API_BASE is set this will
to override the normal LMS base with a proxy such as ngrok, use override the default LTI_BASE url for these URLs only.
the setting LTI_LMS_BASE_URL_OVERRIDE in your LMS settings if
necessary.
TODO: This needs to be improved and account for Open edX sites and TODO: This needs to be improved and account for Open edX sites and
organizations. organizations.
One possible improvement is to use `contentstore.get_lms_link_for_item` One possible improvement is to use `contentstore.get_lms_link_for_item`
and strip the base domain name. and strip the base domain name.
""" """
if hasattr(settings, 'LTI_LMS_BASE_URL_OVERRIDE'): if hasattr(settings, 'LTI_API_BASE'):
return settings.LTI_LMS_BASE_URL_OVERRIDE return settings.LTI_API_BASE
elif hasattr(settings, 'LTI_BASE'):
return settings.LTI_BASE
else: 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 return settings.LMS_ROOT_URL
...@@ -52,7 +67,7 @@ def get_lms_lti_keyset_link(config_id): ...@@ -52,7 +67,7 @@ def get_lms_lti_keyset_link(config_id):
:param config_id: the config_id of the LtiConfiguration object :param config_id: the config_id of the LtiConfiguration object
""" """
return "{lms_base}/api/lti_consumer/v1/public_keysets/{config_id}".format( 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), config_id=str(config_id),
) )
...@@ -64,7 +79,7 @@ def get_lms_lti_launch_link(): ...@@ -64,7 +79,7 @@ def get_lms_lti_launch_link():
:param location: the location of the block :param location: the location of the block
""" """
return "{lms_base}/api/lti_consumer/v1/launch/".format( 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): ...@@ -75,7 +90,7 @@ def get_lms_lti_access_token_link(config_id):
:param config_id: the config_id of the LtiConfiguration object :param config_id: the config_id of the LtiConfiguration object
""" """
return "{lms_base}/api/lti_consumer/v1/token/{config_id}".format( 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), config_id=str(config_id),
) )
...@@ -90,7 +105,7 @@ def get_lti_ags_lineitems_url(lti_config_id, lineitem_id=None): ...@@ -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( 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), lti_config_id=str(lti_config_id),
) )
...@@ -107,7 +122,7 @@ def get_lti_deeplinking_response_url(lti_config_id): ...@@ -107,7 +122,7 @@ def get_lti_deeplinking_response_url(lti_config_id):
:param lti_config_id: LTI configuration id :param lti_config_id: LTI configuration id
""" """
return "{lms_base}/api/lti_consumer/v1/lti/{lti_config_id}/lti-dl/response".format( 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), lti_config_id=str(lti_config_id),
) )
...@@ -120,7 +135,7 @@ def get_lti_deeplinking_content_url(lti_config_id, launch_data): ...@@ -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 :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( 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), lti_config_id=str(lti_config_id),
) )
url += "?" url += "?"
...@@ -142,7 +157,7 @@ def get_lti_nrps_context_membership_url(lti_config_id): ...@@ -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( 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), lti_config_id=str(lti_config_id),
) )
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment