mirror of
https://github.com/mediacms-io/mediacms.git
synced 2026-02-04 06:22:59 -05:00
rev
This commit is contained in:
@@ -95,75 +95,39 @@ class DjangoMessageLaunch:
|
|||||||
|
|
||||||
|
|
||||||
class DjangoSessionService:
|
class DjangoSessionService:
|
||||||
"""Launch data storage using Django sessions with cache fallback for state"""
|
"""Launch data storage using Django sessions"""
|
||||||
|
|
||||||
def __init__(self, request):
|
def __init__(self, request):
|
||||||
self.request = request
|
self.request = request
|
||||||
self._session_key_prefix = 'lti1p3_'
|
self._session_key_prefix = 'lti1p3_'
|
||||||
self._cache_prefix = 'lti1p3_cache_'
|
|
||||||
|
|
||||||
def get_launch_data(self, key):
|
def get_launch_data(self, key):
|
||||||
"""Get launch data from session or cache (for state keys)"""
|
"""Get launch data from session"""
|
||||||
# For state keys, try cache first (more reliable for cross-site flows)
|
|
||||||
if key.startswith('state-'):
|
|
||||||
cache_key = self._cache_prefix + key
|
|
||||||
cached_data = cache.get(cache_key)
|
|
||||||
if cached_data:
|
|
||||||
return json.loads(cached_data) if isinstance(cached_data, str) else cached_data
|
|
||||||
|
|
||||||
# Try session
|
|
||||||
session_key = self._session_key_prefix + key
|
session_key = self._session_key_prefix + key
|
||||||
data = self.request.session.get(session_key)
|
data = self.request.session.get(session_key)
|
||||||
return json.loads(data) if data else None
|
return json.loads(data) if data else None
|
||||||
|
|
||||||
def save_launch_data(self, key, data):
|
def save_launch_data(self, key, data):
|
||||||
"""Save launch data to session and cache (for state keys)"""
|
"""Save launch data to session"""
|
||||||
# Always save to session first
|
|
||||||
session_key = self._session_key_prefix + key
|
session_key = self._session_key_prefix + key
|
||||||
self.request.session[session_key] = json.dumps(data)
|
self.request.session[session_key] = json.dumps(data)
|
||||||
self.request.session.modified = True
|
self.request.session.modified = True
|
||||||
|
|
||||||
# For state keys, ALSO save to cache as backup (for cross-site cookie issues)
|
|
||||||
if key.startswith('state-'):
|
|
||||||
cache_key = self._cache_prefix + key
|
|
||||||
# Store in cache for 10 minutes (longer than typical LTI flow)
|
|
||||||
cache.set(cache_key, json.dumps(data), timeout=600)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def check_launch_data_storage_exists(self, key):
|
def check_launch_data_storage_exists(self, key):
|
||||||
"""Check if launch data exists in session or cache"""
|
"""Check if launch data exists in session"""
|
||||||
# For state keys, check cache first
|
|
||||||
if key.startswith('state-'):
|
|
||||||
cache_key = self._cache_prefix + key
|
|
||||||
if cache.get(cache_key) is not None:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Check session
|
|
||||||
session_key = self._session_key_prefix + key
|
session_key = self._session_key_prefix + key
|
||||||
return session_key in self.request.session
|
return session_key in self.request.session
|
||||||
|
|
||||||
def check_state_is_valid(self, state, nonce):
|
def check_state_is_valid(self, state, nonce):
|
||||||
"""Check if state is valid - state is for CSRF protection, nonce is validated separately by JWT"""
|
"""Check if state is valid - state is for CSRF protection, nonce is validated separately by JWT"""
|
||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
state_key = f'state-{state}'
|
state_key = f'state-{state}'
|
||||||
logger.error(f"[STATE VALIDATION] Checking state: {state}")
|
|
||||||
logger.error(f"[STATE VALIDATION] Looking for session key: {self._session_key_prefix + state_key}")
|
|
||||||
|
|
||||||
# List all session keys for debugging
|
|
||||||
all_keys = [k for k in self.request.session.keys() if k.startswith(self._session_key_prefix)]
|
|
||||||
logger.error(f"[STATE VALIDATION] All LTI session keys: {all_keys}")
|
|
||||||
|
|
||||||
state_data = self.get_launch_data(state_key)
|
state_data = self.get_launch_data(state_key)
|
||||||
|
|
||||||
if not state_data:
|
if not state_data:
|
||||||
logger.error("[STATE VALIDATION] State NOT found in session")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
logger.error(f"[STATE VALIDATION] State found successfully: {state_data}")
|
|
||||||
# State exists - that's sufficient for CSRF protection
|
# State exists - that's sufficient for CSRF protection
|
||||||
# Nonce validation is handled by PyLTI1p3 through JWT signature and claims validation
|
# Nonce validation is handled by PyLTI1p3 through JWT signature and claims validation
|
||||||
return True
|
return True
|
||||||
@@ -172,25 +136,12 @@ class DjangoSessionService:
|
|||||||
"""Check if nonce is valid (not used before) and mark it as used"""
|
"""Check if nonce is valid (not used before) and mark it as used"""
|
||||||
nonce_key = f'nonce-{nonce}'
|
nonce_key = f'nonce-{nonce}'
|
||||||
|
|
||||||
# Use cache for nonce checking (more reliable for cross-site flows)
|
|
||||||
cache_key = self._cache_prefix + nonce_key
|
|
||||||
|
|
||||||
# Check if nonce was already used
|
# Check if nonce was already used
|
||||||
if cache.get(cache_key) is not None:
|
if self.check_launch_data_storage_exists(nonce_key):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Mark nonce as used in cache (expires in 10 minutes)
|
# Mark nonce as used
|
||||||
cache.set(cache_key, json.dumps({'used': True}), timeout=600)
|
self.save_launch_data(nonce_key, {'used': True})
|
||||||
|
|
||||||
# Also save to session for redundancy
|
|
||||||
try:
|
|
||||||
session_key = self._session_key_prefix + nonce_key
|
|
||||||
self.request.session[session_key] = json.dumps({'used': True})
|
|
||||||
self.request.session.modified = True
|
|
||||||
except Exception:
|
|
||||||
# If session fails, cache is sufficient
|
|
||||||
pass
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def set_state_valid(self, state, id_token_hash):
|
def set_state_valid(self, state, id_token_hash):
|
||||||
|
|||||||
30
lti/views.py
30
lti/views.py
@@ -116,15 +116,8 @@ class OIDCLoginView(View):
|
|||||||
if cmid:
|
if cmid:
|
||||||
launch_data['cmid'] = cmid
|
launch_data['cmid'] = cmid
|
||||||
|
|
||||||
logger.error(f"[OIDC LOGIN DEBUG] Generated state: {state}")
|
|
||||||
logger.error(f"[OIDC LOGIN DEBUG] Saving launch data with media_friendly_token: {launch_data.get('media_friendly_token')}")
|
|
||||||
|
|
||||||
session_service.save_launch_data(f'state-{state}', launch_data)
|
session_service.save_launch_data(f'state-{state}', launch_data)
|
||||||
|
|
||||||
# Verify state was saved
|
|
||||||
saved_data = session_service.get_launch_data(f'state-{state}')
|
|
||||||
logger.error(f"[OIDC LOGIN DEBUG] Verified saved state data: {saved_data}")
|
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
'response_type': 'id_token',
|
'response_type': 'id_token',
|
||||||
'redirect_uri': target_link_uri,
|
'redirect_uri': target_link_uri,
|
||||||
@@ -180,10 +173,6 @@ class LaunchView(View):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
id_token = request.POST.get('id_token')
|
id_token = request.POST.get('id_token')
|
||||||
state = request.POST.get('state')
|
|
||||||
|
|
||||||
logger.error(f"[LTI LAUNCH DEBUG] Received state: {state}")
|
|
||||||
|
|
||||||
if not id_token:
|
if not id_token:
|
||||||
raise ValueError("Missing id_token in launch request")
|
raise ValueError("Missing id_token in launch request")
|
||||||
|
|
||||||
@@ -202,12 +191,6 @@ class LaunchView(View):
|
|||||||
session_service = DjangoSessionService(request)
|
session_service = DjangoSessionService(request)
|
||||||
cookie_service = DjangoSessionService(request)
|
cookie_service = DjangoSessionService(request)
|
||||||
|
|
||||||
# Retrieve stored OIDC login data using state parameter
|
|
||||||
stored_oidc_data = None
|
|
||||||
if state:
|
|
||||||
stored_oidc_data = session_service.get_launch_data(f'state-{state}')
|
|
||||||
logger.error(f"[LTI LAUNCH DEBUG] Retrieved stored OIDC data: {stored_oidc_data}")
|
|
||||||
|
|
||||||
class CustomMessageLaunch(MessageLaunch):
|
class CustomMessageLaunch(MessageLaunch):
|
||||||
def _get_request_param(self, key):
|
def _get_request_param(self, key):
|
||||||
"""Override to properly get request parameters"""
|
"""Override to properly get request parameters"""
|
||||||
@@ -252,17 +235,14 @@ class LaunchView(View):
|
|||||||
|
|
||||||
create_lti_session(request, user, message_launch, platform)
|
create_lti_session(request, user, message_launch, platform)
|
||||||
|
|
||||||
# Check for media_friendly_token in multiple places:
|
# Check for media_friendly_token in custom claims
|
||||||
# 1. Custom claims (from Moodle LTI configuration)
|
|
||||||
media_token = custom_claims.get('media_friendly_token')
|
media_token = custom_claims.get('media_friendly_token')
|
||||||
logger.error(f"[LTI LAUNCH DEBUG] media_token from custom_claims: {media_token}")
|
logger.error(f"[LTI LAUNCH DEBUG] media_token from custom_claims: {media_token}")
|
||||||
|
if media_token:
|
||||||
|
logger.error(f"[LTI LAUNCH DEBUG] Found media_friendly_token in custom claims: {media_token}")
|
||||||
|
|
||||||
# 2. Stored OIDC data (from filter-based launches)
|
# Check if media token was passed via target_link_uri query parameter (from filter launch)
|
||||||
if not media_token and stored_oidc_data:
|
logger.error(f"[LTI LAUNCH DEBUG] About to check target_link_uri, media_token is: {media_token}")
|
||||||
media_token = stored_oidc_data.get('media_friendly_token')
|
|
||||||
logger.error(f"[LTI LAUNCH DEBUG] media_token from stored OIDC data: {media_token}")
|
|
||||||
|
|
||||||
# 3. Target link URI query parameters (fallback)
|
|
||||||
if not media_token:
|
if not media_token:
|
||||||
target_link_uri = launch_data.get('https://purl.imsglobal.org/spec/lti/claim/target_link_uri', '')
|
target_link_uri = launch_data.get('https://purl.imsglobal.org/spec/lti/claim/target_link_uri', '')
|
||||||
logger.error(f"[LTI LAUNCH DEBUG] target_link_uri: {target_link_uri}")
|
logger.error(f"[LTI LAUNCH DEBUG] target_link_uri: {target_link_uri}")
|
||||||
|
|||||||
Reference in New Issue
Block a user