mirror of
https://github.com/mediacms-io/mediacms.git
synced 2026-02-04 06:22:59 -05:00
a
This commit is contained in:
119
lti/views.py
119
lti/views.py
@@ -13,7 +13,7 @@ Implements the LTI 1.3 / LTI Advantage flow:
|
|||||||
import logging
|
import logging
|
||||||
import traceback
|
import traceback
|
||||||
import uuid
|
import uuid
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import parse_qs, urlencode, urlparse
|
||||||
|
|
||||||
import jwt
|
import jwt
|
||||||
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
|
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
|
||||||
@@ -233,6 +233,10 @@ class LaunchView(View):
|
|||||||
else:
|
else:
|
||||||
resource_link_obj = None
|
resource_link_obj = None
|
||||||
|
|
||||||
|
# Clear retry counter on successful launch
|
||||||
|
if 'lti_retry_count' in request.session:
|
||||||
|
del request.session['lti_retry_count']
|
||||||
|
|
||||||
create_lti_session(request, user, message_launch, platform)
|
create_lti_session(request, user, message_launch, platform)
|
||||||
|
|
||||||
# Check for media_friendly_token in custom claims
|
# Check for media_friendly_token in custom claims
|
||||||
@@ -247,8 +251,6 @@ class LaunchView(View):
|
|||||||
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}")
|
||||||
if '?media_token=' in target_link_uri or '&media_token=' in target_link_uri:
|
if '?media_token=' in target_link_uri or '&media_token=' in target_link_uri:
|
||||||
from urllib.parse import parse_qs, urlparse
|
|
||||||
|
|
||||||
parsed = urlparse(target_link_uri)
|
parsed = urlparse(target_link_uri)
|
||||||
params = parse_qs(parsed.query)
|
params = parse_qs(parsed.query)
|
||||||
media_token = params.get('media_token', [None])[0]
|
media_token = params.get('media_token', [None])[0]
|
||||||
@@ -269,8 +271,18 @@ class LaunchView(View):
|
|||||||
return HttpResponseRedirect(redirect_url)
|
return HttpResponseRedirect(redirect_url)
|
||||||
|
|
||||||
except LtiException as e: # noqa
|
except LtiException as e: # noqa
|
||||||
|
error_message = str(e)
|
||||||
|
|
||||||
|
# Special handling for "State not found" errors - attempt retry
|
||||||
|
if "State not found" in error_message or "state not found" in error_message.lower():
|
||||||
|
logger.warning("[LTI LAUNCH] State not found error detected, attempting recovery")
|
||||||
|
return self.handle_state_not_found(request, platform)
|
||||||
|
|
||||||
|
# Other LTI exceptions - fail normally
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
except Exception as e: # noqa
|
except Exception as e: # noqa
|
||||||
|
error_message = str(e)
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
if platform:
|
if platform:
|
||||||
@@ -278,6 +290,107 @@ class LaunchView(View):
|
|||||||
|
|
||||||
return render(request, 'lti/launch_error.html', {'error': 'LTI Launch Failed', 'message': error_message}, status=400)
|
return render(request, 'lti/launch_error.html', {'error': 'LTI Launch Failed', 'message': error_message}, status=400)
|
||||||
|
|
||||||
|
def handle_state_not_found(self, request, platform=None):
|
||||||
|
"""
|
||||||
|
Handle state not found errors by attempting to restart the OIDC flow.
|
||||||
|
|
||||||
|
This can happen when:
|
||||||
|
- Cookies are blocked/deleted
|
||||||
|
- Session expired
|
||||||
|
- Browser privacy settings interfere
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Check retry count to prevent infinite loops
|
||||||
|
retry_count = request.session.get('lti_retry_count', 0)
|
||||||
|
MAX_RETRIES = 2
|
||||||
|
|
||||||
|
if retry_count >= MAX_RETRIES:
|
||||||
|
logger.error(f"[LTI RETRY] Max retries ({MAX_RETRIES}) exceeded for state recovery")
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
'lti/launch_error.html',
|
||||||
|
{
|
||||||
|
'error': 'Authentication Failed',
|
||||||
|
'message': (
|
||||||
|
'Unable to establish a secure session. This may be due to browser '
|
||||||
|
'cookie settings or privacy features. Please try:\n\n'
|
||||||
|
'1. Enabling cookies for this site\n'
|
||||||
|
'2. Disabling tracking protection for this site\n'
|
||||||
|
'3. Using a different browser\n'
|
||||||
|
'4. Contacting your administrator if the issue persists'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extract launch parameters from the POST request
|
||||||
|
id_token = request.POST.get('id_token')
|
||||||
|
state = request.POST.get('state')
|
||||||
|
|
||||||
|
if not id_token:
|
||||||
|
raise ValueError("No id_token available for retry")
|
||||||
|
|
||||||
|
# Decode JWT to extract issuer and target info (no verification needed for this)
|
||||||
|
unverified = jwt.decode(id_token, options={"verify_signature": False})
|
||||||
|
|
||||||
|
iss = unverified.get('iss')
|
||||||
|
aud = unverified.get('aud') # This is the client_id
|
||||||
|
target_link_uri = unverified.get('https://purl.imsglobal.org/spec/lti/claim/target_link_uri')
|
||||||
|
|
||||||
|
# Get login_hint from JWT sub claim
|
||||||
|
login_hint = request.POST.get('login_hint') or unverified.get('sub')
|
||||||
|
|
||||||
|
if not all([iss, aud, target_link_uri]):
|
||||||
|
raise ValueError("Missing required parameters for OIDC retry")
|
||||||
|
|
||||||
|
# Try to identify platform
|
||||||
|
if not platform:
|
||||||
|
try:
|
||||||
|
platform = LTIPlatform.objects.get(platform_id=iss, client_id=aud)
|
||||||
|
except LTIPlatform.DoesNotExist:
|
||||||
|
raise ValueError(f"Platform not found: {iss}/{aud}")
|
||||||
|
|
||||||
|
# Increment retry counter
|
||||||
|
request.session['lti_retry_count'] = retry_count + 1
|
||||||
|
request.session.modified = True
|
||||||
|
|
||||||
|
logger.warning(f"[LTI RETRY] State not found, attempting retry #{retry_count + 1}. " f"Platform: {platform.name}, State: {state}, Target: {target_link_uri}")
|
||||||
|
|
||||||
|
# Build OIDC login URL with all parameters
|
||||||
|
oidc_login_url = request.build_absolute_uri(reverse('lti:oidc_login'))
|
||||||
|
|
||||||
|
params = {
|
||||||
|
'iss': iss,
|
||||||
|
'client_id': aud,
|
||||||
|
'target_link_uri': target_link_uri,
|
||||||
|
'login_hint': login_hint,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Include lti_message_hint if we have it
|
||||||
|
lti_message_hint = request.POST.get('lti_message_hint')
|
||||||
|
if lti_message_hint:
|
||||||
|
params['lti_message_hint'] = lti_message_hint
|
||||||
|
|
||||||
|
# Add retry indicator
|
||||||
|
params['retry'] = retry_count + 1
|
||||||
|
|
||||||
|
redirect_url = f"{oidc_login_url}?{urlencode(params)}"
|
||||||
|
|
||||||
|
logger.error(f"[LTI RETRY] Redirecting to OIDC login: {redirect_url}")
|
||||||
|
|
||||||
|
return HttpResponseRedirect(redirect_url)
|
||||||
|
|
||||||
|
except Exception as retry_error:
|
||||||
|
logger.error(f"[LTI RETRY] Failed to handle state recovery: {retry_error}")
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
'lti/launch_error.html',
|
||||||
|
{'error': 'LTI Launch Failed', 'message': f'State validation failed and automatic retry was unsuccessful: {str(retry_error)}'},
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
|
||||||
def sanitize_claims(self, claims):
|
def sanitize_claims(self, claims):
|
||||||
"""Remove sensitive data from claims before logging"""
|
"""Remove sensitive data from claims before logging"""
|
||||||
safe_claims = claims.copy()
|
safe_claims = claims.copy()
|
||||||
|
|||||||
@@ -39,6 +39,26 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
.troubleshooting {
|
||||||
|
background: #e3f2fd;
|
||||||
|
border-left: 4px solid #1976d2;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 20px 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.troubleshooting h3 {
|
||||||
|
color: #1976d2;
|
||||||
|
margin-top: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.troubleshooting ol {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
.troubleshooting li {
|
||||||
|
margin: 8px 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -47,11 +67,27 @@
|
|||||||
<h1>{{ error }}</h1>
|
<h1>{{ error }}</h1>
|
||||||
<div class="error-message">
|
<div class="error-message">
|
||||||
<strong>Error Details:</strong><br>
|
<strong>Error Details:</strong><br>
|
||||||
{{ message }}
|
<pre style="white-space: pre-wrap; word-wrap: break-word; font-family: inherit;">{{ message }}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if 'cookie' in message|lower or 'session' in message|lower or 'Authentication Failed' in error %}
|
||||||
|
<div class="troubleshooting">
|
||||||
|
<h3>🔧 Troubleshooting Steps:</h3>
|
||||||
|
<ol>
|
||||||
|
<li><strong>Enable Cookies:</strong> Ensure your browser allows cookies for this site. Check your browser settings under Privacy & Security.</li>
|
||||||
|
<li><strong>Disable Tracking Protection:</strong> Turn off Enhanced Tracking Protection, Shield, or similar privacy features for this site.</li>
|
||||||
|
<li><strong>Allow Third-Party Cookies:</strong> Some browsers block cookies from embedded content. Try allowing cookies from all sites temporarily.</li>
|
||||||
|
<li><strong>Clear Browser Cache:</strong> Clear cookies and cached data for this site, then try launching again from your course.</li>
|
||||||
|
<li><strong>Try a Different Browser:</strong> Test with Chrome, Firefox, Safari, or Edge to rule out browser-specific issues.</li>
|
||||||
|
<li><strong>Disable Extensions:</strong> Browser extensions (ad blockers, privacy tools) can interfere with authentication. Try disabling them.</li>
|
||||||
|
<li><strong>Contact Support:</strong> If the issue persists after trying the above, contact your system administrator with the error details above.</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="help-text">
|
<div class="help-text">
|
||||||
<p>If this problem persists, please contact your system administrator.</p>
|
<p>If this problem persists, please contact your system administrator.</p>
|
||||||
<p>You may close this window and try again.</p>
|
<p>You may close this window and return to your course to try again.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user