def test_email_to_username_retrieves_only_first_part_before_plus_symbol(): """ Does the email_to_username method remove '+' symbol? """ email = '*****@*****.**' utils = LTIUtils() # act result = utils.email_to_username(email) assert result == 'user'
def test_email_to_username_retrieves_only_username_part_before_at_symbol(): """ Does the email_to_username method remove all after @? """ email = '*****@*****.**' utils = LTIUtils() # act result = utils.email_to_username(email) assert result == 'user1'
def test_email_to_username_converts_username_in_lowecase(): """ Does the email_to_username method convert to lowecase the username part? """ email = '*****@*****.**' utils = LTIUtils() # act result = utils.email_to_username(email) assert result == 'user_name1'
async def authenticate( # noqa: C901 self, handler: LTI13LoginHandler, data: Dict[str, str] = None ) -> Dict[str, str]: """ Overrides authenticate from base class to handle LTI 1.3 authentication requests. Args: handler: handler object data: authentication dictionary Returns: Authentication dictionary """ lti_utils = LTIUtils() validator = LTI13LaunchValidator() # get jwks endpoint and token to use as args to decode jwt. we could pass in # self.endpoint directly as arg to jwt_verify_and_decode() but logging the self.log.debug('JWKS platform endpoint is %s' % self.endpoint) id_token = handler.get_argument('id_token') self.log.debug('ID token issued by platform is %s' % id_token) # extract claims from jwt (id_token) sent by the platform. as tool use the jwks (public key) # to verify the jwt's signature. jwt_decoded = await validator.jwt_verify_and_decode(id_token, self.endpoint, False, audience=self.client_id) self.log.debug('Decoded JWT is %s' % jwt_decoded) if validator.validate_launch_request(jwt_decoded): course_id = jwt_decoded['https://purl.imsglobal.org/spec/lti/claim/context']['label'] course_id = lti_utils.normalize_string(course_id) self.log.debug('Normalized course label is %s' % course_id) username = '' if 'email' in jwt_decoded and jwt_decoded['email']: username = lti_utils.email_to_username(jwt_decoded['email']) elif 'name' in jwt_decoded and jwt_decoded['name']: username = jwt_decoded['name'] elif 'given_name' in jwt_decoded and jwt_decoded['given_name']: username = jwt_decoded['given_name'] elif 'family_name' in jwt_decoded and jwt_decoded['family_name']: username = jwt_decoded['family_name'] elif ( 'https://purl.imsglobal.org/spec/lti/claim/lis' in jwt_decoded and 'person_sourcedid' in jwt_decoded['https://purl.imsglobal.org/spec/lti/claim/lis'] and jwt_decoded['https://purl.imsglobal.org/spec/lti/claim/lis']['person_sourcedid'] ): username = jwt_decoded['https://purl.imsglobal.org/spec/lti/claim/lis']['person_sourcedid'].lower() elif ( 'lms_user_id' in jwt_decoded['https://purl.imsglobal.org/spec/lti/claim/custom'] and jwt_decoded['https://purl.imsglobal.org/spec/lti/claim/custom']['lms_user_id'] ): username = str(jwt_decoded['https://purl.imsglobal.org/spec/lti/claim/custom']['lms_user_id']) self.log.debug('username is %s' % username) # ensure the username is normalized self.log.debug('username is %s' % username) if username == '': raise HTTPError('Unable to set the username') # set role to learner role (by default) if instructor or learner/student roles aren't # sent with the request user_role = 'Learner' for role in jwt_decoded['https://purl.imsglobal.org/spec/lti/claim/roles']: if role.find('Instructor') >= 1: user_role = 'Instructor' elif role.find('Learner') >= 1 or role.find('Student') >= 1: user_role = 'Learner' self.log.debug('user_role is %s' % user_role) launch_return_url = '' if ( 'https://purl.imsglobal.org/spec/lti/claim/launch_presentation' in jwt_decoded and 'return_url' in jwt_decoded['https://purl.imsglobal.org/spec/lti/claim/launch_presentation'] ): launch_return_url = jwt_decoded['https://purl.imsglobal.org/spec/lti/claim/launch_presentation'][ 'return_url' ] # if there is a resource link request then process additional steps if not validator.is_deep_link_launch(jwt_decoded): await process_resource_link(self.log, course_id, jwt_decoded) lms_user_id = jwt_decoded['sub'] if 'sub' in jwt_decoded else username # ensure the user name is normalized username_normalized = lti_utils.normalize_string(username) self.log.debug('Assigned username is: %s' % username_normalized) return { 'name': username_normalized, 'auth_state': { 'course_id': course_id, 'user_role': user_role, 'lms_user_id': lms_user_id, 'launch_return_url': launch_return_url, }, # noqa: E231 }
async def authenticate(self, handler: BaseHandler, data: Dict[str, str] = None) -> Dict[str, str]: # noqa: C901 """ LTI 1.1 authenticator which overrides authenticate function from base LTIAuthenticator. After validating the LTI 1.1 signuature, this function decodes the dictionary object from the request payload to set normalized strings for the course_id, username, user role, and lms user id. Once those values are set they are added to the auth_state and returned as a dictionary for further processing by hooks defined in jupyterhub_config. One or more consumer keys/values must be set in the jupyterhub config with the LTIAuthenticator.consumers dict. Args: handler: JupyterHub's Authenticator handler object. For LTI 1.1 requests, the handler is an instance of LTIAuthenticateHandler. data: optional data object Returns: Authentication's auth_state dictionary Raises: HTTPError if the required values are not in the request """ validator = LTI11LaunchValidator(self.consumers) lti_utils = LTIUtils() self.log.debug('Original arguments received in request: %s' % handler.request.arguments) # extract the request arguments to a dict args = lti_utils.convert_request_to_dict(handler.request.arguments) self.log.debug('Decoded args from request: %s' % args) # get the origin protocol protocol = lti_utils.get_client_protocol(handler) self.log.debug('Origin protocol is: %s' % protocol) # build the full launch url value required for oauth1 signatures launch_url = f'{protocol}://{handler.request.host}{handler.request.uri}' self.log.debug('Launch url is: %s' % launch_url) if validator.validate_launch_request(launch_url, handler.request.headers, args): # get the lms vendor to implement optional logic for said vendor lms_vendor = '' if 'tool_consumer_info_product_family_code' in args and args['tool_consumer_info_product_family_code']: lms_vendor = args['tool_consumer_info_product_family_code'] # We use the course_id to setup the grader service notebook. Since this service # runs as a container we need to normalize the string so we can use it # as a container name. if 'context_label' in args and args['context_label']: course_id = args['context_label'] self.log.debug('Course context_label normalized to: %s' % course_id) elif 'context_title' in args and args['context_title']: course_id = args['context_title'] self.log.debug('Course context_title normalized to: %s' % course_id) else: raise HTTPError(400, 'course_label or course_title not included in the LTI request') # Get the user's role, assign to Learner role by default. Roles are sent as institution # roles, where the roles' value is <handle>,<full URN>. # https://www.imsglobal.org/specs/ltiv1p0/implementation-guide#toc-16 user_role = 'Learner' if 'roles' in args and args['roles']: user_role = args['roles'].split(',')[0] self.log.debug('User LTI role is: %s' % user_role) else: raise HTTPError(400, 'User role not included in the LTI request') # Assign the user_id. Check the tool consumer (lms) vendor. If canvas use their # custom user id extension by default, else use standar lti values. username = '' if lms_vendor == 'canvas': login_id = '' user_id = '' self.log.debug('TC is a Canvas LMS instance') if ( 'custom_canvas_user_login_id' in args and args['custom_canvas_user_login_id'] and 'custom_canvas_user_id' in args and args['custom_canvas_user_id'] ): custom_canvas_user_login_id = args['custom_canvas_user_login_id'] login_id = lti_utils.email_to_username(custom_canvas_user_login_id) self.log.debug('using custom_canvas_user_login_id for username') if 'custom_canvas_user_id' in args and args['custom_canvas_user_id']: custom_canvas_user_id = args['custom_canvas_user_id'] user_id = lti_utils.email_to_username(custom_canvas_user_id) self.log.debug('using custom_canvas_user_id for username') username = f'{login_id}-{user_id}' if ( not username and 'lis_person_contact_email_primary' in args and args['lis_person_contact_email_primary'] ): email = args['lis_person_contact_email_primary'] username = lti_utils.email_to_username(email) self.log.debug('using lis_person_contact_email_primary for username') elif not username and 'lis_person_name_given' in args and args['lis_person_name_given']: username = args['lis_person_name_given'] self.log.debug('using lis_person_name_given for username') elif not username and 'lis_person_sourcedid' in args and args['lis_person_sourcedid']: username = args['lis_person_sourcedid'] self.log.debug('using lis_person_sourcedid for username') elif not username and 'lis_person_name_family' in args and args['lis_person_name_family']: username = args['lis_person_name_family'] self.log.debug('using lis_person_name_family for username') elif not username and 'lis_person_name_full' in args and args['lis_person_name_full']: username = args['lis_person_name_full'] self.log.debug('using lis_person_name_full for username') elif not username and 'user_id' in args and args['user_id']: username = args['user_id'] elif not username: raise HTTPError(400, 'Unable to get username from request arguments') # use the user_id to identify the unique user id, if its not sent with the request # then default to the username lms_user_id = args['user_id'] if 'user_id' in args else username # GRADES-SENDER: fetch the information needed to register assignments within the control file # retrieve assignment_name from standard property vs custom lms properties assignment_name = '' # the next fields must come in args if 'custom_canvas_assignment_title' in args and args['custom_canvas_assignment_title']: assignment_name = lti_utils.normalize_string(args['custom_canvas_assignment_title']) # this requires adding a the assignment_title as a custom parameter in the tool consumer (lms) elif 'custom_assignment_title' in args and args['custom_assignment_title']: assignment_name = lti_utils.normalize_string(args['custom_assignment_title']) elif 'resource_link_title' in args and args['resource_link_title']: assignment_name = lti_utils.normalize_string(args['resource_link_title']) elif 'resource_link_id' in args and args['resource_link_id']: assignment_name = lti_utils.normalize_string(args['resource_link_id']) # Get lis_outcome_service_url and lis_result_sourcedid values that will help us to submit grades later lis_outcome_service_url = '' lis_result_sourcedid = '' # the next fields must come in args if 'lis_outcome_service_url' in args and args['lis_outcome_service_url']: lis_outcome_service_url = args['lis_outcome_service_url'] if 'lis_result_sourcedid' in args and args['lis_result_sourcedid']: lis_result_sourcedid = args['lis_result_sourcedid'] # only if both values exist we can register them to submit grades later if lis_outcome_service_url and lis_result_sourcedid: control_file = LTIGradesSenderControlFile(f'/home/grader-{course_id}/{course_id}') control_file.register_data(assignment_name, lis_outcome_service_url, lms_user_id, lis_result_sourcedid) # Assignment creation if assignment_name: nbgrader_service = NbGraderServiceHelper(course_id, True) self.log.debug( 'Creating a new assignment from the Authentication flow with title %s' % assignment_name ) nbgrader_service.register_assignment(assignment_name) # ensure the user name is normalized username_normalized = lti_utils.normalize_string(username) self.log.debug('Assigned username is: %s' % username_normalized) return { 'name': username_normalized, 'auth_state': { 'course_id': course_id, 'lms_user_id': lms_user_id, 'user_role': user_role, }, # noqa: E231 }
async def authenticate( # noqa: C901 self, handler: LTI13LoginHandler, data: Dict[str, str] = None) -> Dict[str, str]: """ Overrides authenticate from base class to handle LTI 1.3 authentication requests. Args: handler: handler object data: authentication dictionary Returns: Authentication dictionary """ lti_utils = LTIUtils() validator = LTI13LaunchValidator() # get jwks endpoint and token to use as args to decode jwt. we could pass in # self.endpoint directly as arg to jwt_verify_and_decode() but logging the self.log.debug("JWKS platform endpoint is %s" % self.endpoint) id_token = handler.get_argument("id_token") self.log.debug("ID token issued by platform is %s" % id_token) # extract claims from jwt (id_token) sent by the platform. as tool use the jwks (public key) # to verify the jwt's signature. jwt_decoded = await validator.jwt_verify_and_decode( id_token, self.endpoint, False, audience=self.client_id) self.log.debug("Decoded JWT is %s" % jwt_decoded) if validator.validate_launch_request(jwt_decoded): course_id = jwt_decoded[ "https://purl.imsglobal.org/spec/lti/claim/context"]["label"] course_id = lti_utils.normalize_string(course_id) self.log.debug("Normalized course label is %s" % course_id) username = "" if "email" in jwt_decoded and jwt_decoded["email"]: username = lti_utils.email_to_username(jwt_decoded["email"]) elif "name" in jwt_decoded and jwt_decoded["name"]: username = jwt_decoded["name"] elif "given_name" in jwt_decoded and jwt_decoded["given_name"]: username = jwt_decoded["given_name"] elif "family_name" in jwt_decoded and jwt_decoded["family_name"]: username = jwt_decoded["family_name"] elif ("https://purl.imsglobal.org/spec/lti/claim/lis" in jwt_decoded and "person_sourcedid" in jwt_decoded["https://purl.imsglobal.org/spec/lti/claim/lis"] and jwt_decoded["https://purl.imsglobal.org/spec/lti/claim/lis"] ["person_sourcedid"]): username = jwt_decoded[ "https://purl.imsglobal.org/spec/lti/claim/lis"][ "person_sourcedid"].lower() elif ("lms_user_id" in jwt_decoded[ "https://purl.imsglobal.org/spec/lti/claim/custom"] and jwt_decoded[ "https://purl.imsglobal.org/spec/lti/claim/custom"] ["lms_user_id"]): username = str(jwt_decoded[ "https://purl.imsglobal.org/spec/lti/claim/custom"] ["lms_user_id"]) # set role to learner role (by default) if instructor or learner/student roles aren't # sent with the request user_role = "Learner" for role in jwt_decoded[ "https://purl.imsglobal.org/spec/lti/claim/roles"]: if role.find("Instructor") >= 1: user_role = "Instructor" elif role.find("Learner") >= 1 or role.find("Student") >= 1: user_role = "Learner" self.log.debug("user_role is %s" % user_role) launch_return_url = "" if ("https://purl.imsglobal.org/spec/lti/claim/launch_presentation" in jwt_decoded and "return_url" in jwt_decoded[ "https://purl.imsglobal.org/spec/lti/claim/launch_presentation"] ): launch_return_url = jwt_decoded[ "https://purl.imsglobal.org/spec/lti/claim/launch_presentation"][ "return_url"] # if there is a resource link request then process additional steps if not validator.is_deep_link_launch(jwt_decoded): await process_resource_link_lti_13(self.log, course_id, jwt_decoded) lms_user_id = jwt_decoded[ "sub"] if "sub" in jwt_decoded else username # ensure the username is normalized self.log.debug("username is %s" % username) if not username: raise HTTPError(400, "Unable to set the username") # ensure the user name is normalized username_normalized = lti_utils.normalize_string(username) self.log.debug("Assigned username is: %s" % username_normalized) return { "name": username_normalized, "auth_state": { "course_id": course_id, "user_role": user_role, "lms_user_id": lms_user_id, "launch_return_url": launch_return_url, }, # noqa: E231 }
async def authenticate( # noqa: C901 self, handler: LTI13LoginHandler, data: Dict[str, str] = None) -> Dict[str, str]: """ Overrides authenticate from base class to handle LTI 1.3 authentication requests. Args: handler: handler object data: authentication dictionary Returns: Authentication dictionary """ lti_utils = LTIUtils() validator = LTI13LaunchValidator() # get jwks endpoint and token to use as args to decode jwt. we could pass in # self.endpoint directly as arg to jwt_verify_and_decode() but logging the self.log.debug('JWKS platform endpoint is %s' % self.endpoint) id_token = handler.get_argument('id_token') self.log.debug('ID token issued by platform is %s' % id_token) # extract claims from jwt (id_token) sent by the platform. as tool use the jwks (public key) # to verify the jwt's signature. jwt_decoded = await validator.jwt_verify_and_decode( id_token, self.endpoint, False, audience=self.client_id) self.log.debug('Decoded JWT is %s' % jwt_decoded) if validator.validate_launch_request(jwt_decoded): course_id = jwt_decoded[ 'https://purl.imsglobal.org/spec/lti/claim/context']['label'] self.log.debug('Normalized course label is %s' % course_id) username = '' if 'email' in jwt_decoded.keys() and jwt_decoded.get('email'): username = lti_utils.email_to_username(jwt_decoded['email']) elif 'name' in jwt_decoded.keys() and jwt_decoded.get('name'): username = jwt_decoded.get('name') elif 'given_name' in jwt_decoded.keys() and jwt_decoded.get( 'given_name'): username = jwt_decoded.get('given_name') elif 'family_name' in jwt_decoded.keys() and jwt_decoded.get( 'family_name'): username = jwt_decoded.get('family_name') elif ('person_sourcedid' in jwt_decoded['https://purl.imsglobal.org/spec/lti/claim/lis'] and jwt_decoded['https://purl.imsglobal.org/spec/lti/claim/lis'] ['person_sourcedid']): username = jwt_decoded[ 'https://purl.imsglobal.org/spec/lti/claim/lis'][ 'person_sourcedid'].lower() if username == '': raise HTTPError('Unable to set the username') self.log.debug('username is %s' % username) # assign a workspace type, if provided, otherwise defaults to jupyter classic nb workspace_type = '' if ('https://purl.imsglobal.org/spec/lti/claim/custom' in jwt_decoded and jwt_decoded[ 'https://purl.imsglobal.org/spec/lti/claim/custom'] is not None): if ('workspace_type' in jwt_decoded[ 'https://purl.imsglobal.org/spec/lti/claim/custom'] and jwt_decoded[ 'https://purl.imsglobal.org/spec/lti/claim/custom'] ['workspace_type'] is not None): workspace_type = jwt_decoded[ 'https://purl.imsglobal.org/spec/lti/claim/custom'][ 'workspace_type'] if workspace_type not in WORKSPACE_TYPES: workspace_type = 'notebook' self.log.debug('workspace type is %s' % workspace_type) user_role = '' for role in jwt_decoded[ 'https://purl.imsglobal.org/spec/lti/claim/roles']: if role.find('Instructor') >= 1: user_role = 'Instructor' elif role.find('Learner') >= 1 or role.find('Student') >= 1: user_role = 'Learner' # set role to learner role if instructor or learner/student roles aren't # sent with the request if user_role == '': user_role = 'Learner' self.log.debug('user_role is %s' % user_role) lms_user_id = jwt_decoded[ 'sub'] if 'sub' in jwt_decoded else username # Values for the send-grades functionality course_lineitems = '' if 'https://purl.imsglobal.org/spec/lti-ags/claim/endpoint' in jwt_decoded: course_lineitems = jwt_decoded[ 'https://purl.imsglobal.org/spec/lti-ags/claim/endpoint'][ 'lineitems'] return { 'name': username, 'auth_state': { 'course_id': course_id, 'user_role': user_role, 'workspace_type': workspace_type, 'course_lineitems': course_lineitems, 'user_role': user_role, 'lms_user_id': lms_user_id, }, # noqa: E231 }
async def authenticate( self, handler: BaseHandler, data: Dict[str, str] = None) -> Dict[str, str]: # noqa: C901 """ LTI 1.1 authenticator which overrides authenticate function from base LTIAuthenticator. After validating the LTI 1.1 signuature, this function decodes the dictionary object from the request payload to set normalized strings for the course_id, username, user role, and lms user id. Once those values are set they are added to the auth_state and returned as a dictionary for further processing by hooks defined in jupyterhub_config. One or more consumer keys/values must be set in the jupyterhub config with the LTIAuthenticator.consumers dict. Args: handler: JupyterHub's Authenticator handler object. For LTI 1.1 requests, the handler is an instance of LTIAuthenticateHandler. data: optional data object Returns: Authentication's auth_state dictionary Raises: HTTPError if the required values are not in the request """ validator = LTI11LaunchValidator(self.consumers) lti_utils = LTIUtils() self.log.debug('Original arguments received in request: %s' % handler.request.arguments) # extract the request arguments to a dict args = lti_utils.convert_request_to_dict(handler.request.arguments) self.log.debug('Decoded args from request: %s' % args) # get the origin protocol protocol = lti_utils.get_client_protocol(handler) self.log.debug('Origin protocol is: %s' % protocol) # build the full launch url value required for oauth1 signatures launch_url = f'{protocol}://{handler.request.host}{handler.request.uri}' self.log.debug('Launch url is: %s' % launch_url) if validator.validate_launch_request(launch_url, handler.request.headers, args): # get the lms vendor to implement optional logic for said vendor lms_vendor = '' if ('tool_consumer_info_product_family_code' in args and args['tool_consumer_info_product_family_code'] is not None): lms_vendor = args['tool_consumer_info_product_family_code'] # We use the course_id to setup the grader service notebook. Since this service # runs as a docker container we need to normalize the string so we can use it # as a container name. if 'context_label' in args and args['context_label'] is not None: course_id = args['context_label'] self.log.debug('Course context_label normalized to: %s' % course_id) else: raise HTTPError( 400, 'Course label not included in the LTI request') # Users have the option to initiate a launch request with the workspace_type they would like # to launch. edX prepends arguments with custom_*, so we need to check for those too. workspace_type = '' if 'custom_workspace_type' in args or 'workspace_type' in args: workspace_type = (args['custom_workspace_type'] if 'custom_workspace_type' in args else args['workspace_type']) if workspace_type is None or workspace_type not in WORKSPACE_TYPES: workspace_type = 'notebook' self.log.debug('Workspace type assigned as: %s' % workspace_type) # Get the user's role, assign to Learner role by default. Roles are sent as institution # roles, where the roles' value is <handle>,<full URN>. # https://www.imsglobal.org/specs/ltiv1p0/implementation-guide#toc-16 user_role = 'Learner' if 'roles' in args and args['roles'] is not None: user_role = args['roles'].split(',')[0] self.log.debug('User LTI role is: %s' % user_role) else: raise HTTPError(400, 'User role not included in the LTI request') # Assign the user_id. Check the tool consumer (lms) vendor. If canvas use their # custom user id extension by default, else use standar lti values. username = '' # GRADES-SENDER: retrieve assignment_name from standard property vs custom lms # properties, such as custom_canvas_... assignment_name = args[ 'resource_link_title'] if 'resource_link_title' in args else 'unknown' if lms_vendor == 'canvas': self.log.debug('TC is a Canvas LMS instance') if 'custom_canvas_user_login_id' in args and args[ 'custom_canvas_user_login_id'] is not None: custom_canvas_user_id = args['custom_canvas_user_login_id'] username = lti_utils.email_to_username( custom_canvas_user_id) self.log.debug('using custom_canvas_user_id for username') # GRADES-SENDER >>>> retrieve assignment_name from custom property assignment_name = (args['custom_canvas_assignment_title'] if 'custom_canvas_assignment_title' in args else 'unknown') else: if (username == '' and 'lis_person_contact_email_primary' in args and args['lis_person_contact_email_primary'] is not None): email = args['lis_person_contact_email_primary'] username = lti_utils.email_to_username(email) self.log.debug( 'using lis_person_contact_email_primary for username') elif 'lis_person_name_given' in args and args[ 'lis_person_name_given'] is not None: username = args['lis_person_name_given'] self.log.debug('using lis_person_name_given for username') elif 'lis_person_sourcedid' in args and args[ 'lis_person_sourcedid'] is not None: username = args['lis_person_sourcedid'] self.log.debug('using lis_person_sourcedid for username') if username == '': self.log.debug('using user_id for username') if 'user_id' in args and args['user_id'] is not None: username = args['user_id'] else: raise HTTPError( 400, 'Unable to get username from request arguments') self.log.debug('Assigned username is: %s' % username) # use the user_id to identify the unique user id, if its not sent with the request # then default to the username lms_user_id = args['user_id'] if 'user_id' in args else username # with all info extracted from lms request, register info for grades sender only if the user has # the Learner role lis_outcome_service_url = None lis_result_sourcedid = None if user_role == 'Learner' or user_role == 'Student': # the next fields must come in args if 'lis_outcome_service_url' in args and args[ 'lis_outcome_service_url'] is not None: lis_outcome_service_url = args['lis_outcome_service_url'] if 'lis_result_sourcedid' in args and args[ 'lis_result_sourcedid'] is not None: lis_result_sourcedid = args['lis_result_sourcedid'] # only if both values exist we can register them to submit grades later if lis_outcome_service_url and lis_result_sourcedid: control_file = LTIGradesSenderControlFile( f'/home/grader-{course_id}/{course_id}') control_file.register_data(assignment_name, lis_outcome_service_url, lms_user_id, lis_result_sourcedid) return { 'name': username, 'auth_state': { 'course_id': course_id, 'lms_user_id': lms_user_id, 'user_role': user_role, 'workspace_type': workspace_type, }, # noqa: E231 }