def get(self): """ :return: """ tool_conf = settings.LTI_TOOL_CONFIG launch_data_storage = FlaskCacheDataStorage(cache) flask_request = FlaskRequest() target_link_uri = flask_request.get_param('target_link_uri') if not target_link_uri: raise Exception('Missing "target_link_uri" param') logs.api_logger.info("LTI Login", extra={ "clientip": request.remote_addr, "path": request.path, "user": request.remote_user }) oidc_login = FlaskOIDCLogin(flask_request, tool_conf, launch_data_storage=launch_data_storage) return oidc_login.enable_check_cookies().redirect(target_link_uri)
def launch(): launch_unique_id = str(request.args.get('launch_id', '')) # reload page in case if session cookie is unavailable (chrome samesite issue): # https://chromestatus.com/feature/5088147346030592 # to share GET/POST data between requests we save them into cache session_key = request.cookies.get(app.config['SESSION_COOKIE_NAME'], None) if not session_key and not launch_unique_id: launch_unique_id = str(uuid.uuid4()) cache.set(launch_unique_id, { 'GET': request.args.to_dict(), 'POST': request.form.to_dict() }, 3600) current_url = request.base_url if '?' in current_url: current_url += '&' else: current_url += '?' current_url = current_url + 'launch_id=' + launch_unique_id return '<script type="text/javascript">window.location="%s";</script>' % current_url launch_request = FlaskRequest() if request.method == "GET": launch_data = cache.get(launch_unique_id) if not launch_data: raise Exception("Can't restore launch data from cache") request_params_dict = {} request_params_dict.update(launch_data['GET']) request_params_dict.update(launch_data['POST']) launch_request = FlaskRequest(request_data=request_params_dict) tool_conf = ToolConfJsonFile(get_lti_config_path()) message_launch = ExtendedFlaskMessageLaunch(launch_request, tool_conf) message_launch_data = message_launch.get_launch_data() pprint.pprint(message_launch_data) tpl_kwargs = { 'page_title': PAGE_TITLE, 'is_deep_link_launch': message_launch.is_deep_link_launch(), 'launch_data': message_launch.get_launch_data(), 'launch_id': message_launch.get_launch_id(), 'curr_user_name': message_launch_data.get('name', ''), 'curr_diff': message_launch_data.get( 'https://purl.imsglobal.org/spec/lti/claim/custom', {}).get('difficulty', 'normal') } return render_template('game.html', **tpl_kwargs)
def login(): tool_conf = ToolConfJsonFile(get_lti_config_path()) launch_data_storage = get_launch_data_storage() flask_request = FlaskRequest() target_link_uri = flask_request.get_param('target_link_uri') if not target_link_uri: raise Exception('Missing "target_link_uri" param') oidc_login = FlaskOIDCLogin(flask_request, tool_conf, launch_data_storage=launch_data_storage) return oidc_login\ .enable_check_cookies()\ .redirect(target_link_uri)
def post(self): """ Post method :return: """ flask_request = FlaskRequest() launch_data_storage = FlaskCacheDataStorage(cache) message_launch = FlaskMessageLaunch( flask_request, current_app.config['LTI_TOOL_CONFIG'], launch_data_storage=launch_data_storage) message_launch_data = message_launch.get_launch_data() pprint.pprint(message_launch_data) token = message_launch.get_launch_id() cache.set(token, message_launch_data) redirection = redirect('http://localhost:3000/deepLinkContent/' + token) return redirection redirection = redirect('http://localhost:3000/deepLinkContent/' + token) return redirection
def launch(): tool_conf = ToolConfJsonFile(get_lti_config_path()) flask_request = FlaskRequest() launch_data_storage = get_launch_data_storage() message_launch = ExtendedFlaskMessageLaunch( flask_request, tool_conf, launch_data_storage=launch_data_storage) message_launch_data = message_launch.get_launch_data() pprint.pprint(message_launch_data) tpl_kwargs = { 'page_title': PAGE_TITLE, 'is_deep_link_launch': message_launch.is_deep_link_launch(), 'launch_data': message_launch.get_launch_data(), 'launch_id': message_launch.get_launch_id(), 'curr_user_name': message_launch_data.get('name', '') } learn_url = message_launch_data[ 'https://purl.imsglobal.org/spec/lti/claim/tool_platform'][ 'url'].rstrip('/') params = { 'redirect_uri': Config.config['app_url'] + '/authcode/', 'response_type': 'code', 'client_id': Config.config['learn_rest_key'], 'scope': '*', 'state': str(uuid.uuid4()) } encodedParams = urllib.parse.urlencode(params) get_authcode_url = learn_url + '/learn/api/public/v1/oauth2/authorizationcode?' + encodedParams print("authcode_URL: " + get_authcode_url) return (redirect(get_authcode_url))
def score(launch_id, earned_score, time_spent): tool_conf = ToolConfJsonFile(get_lti_config_path()) flask_request = FlaskRequest() launch_data_storage = get_launch_data_storage() message_launch = ExtendedFlaskMessageLaunch.from_cache( launch_id, flask_request, tool_conf, launch_data_storage=launch_data_storage) resource_link_id = message_launch.get_launch_data() \ .get('https://purl.imsglobal.org/spec/lti/claim/resource_link', {}).get('id') if not message_launch.has_ags(): raise Forbidden("Don't have grades!") sub = message_launch.get_launch_data().get('sub') timestamp = datetime.datetime.utcnow().isoformat() + 'Z' earned_score = int(earned_score) time_spent = int(time_spent) grades = message_launch.get_ags() sc = Grade() sc.set_score_given(earned_score) \ .set_score_maximum(100) \ .set_timestamp(timestamp) \ .set_activity_progress('Completed') \ .set_grading_progress('FullyGraded') \ .set_user_id(sub) sc_line_item = LineItem() sc_line_item.set_tag('score') \ .set_score_maximum(100) \ .set_label('Score') if resource_link_id: sc_line_item.set_resource_id(resource_link_id) grades.put_grade(sc, sc_line_item) tm = Grade() tm.set_score_given(time_spent) \ .set_score_maximum(999) \ .set_timestamp(timestamp) \ .set_activity_progress('Completed') \ .set_grading_progress('FullyGraded') \ .set_user_id(sub) tm_line_item = LineItem() tm_line_item.set_tag('time') \ .set_score_maximum(999) \ .set_label('Time Taken') if resource_link_id: tm_line_item.set_resource_id(resource_link_id) result = grades.put_grade(tm, tm_line_item) return jsonify({'success': True, 'result': result.get('body')})
def launch(): tool_conf = ToolConfJsonFile(get_lti_config_path()) flask_request = FlaskRequest() launch_data_storage = get_launch_data_storage() message_launch = ExtendedFlaskMessageLaunch( flask_request, tool_conf, launch_data_storage=launch_data_storage) message_launch_data = message_launch.get_launch_data() pprint.pprint(message_launch_data) tpl_kwargs = { 'page_title': PAGE_TITLE, 'is_deep_link_launch': message_launch.is_deep_link_launch(), 'launch_data': message_launch.get_launch_data(), 'launch_id': message_launch.get_launch_id(), 'curr_user_name': message_launch_data.get('name', '') } """ We could do the launch to the external page here. The following which does the 3LO with REST APIs back to the Learn system is not necessary. It's an artifact of project this one was leveraged from. We left it here for the most part to demonstrate how one can pass data through the 3LO process using the state parameter. The state is an opaque value that doesn't get modified by the developer portal or by Learn. We take the external URL that will be launched to and include it as a portion of the state to be pulled out on the other side of 3LO. It's the only way across. Attempts to pass the data by adding an additional parameter to the request for a authroization code will fail because those will be dropped. I.E setting your redirect_uri to .../authcode/?launch_url=URL does not work. https://stackabuse.com/encoding-and-decoding-base64-strings-in-python/ """ learn_url = message_launch_data[ 'https://purl.imsglobal.org/spec/lti/claim/tool_platform'][ 'url'].rstrip('/') # MUST include a custom parameter like 'external_url=https://www.foodies.com' in the custom params external_url = message_launch_data[ 'https://purl.imsglobal.org/spec/lti/claim/custom'][ 'external_url'].rstrip('/') state = str(uuid.uuid4()) + f'&launch_url={external_url}' message_bytes = state.encode('ascii') base64_bytes = base64.b64encode(message_bytes) base64_message = base64_bytes.decode('ascii') params = { 'redirect_uri': Config.config['app_url'] + '/authcode/', 'response_type': 'code', 'client_id': Config.config['learn_rest_key'], 'scope': '*', 'state': base64_message } encodedParams = urllib.parse.urlencode(params) get_authcode_url = learn_url + '/learn/api/public/v1/oauth2/authorizationcode?' + encodedParams print("authcode_URL: " + get_authcode_url, flush=True) return (redirect(get_authcode_url))
def scoreboard(launch_id): tool_conf = ToolConfJsonFile(get_lti_config_path()) flask_request = FlaskRequest() launch_data_storage = get_launch_data_storage() message_launch = ExtendedFlaskMessageLaunch.from_cache( launch_id, flask_request, tool_conf, launch_data_storage=launch_data_storage) resource_link_id = message_launch.get_launch_data() \ .get('https://purl.imsglobal.org/spec/lti/claim/resource_link', {}).get('id') if not message_launch.has_nrps(): raise Forbidden("Don't have names and roles!") if not message_launch.has_ags(): raise Forbidden("Don't have grades!") ags = message_launch.get_ags() score_line_item = LineItem() score_line_item.set_tag('score') \ .set_score_maximum(100) \ .set_label('Score') if resource_link_id: score_line_item.set_resource_id(resource_link_id) scores = ags.get_grades(score_line_item) time_line_item = LineItem() time_line_item.set_tag('time') \ .set_score_maximum(999) \ .set_label('Time Taken') if resource_link_id: time_line_item.set_resource_id(resource_link_id) times = ags.get_grades(time_line_item) members = message_launch.get_nrps().get_members() scoreboard_result = [] for sc in scores: result = {'score': sc['resultScore']} for tm in times: if tm['userId'] == sc['userId']: result['time'] = tm['resultScore'] break for member in members: if member['user_id'] == sc['userId']: result['name'] = member.get('name', 'Unknown') break scoreboard_result.append(result) return jsonify(scoreboard_result)
def _get_request(self, login_request, login_response, request_is_secure=False, post_data=None, empty_session=False, empty_cookies=False): session = {} if empty_session else login_request.session cookies = {} if empty_cookies else self.get_cookies_dict_from_response( login_response) post_launch_data = post_data if post_data else self.post_launch_data return FlaskRequest(request_data=post_launch_data, cookies=cookies, session=session, request_is_secure=request_is_secure)
def launch(): tool_conf = ToolConfJsonFile(get_lti_config_path()) flask_request = FlaskRequest() launch_data_storage = get_launch_data_storage() message_launch = ExtendedFlaskMessageLaunch( flask_request, tool_conf, launch_data_storage=launch_data_storage) message_launch_data = message_launch.get_launch_data() pprint.pprint(message_launch_data) jsonData = dict(message_launch_data) app.logger.info(jsonData) #return redirect('/predict') return render_template( 'camera.html', jsonData=jsonData[ 'https://purl.imsglobal.org/spec/lti/claim/tool_platform']['name'])
def launch(): tool_conf = ToolConfJsonFile(get_lti_config_path()) flask_request = FlaskRequest() launch_data_storage = get_launch_data_storage() message_launch = ExtendedFlaskMessageLaunch( flask_request, tool_conf, launch_data_storage=launch_data_storage) message_launch_data = message_launch.get_launch_data() pprint.pprint(message_launch_data) tpl_kwargs = { 'page_title': PAGE_TITLE, 'is_deep_link_launch': message_launch.is_deep_link_launch(), 'launch_data': message_launch.get_launch_data(), 'launch_id': message_launch.get_launch_id(), 'curr_user_name': message_launch_data.get('name', '') } learn_url = message_launch_data[ 'https://purl.imsglobal.org/spec/lti/claim/tool_platform'][ 'url'].rstrip('/') # Rererence: https://docs.blackboard.com/blog/2021/05/10/use-one-time-session-tokens-instead-of-cookies-for-UEF-authentication.html # Get the value of the one time session token from the LTI claim one_time_session_token = message_launch_data[ 'https://blackboard.com/lti/claim/one_time_session_token'] # If there is no comma in the value, we've hit the bug. Add it and the user's UUID if "," not in one_time_session_token: one_time_session_token += "," + message_launch_data['sub'] params = { 'redirect_uri': Config.config['app_url'] + '/authcode/', 'response_type': 'code', 'client_id': Config.config['learn_rest_key'], 'scope': '*', 'state': str(uuid.uuid4()), 'one_time_session_token': one_time_session_token } encodedParams = urllib.parse.urlencode(params) get_authcode_url = learn_url + '/learn/api/public/v1/oauth2/authorizationcode?' + encodedParams print("authcode_URL: " + get_authcode_url, flush=True) return (redirect(get_authcode_url))
def configure(launch_id, difficulty): tool_conf = ToolConfJsonFile(get_lti_config_path()) flask_request = FlaskRequest() message_launch = ExtendedFlaskMessageLaunch.from_cache( launch_id, flask_request, tool_conf) if not message_launch.is_deep_link_launch(): raise Forbidden('Must be a deep link!') launch_url = url_for('launch', _external=True) resource = DeepLinkResource() resource.set_url(launch_url) \ .set_custom_params({'difficulty': difficulty}) \ .set_title('Breakout ' + difficulty + ' mode!') html = message_launch.get_deep_link().output_response_form([resource]) return html
def deepLink(): tool_conf = ToolConfJsonFile(get_lti_config_path()) flask_request = FlaskRequest() launch_data_storage = get_launch_data_storage() message_launch = ExtendedFlaskMessageLaunch( flask_request, tool_conf, launch_data_storage=launch_data_storage) message_launch_data = message_launch.get_launch_data() resource = DeepLinkResource() resource.set_url('https://inviguluscanvas.online/launch/') \ .set_custom_params({'text': 'Invigulus'}) \ .set_title('LTI Launch Invigulus') html = message_launch.get_deep_link().output_response_form([resource]) app.logger.info(html) return html pprint.pprint(message_launch_data)
def launch(): tool_conf = ToolConfJsonFile(get_lti_config_path()) flask_request = FlaskRequest() launch_data_storage = get_launch_data_storage() message_launch = ExtendedFlaskMessageLaunch( flask_request, tool_conf, launch_data_storage=launch_data_storage) message_launch_data = message_launch.get_launch_data() pprint.pprint(message_launch_data) difficulty = message_launch_data.get('https://purl.imsglobal.org/spec/lti/claim/custom', {}) \ .get('difficulty', None) if not difficulty: difficulty = request.args.get('difficulty', 'normal') tpl_kwargs = { 'page_title': PAGE_TITLE, 'is_deep_link_launch': message_launch.is_deep_link_launch(), 'launch_data': message_launch.get_launch_data(), 'launch_id': message_launch.get_launch_id(), 'curr_user_name': message_launch_data.get('name', ''), 'curr_diff': difficulty } return render_template('game.html', **tpl_kwargs)
def login(): cookies_allowed = str(request.args.get('cookies_allowed', '')) # check cookies and ask to open page in the new window in case if cookies are not allowed # https://chromestatus.com/feature/5088147346030592 # to share GET/POST data between requests we save them into cache if cookies_allowed: login_unique_id = str(request.args.get('login_unique_id', '')) if not login_unique_id: raise Exception('Missing "login_unique_id" param') login_data = cache.get(login_unique_id) if not login_data: raise Exception("Can't restore login data from cache") tool_conf = ToolConfJsonFile(get_lti_config_path()) request_params_dict = {} request_params_dict.update(login_data['GET']) request_params_dict.update(login_data['POST']) oidc_request = FlaskRequest(request_data=request_params_dict) oidc_login = FlaskOIDCLogin(oidc_request, tool_conf) target_link_uri = request_params_dict.get('target_link_uri') return oidc_login.redirect(target_link_uri) else: login_unique_id = str(uuid.uuid4()) cache.set(login_unique_id, { 'GET': request.args.to_dict(), 'POST': request.form.to_dict() }, 3600) tpl_kwargs = { 'login_unique_id': login_unique_id, 'same_site': app.config['SESSION_COOKIE_SAMESITE'], 'site_protocol': 'https' if request.is_secure else 'http', 'page_title': PAGE_TITLE } return render_template('check_cookie.html', **tpl_kwargs)
def _make_oidc_login(self, uuid_val=None, tool_conf_cls=None, secure=False): tool_conf = get_test_tool_conf(tool_conf_cls) if not uuid_val: uuid_val = 'test-uuid-1234' login_data = { 'iss': 'https://canvas.instructure.com', 'login_hint': '86157096483e6b3a50bfedc6bac902c0b20a824f', 'target_link_uri': 'http://lti.django.test/launch/', 'lti_message_hint': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ2ZXJpZmllciI6Ijg0NjMxZjc1Z' 'GYxNmNiZjNmYTM5YzEwMzk4YTg0M2U1NTAwZTc5MTU2OTBhN2RjYTJhNGMzMTJjYjR' 'jOWU0YWY5NzE2MWVhYjg4ODhmOWJlNDc2MmViNzUzZDE5ZmI3YWU5N2I2MjAxZWZjM' 'jRmODY4NWE3NjJmY2U0ZWU4MDk4IiwiY2FudmFzX2RvbWFpbiI6ImNhbnZhcy5kb2N' 'rZXIiLCJjb250ZXh0X3R5cGUiOiJDb3Vyc2UiLCJjb250ZXh0X2lkIjoxMDAwMDAwM' 'DAwMDAwMSwiZXhwIjoxNTY1NDQyMzcwfQ.B1Lddgthaa-YBT4-Lkm3OM_noETl3dIz' '5E14YWJ8m_Q' } request = FlaskRequest(request_data=login_data, cookies={}, session={}, request_is_secure=secure) with patch('flask.redirect') as mock_redirect: from pylti1p3.contrib.flask import FlaskOIDCLogin with patch.object(FlaskOIDCLogin, "_get_uuid", autospec=True) as get_uuid: get_uuid.side_effect = lambda x: uuid_val # pylint: disable=unnecessary-lambda oidc_login = FlaskOIDCLogin( request, tool_conf, cookie_service=FlaskCookieService(request), session_service=FlaskSessionService(request)) mock_redirect.side_effect = lambda x: FakeResponse(x) # pylint: disable=unnecessary-lambda launch_url = 'http://lti.django.test/launch/' response = oidc_login.redirect(launch_url) # check cookie data self.assertTrue('Set-Cookie' in response.headers) set_cookie_header = response.headers['Set-Cookie'] expected_cookie = 'lti1p3-state-' + uuid_val + '=state-' + uuid_val self.assertTrue(expected_cookie in set_cookie_header) if secure: self.assertTrue('Secure' in set_cookie_header) self.assertTrue('SameSite=None' in set_cookie_header) else: self.assertFalse('Secure' in set_cookie_header) self.assertFalse('SameSite' in set_cookie_header) # check session data self.assertEqual(len(request.session), 1) self.assertEqual(request.session['lti1p3-nonce-' + uuid_val], True) # check redirect_url redirect_url = response.location self.assertTrue( redirect_url.startswith( TOOL_CONFIG[login_data['iss']]['auth_login_url'])) url_params = redirect_url.split('?')[1].split('&') self.assertTrue(('nonce=' + uuid_val) in url_params) self.assertTrue(('state=state-' + uuid_val) in url_params) self.assertTrue(('state=state-' + uuid_val) in url_params) self.assertTrue('prompt=none' in url_params) self.assertTrue('response_type=id_token' in url_params) self.assertTrue(( 'client_id=' + TOOL_CONFIG[login_data['iss']]['client_id']) in url_params) self.assertTrue(('login_hint=' + login_data['login_hint']) in url_params) self.assertTrue(('lti_message_hint=' + login_data['lti_message_hint']) in url_params) self.assertTrue('scope=openid' in url_params) self.assertTrue('response_mode=form_post' in url_params) self.assertTrue(('redirect_uri=' + quote(launch_url, '')) in url_params) return tool_conf, request, response
def launch(): launch_unique_id = str(request.args.get('launch_id', '')) print("launch_unique_id: " + launch_unique_id) # reload page in case if session cookie is unavailable (chrome samesite issue): # https://chromestatus.com/feature/5088147346030592 # to share GET/POST data between requests we save them into cache session_key = request.cookies.get(app.config['SESSION_COOKIE_NAME'], None) if not session_key: print("session_key: None") else: print("session_key: " + session_key) if not session_key and not launch_unique_id: launch_unique_id = str(uuid.uuid4()) cache.set(launch_unique_id, { 'GET': request.args.to_dict(), 'POST': request.form.to_dict() }, 3600) current_url = request.base_url parsed_url = urlparse(current_url) parsed_url = parsed_url._replace(scheme='https') current_url = parsed_url.geturl() if '?' in current_url: current_url += '&' else: current_url += '?' current_url = current_url + 'launch_id=' + launch_unique_id return '<script type="text/javascript">window.location="%s";</script>' % current_url launch_request = FlaskRequest() if request.method == "GET": launch_data = cache.get(launch_unique_id) print("launch_data: " + str(launch_data)) if not launch_data: raise Exception("Can't restore launch data from cache") request_params_dict = {} request_params_dict.update(launch_data['GET']) request_params_dict.update(launch_data['POST']) print("request_params_dict: " + str(request_params_dict)) launch_request = FlaskRequest(request_data=request_params_dict) print("launch_request: " + str(launch_request)) tool_conf = ToolConfJsonFile(get_lti_config_path()) print("tool_conf: " + str(tool_conf)) message_launch = ExtendedFlaskMessageLaunch(launch_request, tool_conf) print("message_launch: " + str(message_launch)) message_launch_data = message_launch.get_launch_data() pprint.pprint(message_launch_data) learn_url = message_launch_data['https://purl.imsglobal.org/spec/lti/claim/tool_platform']['url'].rstrip('/') tpl_kwargs = { 'page_title': PAGE_TITLE, 'is_deep_link_launch': message_launch.is_deep_link_launch(), 'launch_data': message_launch.get_launch_data(), 'launch_id': message_launch.get_launch_id(), 'family_name': message_launch_data.get('family_name', ''), 'given_name': message_launch_data.get('given_name', ''), 'user_email': message_launch_data.get('email', ''), 'user_uuid': message_launch_data.get('sub', ''), 'learn_url': learn_url } print("tpl_kwargs: " + str(tpl_kwargs)) params = { 'redirect_uri' : 'https://ask-an-expert.herokuapp.com/authcode/', 'response_type' : 'code', 'client_id' : Config.config['learn_rest_key'], 'scope' : '*', 'state' : str(uuid.uuid4()) } encodedParams = urllib.parse.urlencode(params) get_authcode_url = learn_url + '/learn/api/public/v1/oauth2/authorizationcode?' + encodedParams return(redirect(get_authcode_url))