Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion authentication/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,18 @@

from django.urls import re_path

from authentication.views import CustomLoginView, CustomLogoutView
from authentication.views import (
CustomLoginView,
CustomLogoutView,
lti_auth,
lti_login,
public_keyset,
)

urlpatterns = [
re_path(r"^logout", CustomLogoutView.as_view(), name="logout"),
re_path(r"^login", CustomLoginView.as_view(), name="login"),
re_path(r"^lti_login", lti_login, name="lti_login"),
re_path(r"^lti_auth", lti_auth, name="lti_auth"),
re_path(r"^lti_jwks", public_keyset, name="lti_jwks"),
]
200 changes: 199 additions & 1 deletion authentication/views.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
# ruff: noqa: ARG001, E501, TD002, TD003, FIX002, ERA001, SIM115, PTH123, D401, T201, RET503
"""Authentication views"""

import logging
from urllib.parse import urljoin

from django.contrib.auth import logout
from django.shortcuts import redirect
from django.core.cache import caches
from django.core.exceptions import PermissionDenied
from django.http import JsonResponse
from django.shortcuts import redirect, render
from django.utils.http import url_has_allowed_host_and_scheme, urlencode
from django.utils.text import slugify
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from lti_consumer.data import Lti1p3LaunchData

from main import settings
from main.middleware.apisix_user import ApisixUserMiddleware, decode_apisix_headers
Expand Down Expand Up @@ -120,3 +126,195 @@ def get(
profile.save()

return redirect(redirect_url)


# lti_oidc_url = 'localhost:8000/oidc/initiate'
# lti_launch_url = 'localhost:8000/launch'
# client_id = 'learn-jupyter-notebooks'
# deployment_id = '0cb94445-2066-4295-9c03-4bfdc1d8aacb'

# lti_oidc_url = "http://127.0.0.1:8000"
# lti_launch_url = "http://127.0.0.1:8000"

lti_oidc_url = "http://127.0.0.1:8000/hub/lti13/oauth_login"
# The launch URL includes the next param, but all other bits of state are passed via launch request claims
lti_launch_url = (
"http://127.0.0.1:8000/hub/lti13/oauth_callback?next=/hub/spawn/test-user-id"
)
redirect_uris = ["http://127.0.0.1:8000/hub/lti13/oauth_callback", lti_launch_url]
client_id = "learn-jupyter-notebooks"
deployment_id = "cb5fd9ae5c92eddbc5054e27fa010c5478d2f7c3"
platform_private_key_id = "JZOHScC4BQ"
platform_public_key = open("platform_id_rsa.pub").read()
platform_private_key = open("platform_id_rsa").read()
rsa_key = platform_private_key
rsa_key_id = platform_private_key_id
iss = "http://api.open.odl.local"
tool_public_key = None # PEM tool public key, not required just to launch LTI

"""
# lti1p3platform - the only platform impl I could find packaged. Not well supported, no GH.
# Consider using https://github.com/academic-innovation/django-lti for client, though it's probably not gonna work OOTB
# Below is the OpenEdX impl. It's technically request agnostic, so we could vendor it in and just use pieces we care about
# https://github.com/openedx/xblock-lti-consumer/blob/92ea78f9fee5aa8551511db5f0587d1673e25159/lti_consumer/lti_1p3/README.md?plain=1
Render a view which starts an LTI negotiation
Take a notebook link as the next parameter
At the end of the negotiation, we want to end up on a page that shows our learn logged in user's info at minimum

"""


def lti_login(
request,
*args,
**kwargs,
):
"""
Render form taking relevant bits of info to start LTI negotiation
"""
if request.method == "GET":
# Render form for LTI preflight with image and course_name fields
return render(request, "lti_preflight_form.html")
elif request.method == "POST":
if request.user and request.user.is_authenticated:
cache = caches["redis"]
cache.set(
f"{request.user.global_id}:lti_image",
request.POST.get("image"),
timeout=30,
)
cache.set(
f"{request.user.global_id}:lti_course_name",
request.POST.get("course_name"),
timeout=30,
)
else:
raise PermissionDenied
return lti_preflight_request(request)


@csrf_exempt
def lti_auth(
request,
*args,
**kwargs,
):
"""
Perform LTI negotiation and redirect to notebook URL
"""
return lti_launch_endpoint(request)


def _get_lti1p3_consumer():
"""
Returns an configured instance of LTI consumer.
"""
from lti_consumer.lti_1p3.consumer import LtiConsumer1p3

return LtiConsumer1p3(
# Tool urls
lti_oidc_url=lti_oidc_url,
lti_launch_url=lti_launch_url,
redirect_uris=redirect_uris,
# Platform and deployment configuration
iss=iss, # Whatever the host is for the platform?
client_id=client_id,
deployment_id=deployment_id,
# Platform key
rsa_key=rsa_key,
rsa_key_id=rsa_key_id,
# Tool key
# tool_key=tool_public_key,
)


def public_keyset(request):
"""
Return LTI Public Keyset url.

This endpoint must be configured in the tool.
"""

return JsonResponse(
_get_lti1p3_consumer().get_public_keyset(), content_type="application/json"
)


def lti_preflight_request(request):
"""
Endpoint that'll render the initial OIDC authorization request form
and submit it to the tool.

The platform needs to know the tool OIDC endpoint.
"""
# TODO: This doesn't work w/ anonymous users as they don't have a global_id. Shouldn't happen in practice, so just bail
user = request.user
if user and not user.is_authenticated:
raise PermissionDenied
lti_consumer = _get_lti1p3_consumer()
# AFAICT, config_id and resource_link_id are pretty much used to get stuff out of cache later
launch_data = Lti1p3LaunchData(
user_id=user.global_id,
user_role="admin" if user.is_superuser else "student",
config_id=client_id,
resource_link_id="link_id",
)

# This template should render a simple redirection to the URL
# provided by the context through the `oidc_url` key above.
# This can also be a redirect.
url = lti_consumer.prepare_preflight_url(launch_data) + urlencode(
{"next": "http://127.0.0.1:8000/user/test-user-id/"}
)
return redirect(url)


def lti_launch_endpoint(request):
"""
Platform endpoint that'll receive OIDC login request variables and generate launch request.
"""
lti_consumer = _get_lti1p3_consumer()
context = {}
user = request.user
if user and not user.is_authenticated:
raise PermissionDenied

# Required user claim data
# This will need to be solved.
lti_consumer.set_user_data(
user_id=user.global_id,
# Pass django user role to library
role="admin" if user.is_superuser else "student",
)
# TODO: Not sure what this is used for but it's required.
# It doesn't seem to affect the basic test, but we should adjust it later
lti_consumer.set_resource_link_claim("link_id")

# We can add any custom parameters we want to the launch request here.
cache = caches["redis"]
image = cache.get(f"{user.global_id}:lti_image")
course = cache.get(f"{request.user.global_id}:lti_course_name")
if image and course:
lti_consumer.set_custom_parameters(
{
"image": image,
"course_name": course,
}
)
else:
print("No cached information found for current user")

payload = request.POST if request.method == "POST" else request.GET
context.update(
{
"preflight_response": dict(payload),
"launch_request": lti_consumer.generate_launch_request(
preflight_response=payload
),
}
)

context.update({"launch_url": lti_consumer.launch_url})
# This template should render a form, and then submit it to the tool's launch URL, as
# described in http://www.imsglobal.org/spec/lti/v1p3/#lti-message-general-details
return render(request, "lti_launch_request_form.html", context)
4 changes: 2 additions & 2 deletions env/backend.env
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ CELERY_TASK_ALWAYS_EAGER=False
CELERY_WORKER_CONCURRENCY=8

# local hostname shenanigans
CORS_ALLOWED_ORIGINS='["http://open.odl.local:8062"]'
CSRF_TRUSTED_ORIGINS='["http://open.odl.local:8062", "http://api.open.odl.local:8063"]'
CORS_ALLOWED_ORIGINS='["http://open.odl.local:8062", "https://saltire.lti.app"]'
CSRF_TRUSTED_ORIGINS='["http://open.odl.local:8062", "http://api.open.odl.local:8063", "https://saltire.lti.app"]'
CSRF_COOKIE_DOMAIN=open.odl.local
CSRF_COOKIE_SECURE=False
MITOL_COOKIE_DOMAIN=open.odl.local
Expand Down
2 changes: 2 additions & 0 deletions main/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -854,3 +854,5 @@ def get_all_config_keys():
OPENTELEMETRY_TRACES_BATCH_SIZE = get_int("OPENTELEMETRY_TRACES_BATCH_SIZE", 512)
OPENTELEMETRY_EXPORT_TIMEOUT_MS = get_int("OPENTELEMETRY_EXPORT_TIMEOUT_MS", 5000)
CANVAS_TUTORBOT_FOLDER = get_string("CANVAS_TUTORBOT_FOLDER", "web_resources/ai/tutor/")
# Required while geenrating the LTI launch message
PLATFORM_NAME = "mit-learn"
Loading
Loading