def test_configurable_userinfo_endpoint_method_is_used(self, userinfo_http_method):
        userinfo_endpoint = self.PROVIDER_BASEURL + '/userinfo'
        userinfo_response = OpenIDSchema(sub='user1')
        responses.add(userinfo_http_method, userinfo_endpoint, json=userinfo_response.to_dict())

        provider_metadata = self.PROVIDER_METADATA.copy(userinfo_endpoint=userinfo_endpoint)
        facade = PyoidcFacade(ProviderConfiguration(provider_metadata=provider_metadata,
                                                    client_metadata=self.CLIENT_METADATA,
                                                    userinfo_http_method=userinfo_http_method),
                              self.REDIRECT_URI)
        assert facade.userinfo_request('test_token') == userinfo_response
Exemple #2
0
 def test_should_detect_mismatching_subject(self, client_mock):
     client_mock.exchange_authorization_code.return_value = AccessTokenResponse(**self.TOKEN_RESPONSE)
     client_mock.userinfo_request.return_value = OpenIDSchema(**{'sub': 'other_sub'})
     with pytest.raises(AuthResponseMismatchingSubjectError):
         AuthResponseHandler(client_mock).process_auth_response(AuthorizationResponse(**self.AUTH_RESPONSE),
                                                                self.AUTH_RESPONSE['state'],
                                                                self.TOKEN_RESPONSE['id_token']['nonce'])
Exemple #3
0
    def __call__(self, userid, client_id, user_info_claims=None, **kwargs):
        """
        :param userid: The local user id
        :param user_info_claims: Possible userinfo claims (a dictionary)
        :return: A schema dependent userinfo instance
        """

        logger.info("User_info about '%s'" % userid)
        identity = copy.copy(self.db[userid])

        if user_info_claims:
            result = {}
            missing = []
            optional = []
            if "claims" in user_info_claims:
                for key, restr in user_info_claims["claims"].items():
                    try:
                        result[key] = identity[key]
                    except KeyError:
                        if restr == {"essential": True}:
                            missing.append(key)
                        else:
                            optional.append(key)

            # Check if anything asked for is somewhere else
            if (missing or optional) and "_external_" in identity:
                cpoints = {}
                remaining = missing[:]
                missing.extend(optional)
                for key in missing:
                    for _srv, what in identity["_external_"].items():
                        if key in what:
                            try:
                                cpoints[_srv].append(key)
                            except KeyError:
                                cpoints[_srv] = [key]
                            try:
                                remaining.remove(key)
                            except ValueError:
                                pass

                if remaining:
                    raise MissingAttribute("Missing properties '%s'" %
                                           remaining)

                for srv, what in cpoints.items():
                    cc = self.oidcsrv.claims_clients[srv]
                    logger.debug("srv: %s, what: %s" %
                                 (sanitize(srv), sanitize(what)))
                    _res = self._collect_distributed(srv, cc, userid, what)
                    logger.debug("Got: %s" % sanitize(_res))
                    for key, val in _res.items():
                        if key in result:
                            result[key].update(val)
                        else:
                            result[key] = val

        else:
            # default is what "openid" demands which is sub
            # result = identity
            result = {"sub": userid}

        return OpenIDSchema(**result)
class TestAuthResponseHandler:
    AUTH_RESPONSE = AuthorizationResponse(**{
        'code': 'test_auth_code',
        'state': 'test_state'
    })
    TOKEN_RESPONSE = AccessTokenResponse(
        **{
            'access_token': 'test_token',
            'id_token': IdToken(**{
                'sub': 'test_sub',
                'nonce': 'test_nonce'
            }),
            'id_token_jwt': 'test_id_token_jwt'
        })
    USERINFO_RESPONSE = OpenIDSchema(**{'sub': 'test_sub'})
    ERROR_RESPONSE = {
        'error': 'test_error',
        'error_description': 'something went wrong'
    }

    @pytest.fixture
    def client_mock(self):
        return create_autospec(PyoidcFacade, True, True)

    def test_should_detect_state_mismatch(self, client_mock):
        with pytest.raises(AuthResponseUnexpectedStateError):
            AuthResponseHandler(client_mock).process_auth_response(
                self.AUTH_RESPONSE, 'other_state')

    def test_should_detect_nonce_mismatch(self, client_mock):
        client_mock.token_request.return_value = self.TOKEN_RESPONSE
        with pytest.raises(AuthResponseUnexpectedNonceError):
            AuthResponseHandler(client_mock).process_auth_response(
                self.AUTH_RESPONSE, self.AUTH_RESPONSE['state'], 'other_nonce')

    def test_should_handle_auth_error_response(self, client_mock):
        with pytest.raises(AuthResponseErrorResponseError) as exc:
            AuthResponseHandler(client_mock).process_auth_response(
                AuthorizationErrorResponse(**self.ERROR_RESPONSE),
                self.AUTH_RESPONSE['state'])
        assert exc.value.error_response == self.ERROR_RESPONSE

    def test_should_handle_token_error_response(self, client_mock):
        client_mock.token_request.return_value = TokenErrorResponse(
            **self.ERROR_RESPONSE)
        with pytest.raises(AuthResponseErrorResponseError) as exc:
            AuthResponseHandler(client_mock).process_auth_response(
                AuthorizationResponse(**self.AUTH_RESPONSE),
                self.AUTH_RESPONSE['state'])
        assert exc.value.error_response == self.ERROR_RESPONSE

    def test_should_detect_mismatching_subject(self, client_mock):
        client_mock.token_request.return_value = AccessTokenResponse(
            **self.TOKEN_RESPONSE)
        client_mock.userinfo_request.return_value = OpenIDSchema(
            **{'sub': 'other_sub'})
        with pytest.raises(AuthResponseMismatchingSubjectError):
            AuthResponseHandler(client_mock).process_auth_response(
                AuthorizationResponse(**self.AUTH_RESPONSE),
                self.AUTH_RESPONSE['state'],
                self.TOKEN_RESPONSE['id_token']['nonce'])

    def test_should_handle_auth_response_with_authorization_code(
            self, client_mock):
        client_mock.token_request.return_value = self.TOKEN_RESPONSE
        client_mock.userinfo_request.return_value = self.USERINFO_RESPONSE
        result = AuthResponseHandler(client_mock).process_auth_response(
            self.AUTH_RESPONSE, self.AUTH_RESPONSE['state'],
            self.TOKEN_RESPONSE['id_token']['nonce'])
        assert result.access_token == 'test_token'
        assert result.id_token_claims == self.TOKEN_RESPONSE[
            'id_token'].to_dict()
        assert result.id_token_jwt == self.TOKEN_RESPONSE['id_token_jwt']
        assert result.userinfo_claims == self.USERINFO_RESPONSE.to_dict()

    def test_should_handle_auth_response_without_authorization_code(
            self, client_mock):
        auth_response = AuthorizationResponse(**self.TOKEN_RESPONSE)
        auth_response['state'] = 'test_state'
        client_mock.userinfo_request.return_value = self.USERINFO_RESPONSE
        result = AuthResponseHandler(client_mock).process_auth_response(
            auth_response, 'test_state')
        assert not client_mock.token_request.called
        assert result.access_token == 'test_token'
        assert result.id_token_jwt == self.TOKEN_RESPONSE['id_token_jwt']
        assert result.id_token_claims == self.TOKEN_RESPONSE[
            'id_token'].to_dict()
        assert result.userinfo_claims == self.USERINFO_RESPONSE.to_dict()

    def test_should_handle_token_response_without_id_token(self, client_mock):
        token_response = {'access_token': 'test_token'}
        client_mock.token_request.return_value = AccessTokenResponse(
            **token_response)
        result = AuthResponseHandler(client_mock).process_auth_response(
            AuthorizationResponse(**self.AUTH_RESPONSE),
            self.AUTH_RESPONSE['state'],
            self.TOKEN_RESPONSE['id_token']['nonce'])
        assert result.access_token == 'test_token'
        assert result.id_token_claims is None

    def test_should_handle_no_token_response(self, client_mock):
        client_mock.token_request.return_value = None
        client_mock.userinfo_request.return_value = None
        hybrid_auth_response = self.AUTH_RESPONSE.copy()
        hybrid_auth_response.update(self.TOKEN_RESPONSE)
        result = AuthResponseHandler(client_mock).process_auth_response(
            AuthorizationResponse(**hybrid_auth_response),
            self.AUTH_RESPONSE['state'],
            self.TOKEN_RESPONSE['id_token']['nonce'])
        assert result.access_token == 'test_token'
        assert result.id_token_claims == self.TOKEN_RESPONSE[
            'id_token'].to_dict()
        assert result.id_token_jwt == self.TOKEN_RESPONSE['id_token_jwt']

    @pytest.mark.parametrize(
        'response_type, expected',
        [
            ('code', False),  # Authorization Code Flow
            ('id_token', True),  # Implicit Flow
            ('id_token token', True),  # Implicit Flow
            ('code id_token', True),  # Hybrid Flow
            ('code token', True),  # Hybrid Flow
            ('code id_token token', True)  # Hybrid Flow
        ])
    def test_expect_fragment_encoded_response_by_response_type(
            self, response_type, expected):
        assert AuthResponseHandler.expect_fragment_encoded_response(
            {'response_type': response_type}) is expected

    @pytest.mark.parametrize('response_type, response_mode, expected', [
        ('code', 'fragment', True),
        ('id_token', 'query', False),
        ('code token', 'form_post', False),
    ])
    def test_expect_fragment_encoded_response_with_non_default_response_mode(
            self, response_type, response_mode, expected):
        auth_req = {
            'response_type': response_type,
            'response_mode': response_mode
        }
        assert AuthResponseHandler.expect_fragment_encoded_response(
            auth_req) is expected
class TestAuthResponseHandler:
    ISSUER = 'https://issuer.example.com'
    CLIENT_ID = 'client1'
    AUTH_REQUEST = AuthorizationRequest(**{
        'state': 'test_state',
        'nonce': 'test_nonce'
    })
    AUTH_RESPONSE = AuthorizationResponse(**{
        'code': 'test_auth_code',
        'state': AUTH_REQUEST['state']
    })
    TOKEN_RESPONSE = AccessTokenResponse(
        **{
            'access_token': 'test_token',
            'expires_in': 3600,
            'id_token': _create_id_token(ISSUER, CLIENT_ID,
                                         AUTH_REQUEST['nonce']),
            'id_token_jwt': 'test_id_token_jwt',
            'refresh_token': 'test_refresh_token'
        })
    USERINFO_RESPONSE = OpenIDSchema(**{'sub': 'test_sub'})
    ERROR_RESPONSE = {
        'error': 'test_error',
        'error_description': 'something went wrong'
    }

    @pytest.fixture
    def client_mock(self):
        return create_autospec(PyoidcFacade, True, True)

    def test_should_detect_state_mismatch(self, client_mock):
        auth_request = {
            'state': 'other_state',
            'nonce': self.AUTH_REQUEST['nonce']
        }
        with pytest.raises(AuthResponseUnexpectedStateError):
            AuthResponseHandler(client_mock).process_auth_response(
                self.AUTH_RESPONSE, auth_request)

    def test_should_detect_nonce_mismatch(self, client_mock):
        client = PyoidcFacade(
            ProviderConfiguration(
                provider_metadata=ProviderMetadata(issuer=self.ISSUER),
                client_metadata=ClientMetadata(client_id=self.CLIENT_ID)),
            redirect_uri='https://client.example.com/redirect')
        client.exchange_authorization_code = MagicMock(
            return_value=self.TOKEN_RESPONSE)
        auth_request = {
            'state': self.AUTH_RESPONSE['state'],
            'nonce': 'other_nonce'
        }
        with pytest.raises(InvalidIdTokenError):
            AuthResponseHandler(client).process_auth_response(
                self.AUTH_RESPONSE, auth_request)

    def test_should_handle_auth_error_response(self, client_mock):
        with pytest.raises(AuthResponseErrorResponseError) as exc:
            AuthResponseHandler(client_mock).process_auth_response(
                AuthorizationErrorResponse(**self.ERROR_RESPONSE),
                self.AUTH_REQUEST)
        assert exc.value.error_response == self.ERROR_RESPONSE

    def test_should_handle_token_error_response(self, client_mock):
        client_mock.exchange_authorization_code.return_value = TokenErrorResponse(
            **self.ERROR_RESPONSE)
        with pytest.raises(AuthResponseErrorResponseError) as exc:
            AuthResponseHandler(client_mock).process_auth_response(
                AuthorizationResponse(**self.AUTH_RESPONSE), self.AUTH_REQUEST)
        assert exc.value.error_response == self.ERROR_RESPONSE

    def test_should_detect_mismatching_subject(self, client_mock):
        client_mock.exchange_authorization_code.return_value = AccessTokenResponse(
            **self.TOKEN_RESPONSE)
        client_mock.userinfo_request.return_value = OpenIDSchema(
            **{'sub': 'other_sub'})
        with pytest.raises(AuthResponseMismatchingSubjectError):
            AuthResponseHandler(client_mock).process_auth_response(
                AuthorizationResponse(**self.AUTH_RESPONSE), self.AUTH_REQUEST)

    def test_should_handle_auth_response_with_authorization_code(
            self, client_mock):
        client_mock.exchange_authorization_code.return_value = self.TOKEN_RESPONSE
        client_mock.userinfo_request.return_value = self.USERINFO_RESPONSE
        result = AuthResponseHandler(client_mock).process_auth_response(
            self.AUTH_RESPONSE, self.AUTH_REQUEST)
        assert result.access_token == 'test_token'
        assert result.expires_in == self.TOKEN_RESPONSE['expires_in']
        assert result.id_token_claims == self.TOKEN_RESPONSE[
            'id_token'].to_dict()
        assert result.id_token_jwt == self.TOKEN_RESPONSE['id_token_jwt']
        assert result.userinfo_claims == self.USERINFO_RESPONSE.to_dict()
        assert result.refresh_token == self.TOKEN_RESPONSE['refresh_token']

    def test_should_handle_auth_response_without_authorization_code(
            self, client_mock):
        auth_response = AuthorizationResponse(**self.TOKEN_RESPONSE)
        auth_response['state'] = 'test_state'
        client_mock.userinfo_request.return_value = self.USERINFO_RESPONSE
        result = AuthResponseHandler(client_mock).process_auth_response(
            auth_response, self.AUTH_REQUEST)
        assert not client_mock.exchange_authorization_code.called
        assert result.access_token == 'test_token'
        assert result.expires_in == self.TOKEN_RESPONSE['expires_in']
        assert result.id_token_jwt == self.TOKEN_RESPONSE['id_token_jwt']
        assert result.id_token_claims == self.TOKEN_RESPONSE[
            'id_token'].to_dict()
        assert result.userinfo_claims == self.USERINFO_RESPONSE.to_dict()
        assert result.refresh_token == None

    def test_should_handle_token_response_without_id_token(self, client_mock):
        token_response = {'access_token': 'test_token'}
        client_mock.exchange_authorization_code.return_value = AccessTokenResponse(
            **token_response)
        result = AuthResponseHandler(client_mock).process_auth_response(
            AuthorizationResponse(**self.AUTH_RESPONSE), self.AUTH_REQUEST)
        assert result.access_token == 'test_token'
        assert result.id_token_claims is None

    def test_should_handle_no_token_response(self, client_mock):
        client_mock.exchange_authorization_code.return_value = None
        client_mock.userinfo_request.return_value = None
        hybrid_auth_response = self.AUTH_RESPONSE.copy()
        hybrid_auth_response.update(self.TOKEN_RESPONSE)
        result = AuthResponseHandler(client_mock).process_auth_response(
            AuthorizationResponse(**hybrid_auth_response), self.AUTH_REQUEST)
        assert result.access_token == 'test_token'
        assert result.id_token_claims == self.TOKEN_RESPONSE[
            'id_token'].to_dict()
        assert result.id_token_jwt == self.TOKEN_RESPONSE['id_token_jwt']

    @pytest.mark.parametrize(
        'response_type, expected',
        [
            ('code', False),  # Authorization Code Flow
            ('id_token', True),  # Implicit Flow
            ('id_token token', True),  # Implicit Flow
            ('code id_token', True),  # Hybrid Flow
            ('code token', True),  # Hybrid Flow
            ('code id_token token', True)  # Hybrid Flow
        ])
    def test_expect_fragment_encoded_response_by_response_type(
            self, response_type, expected):
        assert AuthResponseHandler.expect_fragment_encoded_response(
            {'response_type': response_type}) is expected

    @pytest.mark.parametrize('response_type, response_mode, expected', [
        ('code', 'fragment', True),
        ('id_token', 'query', False),
        ('code token', 'form_post', False),
    ])
    def test_expect_fragment_encoded_response_with_non_default_response_mode(
            self, response_type, response_mode, expected):
        auth_req = {
            'response_type': response_type,
            'response_mode': response_mode
        }
        assert AuthResponseHandler.expect_fragment_encoded_response(
            auth_req) is expected