This commit is contained in:
Markos Gogoulos
2025-12-30 18:12:49 +02:00
parent 2e57164831
commit 9370706097
4 changed files with 39 additions and 5 deletions

View File

@@ -6,6 +6,7 @@ Provides Django-specific implementations for PyLTI1p3 interfaces
import json import json
import time import time
import uuid
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
import jwt import jwt
@@ -192,6 +193,8 @@ class DjangoServiceConnector(ServiceConnector):
self._access_token_expires = 0 self._access_token_expires = 0
def get_access_token(self, scopes): def get_access_token(self, scopes):
print(f"Requesting access token for scopes: {scopes}")
if self._access_token and time.time() < self._access_token_expires: if self._access_token and time.time() < self._access_token_expires:
return self._access_token return self._access_token
@@ -200,14 +203,19 @@ class DjangoServiceConnector(ServiceConnector):
pem_bytes = jwk_obj.export_to_pem(private_key=True, password=None) pem_bytes = jwk_obj.export_to_pem(private_key=True, password=None)
private_key = serialization.load_pem_private_key(pem_bytes, password=None, backend=default_backend()) private_key = serialization.load_pem_private_key(pem_bytes, password=None, backend=default_backend())
# Use configured audience if available, otherwise auth_token_url
# Moodle expects auth_token_url as audience usually, but some platforms differ
aud = self._registration.get_auth_audience() if self._registration.get_auth_audience() else self._registration.get_auth_token_url()
print(f"Using audience for token request: {aud}")
now = int(time.time()) now = int(time.time())
payload = { payload = {
'iss': self._registration.get_client_id(), 'iss': self._registration.get_client_id(),
'sub': self._registration.get_client_id(), 'sub': self._registration.get_client_id(),
'aud': self._registration.get_auth_token_url(), 'aud': aud,
'iat': now, 'iat': now,
'exp': now + 300, 'exp': now + 300,
'jti': str(time.time()), 'jti': str(uuid.uuid4()),
} }
client_assertion = jwt.encode(payload, private_key, algorithm='RS256', headers={'kid': key_obj.private_key_jwk['kid']}) client_assertion = jwt.encode(payload, private_key, algorithm='RS256', headers={'kid': key_obj.private_key_jwk['kid']})
@@ -220,7 +228,10 @@ class DjangoServiceConnector(ServiceConnector):
'scope': ' '.join(scopes), 'scope': ' '.join(scopes),
} }
print(f"Posting to token URL: {token_url}")
response = requests.post(token_url, data=data, timeout=10) response = requests.post(token_url, data=data, timeout=10)
if not response.ok:
print(f"Token request failed: {response.status_code} {response.text}")
response.raise_for_status() response.raise_for_status()
token_data = response.json() token_data = response.json()
@@ -240,11 +251,14 @@ class DjangoServiceConnector(ServiceConnector):
if 'accept' in kwargs: if 'accept' in kwargs:
headers['Accept'] = kwargs['accept'] headers['Accept'] = kwargs['accept']
print(f"Making service request to: {url}")
if is_post: if is_post:
response = requests.post(url, json=data, headers=headers, timeout=10) response = requests.post(url, json=data, headers=headers, timeout=10)
else: else:
response = requests.get(url, headers=headers, timeout=10) response = requests.get(url, headers=headers, timeout=10)
print(f"Service response status: {response.status_code}")
response.raise_for_status() response.raise_for_status()
try: try:

View File

@@ -201,7 +201,8 @@ class SelectMediaView(View):
lti_content_items.append(lti_item) lti_content_items.append(lti_item)
# Create JWT payload # Create JWT payload
tool_issuer = request.build_absolute_uri('/')[:-1] # Use client_id as issuer to avoid "wrong consumer key" errors in Moodle
tool_issuer = platform.client_id
# Per LTI spec, aud should be the platform's issuer URL # Per LTI spec, aud should be the platform's issuer URL
# Try just the platform URL (some Moodle versions don't accept arrays) # Try just the platform URL (some Moodle versions don't accept arrays)

View File

@@ -5,6 +5,7 @@ Fetches course membership from Moodle via NRPS and syncs to MediaCMS RBAC groups
""" """
import hashlib import hashlib
import traceback
from allauth.account.models import EmailAddress from allauth.account.models import EmailAddress
from django.utils import timezone from django.utils import timezone
@@ -65,7 +66,9 @@ class LTINRPSClient:
return members return members
except Exception: except Exception as e:
print(f"Error fetching members: {e}")
traceback.print_exc()
return [] return []
def sync_members_to_rbac_group(self, rbac_group): def sync_members_to_rbac_group(self, rbac_group):
@@ -100,7 +103,9 @@ class LTINRPSClient:
synced_count += 1 synced_count += 1
except Exception: except Exception as e:
print(f"Error syncing user {member.get('user_id')}: {e}")
traceback.print_exc()
continue continue
removed_count = 0 removed_count = 0

View File

@@ -109,6 +109,8 @@ class OIDCLoginView(View):
params = { params = {
'response_type': 'id_token', 'response_type': 'id_token',
'redirect_uri': target_link_uri, 'redirect_uri': target_link_uri,
# PyLTI1p3 OIDCLogin.redirect() sets cookie with state
# But Moodle might need it in the return
'state': state, 'state': state,
'client_id': client_id, 'client_id': client_id,
'login_hint': login_hint, 'login_hint': login_hint,
@@ -157,8 +159,20 @@ class LaunchView(View):
raise ValueError("Missing id_token in launch request") raise ValueError("Missing id_token in launch request")
unverified = jwt.decode(id_token, options={"verify_signature": False}) unverified = jwt.decode(id_token, options={"verify_signature": False})
# Debug logging
print("LTI LAUNCH UNVERIFIED HEADER:", jwt.get_unverified_header(id_token))
print("LTI LAUNCH UNVERIFIED PAYLOAD:", unverified)
iss = unverified.get('iss') iss = unverified.get('iss')
# Handle Moodle issuer variations (some versions have trailing slash)
# Check exact match first, then try with/without slash
# This is a common issue with LTI 1.3 integrations
aud = unverified.get('aud') aud = unverified.get('aud')
if isinstance(aud, list):
aud = aud[0]
try: try:
platform = LTIPlatform.objects.get(platform_id=iss, client_id=aud) platform = LTIPlatform.objects.get(platform_id=iss, client_id=aud)
except LTIPlatform.DoesNotExist: except LTIPlatform.DoesNotExist: