Skip to content

Commit

Permalink
feat: support separate different base urls for UI flow and API callba…
Browse files Browse the repository at this point in the history
…cks (#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.
  • Loading branch information
zacharis278 authored Jan 10, 2023
1 parent f3eca6b commit 003648c
Show file tree
Hide file tree
Showing 4 changed files with 49 additions and 44 deletions.
40 changes: 15 additions & 25 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -184,55 +184,45 @@ 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::
Tool Launch URL: https://lti-ri.imsglobal.org/lti/tools/[tool_id]/launches
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
Expand Down
2 changes: 1 addition & 1 deletion lti_consumer/lti_1p3/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions lti_consumer/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
45 changes: 30 additions & 15 deletions lti_consumer/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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),
)

Expand All @@ -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(),
)


Expand All @@ -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),
)

Expand All @@ -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),
)

Expand All @@ -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),
)

Expand All @@ -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 += "?"
Expand All @@ -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),
)

Expand Down

0 comments on commit 003648c

Please sign in to comment.