diff --git a/lti/adapters.py b/lti/adapters.py index 7336e17c..8a92873c 100644 --- a/lti/adapters.py +++ b/lti/adapters.py @@ -6,6 +6,7 @@ Provides Django-specific implementations for PyLTI1p3 interfaces import json import time +import uuid from typing import Any, Dict, Optional import jwt @@ -192,6 +193,8 @@ class DjangoServiceConnector(ServiceConnector): self._access_token_expires = 0 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: return self._access_token @@ -200,14 +203,19 @@ class DjangoServiceConnector(ServiceConnector): 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()) + # 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()) payload = { 'iss': self._registration.get_client_id(), 'sub': self._registration.get_client_id(), - 'aud': self._registration.get_auth_token_url(), + 'aud': aud, 'iat': now, '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']}) @@ -220,7 +228,10 @@ class DjangoServiceConnector(ServiceConnector): 'scope': ' '.join(scopes), } + print(f"Posting to token URL: {token_url}") 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() token_data = response.json() @@ -240,11 +251,14 @@ class DjangoServiceConnector(ServiceConnector): if 'accept' in kwargs: headers['Accept'] = kwargs['accept'] + print(f"Making service request to: {url}") if is_post: response = requests.post(url, json=data, headers=headers, timeout=10) else: response = requests.get(url, headers=headers, timeout=10) + print(f"Service response status: {response.status_code}") + response.raise_for_status() try: diff --git a/lti/deep_linking.py b/lti/deep_linking.py index e7e69f8f..044dfc33 100644 --- a/lti/deep_linking.py +++ b/lti/deep_linking.py @@ -201,7 +201,8 @@ class SelectMediaView(View): lti_content_items.append(lti_item) # 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 # Try just the platform URL (some Moodle versions don't accept arrays) diff --git a/lti/services.py b/lti/services.py index 4f3dd92a..b2cac51f 100644 --- a/lti/services.py +++ b/lti/services.py @@ -5,6 +5,7 @@ Fetches course membership from Moodle via NRPS and syncs to MediaCMS RBAC groups """ import hashlib +import traceback from allauth.account.models import EmailAddress from django.utils import timezone @@ -65,7 +66,9 @@ class LTINRPSClient: return members - except Exception: + except Exception as e: + print(f"Error fetching members: {e}") + traceback.print_exc() return [] def sync_members_to_rbac_group(self, rbac_group): @@ -100,7 +103,9 @@ class LTINRPSClient: synced_count += 1 - except Exception: + except Exception as e: + print(f"Error syncing user {member.get('user_id')}: {e}") + traceback.print_exc() continue removed_count = 0 diff --git a/lti/views.py b/lti/views.py index 7f4a3c62..830f0b24 100644 --- a/lti/views.py +++ b/lti/views.py @@ -109,6 +109,8 @@ class OIDCLoginView(View): params = { 'response_type': 'id_token', 'redirect_uri': target_link_uri, + # PyLTI1p3 OIDCLogin.redirect() sets cookie with state + # But Moodle might need it in the return 'state': state, 'client_id': client_id, 'login_hint': login_hint, @@ -157,8 +159,20 @@ class LaunchView(View): raise ValueError("Missing id_token in launch request") 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') + + # 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') + if isinstance(aud, list): + aud = aud[0] + try: platform = LTIPlatform.objects.get(platform_id=iss, client_id=aud) except LTIPlatform.DoesNotExist: