diff --git a/lti/adapters.py b/lti/adapters.py index f79a8d0a..d12f96b8 100644 --- a/lti/adapters.py +++ b/lti/adapters.py @@ -120,7 +120,7 @@ class DjangoSessionService: def check_state_is_valid(self, state, nonce): """Check if state is valid""" state_key = f'state-{state}' - print(f"Checking state validity: state={state}", flush=True) + print(f"Checking state validity: state={state}, nonce={nonce}", flush=True) print(f"Looking for state_key: {state_key}", flush=True) state_data = self.get_launch_data(state_key) @@ -130,8 +130,12 @@ class DjangoSessionService: print("ERROR: State data not found in session!", flush=True) return False - # State exists, which is sufficient for CSRF protection - # Nonce is validated by PyLTI1p3 through JWT signature verification + # Check if nonce matches (if we stored one) + stored_nonce = state_data.get('nonce') + if stored_nonce and stored_nonce != nonce: + print(f"ERROR: Nonce mismatch! Expected: {stored_nonce}, Got: {nonce}", flush=True) + return False + print("State is valid!", flush=True) return True diff --git a/lti/views.py b/lti/views.py index c24b0982..fcb6fedb 100644 --- a/lti/views.py +++ b/lti/views.py @@ -83,7 +83,6 @@ class OIDCLoginView(View): client_id = request.GET.get('client_id') or request.POST.get('client_id') login_hint = request.GET.get('login_hint') or request.POST.get('login_hint') lti_message_hint = request.GET.get('lti_message_hint') or request.POST.get('lti_message_hint') - lti_deployment_id = request.GET.get('lti_deployment_id') or request.POST.get('lti_deployment_id') print(f"OIDC params - iss: {iss}, client_id: {client_id}, target: {target_link_uri}", flush=True) print(f"login_hint: {login_hint}, lti_message_hint: {lti_message_hint}", flush=True) @@ -130,29 +129,32 @@ class OIDCLoginView(View): if not redirect_url: print("PyLTI1p3 redirect failed, building URL manually...", flush=True) - # Manual OIDC redirect construction - # Note: We don't send nonce - Moodle generates it and includes it in the JWT + # Manual OIDC redirect construction with all required OAuth 2.0 parameters import uuid from urllib.parse import urlencode state = str(uuid.uuid4()) + nonce = str(uuid.uuid4()) - # Store state in session (nonce will come from JWT) - session_service.save_launch_data(f'state-{state}', {'target_link_uri': target_link_uri}) + # Store state and nonce in session for validation + session_service.save_launch_data(f'state-{state}', {'target_link_uri': target_link_uri, 'nonce': nonce}) - # Build redirect URL - only LTI 1.3 required parameters + # Build redirect URL with all required parameters params = { - 'iss': iss, - 'login_hint': login_hint, - 'target_link_uri': target_link_uri, + 'response_type': 'id_token', + 'redirect_uri': target_link_uri, + 'state': state, 'client_id': client_id, + 'login_hint': login_hint, + 'scope': 'openid', + 'response_mode': 'form_post', + 'prompt': 'none', + 'nonce': nonce, } # Add optional parameters if present if lti_message_hint: params['lti_message_hint'] = lti_message_hint - if lti_deployment_id: - params['lti_deployment_id'] = lti_deployment_id redirect_url = f"{platform.auth_login_url}?{urlencode(params)}" print(f"Manually built redirect URL: {redirect_url}", flush=True)