Пример #1
0
 def post(self):
     """
     Validates required login arguments sent from platform and then uses the authorize_redirect() method
     to redirect users to the authorization url.
     """
     lti_utils = LTIUtils()
     validator = LTI13LaunchValidator()
     args = lti_utils.convert_request_to_dict(self.request.arguments)
     self.log.debug('Initial login request args are %s' % args)
     if validator.validate_login_request(args):
         login_hint = args['login_hint']
         self.log.debug('login_hint is %s' % login_hint)
         lti_message_hint = args['lti_message_hint']
         self.log.debug('lti_message_hint is %s' % lti_message_hint)
         client_id = args['client_id']
         self.log.debug('client_id is %s' % client_id)
         redirect_uri = guess_callback_uri('https', self.request.host, self.hub.server.base_url)
         self.log.info('redirect_uri: %r', redirect_uri)
         state = self.get_state()
         self.set_state_cookie(state)
         # TODO: validate that received nonces haven't been received before
         # and that they are within the time-based tolerance window
         nonce_raw = hashlib.sha256(state.encode())
         nonce = nonce_raw.hexdigest()
         self.authorize_redirect(
             client_id=client_id,
             login_hint=login_hint,
             lti_message_hint=lti_message_hint,
             nonce=nonce,
             redirect_uri=redirect_uri,
             state=state,
         )
Пример #2
0
def test_validate_with_required_params_in_initial_auth_request(lti13_login_params):
    """
    Is the JWT valid with an correct message type claim?
    """
    validator = LTI13LaunchValidator()
    result = validator.validate_login_request(lti13_login_params)
    assert result is True
def test_validate_empty_target_link_uri_request_claim_value():
    """
    Is the JWT valid with an empty target link uri claim?
    """
    validator = LTI13LaunchValidator()
    jws = factory_lti13_resource_link_request()
    jws['https://purl.imsglobal.org/spec/lti/claim/target_link_uri'] = ''

    assert validator.validate_launch_request(jws) is True
def test_validate_empty_deployment_id_request_claim_value():
    """
    Is the JWT valid with an empty deployment_id claim?
    """
    validator = LTI13LaunchValidator()
    jws = factory_lti13_resource_link_request()
    jws['https://purl.imsglobal.org/spec/lti/claim/deployment_id'] = ''

    assert validator.validate_launch_request(jws) is True
Пример #5
0
def test_validate_empty_roles_claim_value(make_lti13_resource_link_request):
    """
    Is the JWT valid with an empty roles claim value?
    """
    validator = LTI13LaunchValidator()
    jws = make_lti13_resource_link_request
    jws["https://purl.imsglobal.org/spec/lti/claim/roles"] = ""

    assert validator.validate_launch_request(jws)
def test_validate_claim_values_with_privacy_enabled(
        make_lti13_resource_link_request_privacy_enabled):
    """
    Is the JWT valid when privacy is enabled?
    """
    validator = LTI13LaunchValidator()
    jws = make_lti13_resource_link_request_privacy_enabled

    assert validator.validate_launch_request(jws)
Пример #7
0
def test_validate_empty_conext_label_claim_value(make_lti13_resource_link_request):
    """
    Is the JWT valid with an empty context label claim?
    """
    validator = LTI13LaunchValidator()
    jws = make_lti13_resource_link_request
    jws["https://purl.imsglobal.org/spec/lti/claim/context"]["label"] = ""

    with pytest.raises(HTTPError):
        validator.validate_launch_request(jws)
Пример #8
0
def test_validate_invalid_version_request_claim_value(make_lti13_resource_link_request):
    """
    Is the JWT valid with an incorrect version claim?
    """
    validator = LTI13LaunchValidator()
    jws = make_lti13_resource_link_request
    jws["https://purl.imsglobal.org/spec/lti/claim/version"] = "1.0.0"

    with pytest.raises(HTTPError):
        validator.validate_launch_request(jws)
def test_validate_invalid_resource_link_request_message_type_claim_value():
    """
    Is the JWT valid with an incorrect message type claim?
    """
    validator = LTI13LaunchValidator()
    jws = factory_lti13_resource_link_request()
    jws['https://purl.imsglobal.org/spec/lti/claim/message_type'] = 'FakeLinkRequest'

    with pytest.raises(HTTPError):
        validator.validate_launch_request(jws)
def test_validate_empty_context_label_request_claim_value():
    """
    Is the JWT valid with an empty resource request id uri claim?
    """
    validator = LTI13LaunchValidator()
    jws = factory_lti13_resource_link_request()
    jws['https://purl.imsglobal.org/spec/lti/claim/context']['label'] = ''

    with pytest.raises(HTTPError):
        validator.validate_launch_request(jws)
Пример #11
0
def test_validate_deep_linking_request_is_valid_with_message_type_claim(
        make_lti13_resource_link_request):
    """
    Is the JWT valid with for LtiDeepLinkingRequest?
    """
    validator = LTI13LaunchValidator()
    jws = make_lti13_resource_link_request
    jws['https://purl.imsglobal.org/spec/lti/claim/message_type'] = 'LtiDeepLinkingRequest'

    assert validator.validate_launch_request(jws)
Пример #12
0
def test_validate_missing_required_claims_in_step_2_resource_link_request():
    """
    Is the JWT valid with an incorrect message type claim?
    """
    validator = LTI13LaunchValidator()
    fake_jws = {
        "key1": "value1",
    }

    with pytest.raises(HTTPError):
        validator.validate_login_request(fake_jws)
Пример #13
0
def test_validate_empty_resource_link_id_request_claim_value(
        make_lti13_resource_link_request):
    """
    Is the JWT valid with an empty resource request id uri claim?
    """
    validator = LTI13LaunchValidator()
    jws = make_lti13_resource_link_request
    jws['https://purl.imsglobal.org/spec/lti/claim/resource_link']['id'] = ''

    with pytest.raises(HTTPError):
        validator.validate_launch_request(jws)
async def test_validator_jwt_verify_and_decode_invokes_retrieve_matching_jwk():
    """
    Does the validator jwt_verify_and_decode method invoke the retrieve_matching_jwk method?
    """
    validator = LTI13LaunchValidator()
    jwks_endoint = 'https://my.platform.domain/api/lti/security/jwks'
    with patch.object(
        validator, '_retrieve_matching_jwk', return_value=factory_lti13_platform_jwks()
    ) as mock_retrieve_matching_jwks:
        _ = await validator.jwt_verify_and_decode(dummy_lti13_id_token_complete, jwks_endoint, True)

        assert mock_retrieve_matching_jwks.called
async def test_validator_jwt_verify_and_decode_returns_none_with_no_retrieved_platform_keys():
    """
    Does the validator jwt_verify_and_decode method return None when no keys are returned from the
    retrieve_matching_jwk method?
    """
    validator = LTI13LaunchValidator()
    jwks_endoint = 'https://my.platform.domain/api/lti/security/jwks'
    with patch.object(
        validator, '_retrieve_matching_jwk', return_value=factory_lti13_empty_platform_jwks()
    ) as mock_retrieve_matching_jwks:
        result = await validator.jwt_verify_and_decode(dummy_lti13_id_token_complete, jwks_endoint, True)

    assert result is None
Пример #16
0
def test_validate_resource_ling_is_not_required_for_deep_linking_request(
    make_lti13_resource_link_request,
):
    """
    Is the JWT valid with for LtiDeepLinkingRequest?
    """
    validator = LTI13LaunchValidator()
    jws = make_lti13_resource_link_request
    jws[
        "https://purl.imsglobal.org/spec/lti/claim/message_type"
    ] = "LtiDeepLinkingRequest"
    del jws["https://purl.imsglobal.org/spec/lti/claim/resource_link"]

    assert validator.validate_launch_request(jws)
Пример #17
0
async def test_validator_jwt_verify_and_decode_invokes_retrieve_matching_jwk(
        make_lti13_resource_link_request, build_lti13_jwt_id_token):
    """
    Does the validator jwt_verify_and_decode method invoke the retrieve_matching_jwk method?
    """
    validator = LTI13LaunchValidator()
    jwks_endoint = 'https://my.platform.domain/api/lti/security/jwks'
    with patch.object(validator, '_retrieve_matching_jwk',
                      return_value=None) as mock_retrieve_matching_jwks:
        _ = await validator.jwt_verify_and_decode(
            build_lti13_jwt_id_token(make_lti13_resource_link_request),
            jwks_endoint, True)

        assert mock_retrieve_matching_jwks.called
Пример #18
0
async def test_validator_jwt_verify_and_decode_raises_an_error_with_no_retrieved_platform_keys(
        http_async_httpclient_with_simple_response,
        make_lti13_resource_link_request, build_lti13_jwt_id_token):
    """
    Does the validator jwt_verify_and_decode method return None when no keys are returned from the
    retrieve_matching_jwk method?
    """
    validator = LTI13LaunchValidator()
    jwks_endoint = 'https://my.platform.domain/api/lti/security/jwks'

    with (pytest.raises(ValueError)):
        await validator.jwt_verify_and_decode(
            build_lti13_jwt_id_token(make_lti13_resource_link_request),
            jwks_endoint, True)
Пример #19
0
    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
            }
Пример #20
0
    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
            }
Пример #21
0
    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
            }