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 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:

View File

@@ -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)

View File

@@ -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

View File

@@ -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: