Beispiel #1
0
def test_access_token_srv_conf():
    client_config = {
        'client_id': 'client_id',
        'client_secret': 'a longesh password',
        'redirect_uris': ['https://example.com/cli/authz_cb']
    }
    entity = Entity(config=client_config)
    token_service = entity.client_get("service", 'accesstoken')

    _state_interface = token_service.client_get("service_context").state
    _state_val = _state_interface.create_state(
        token_service.client_get("service_context").issuer)
    auth_request = AuthorizationRequest(
        redirect_uri='https://example.com/cli/authz_cb', state=_state_val)

    _state_interface.store_item(auth_request, "auth_request", _state_val)
    auth_response = AuthorizationResponse(code='access_code')
    _state_interface.store_item(auth_response, "auth_response", _state_val)

    req_args = {
        'redirect_uri': 'https://example.com/cli/authz_cb',
        'code': 'access_code'
    }
    token_service.endpoint = 'https://example.com/authorize'
    _info = token_service.get_request_parameters(request_args=req_args,
                                                 state=_state_val)

    assert _info
    msg = AccessTokenRequest().from_urlencoded(_info['body'])
    # client_secret_basic by default
    assert 'client_secret' not in msg
Beispiel #2
0
class TestPKCE384:
    @pytest.fixture(autouse=True)
    def create_client(self):
        config = {
            'client_id': 'client_id',
            'client_secret': 'a longesh password',
            'redirect_uris': ['https://example.com/cli/authz_cb'],
            'add_ons': {
                "pkce": {
                    "function": "oidcrp.oauth2.add_on.pkce.add_support",
                    "kwargs": {
                        "code_challenge_length": 128,
                        "code_challenge_method": "S384"
                    }
                }
            }
        }
        self.entity = Entity(keyjar=CLI_KEY,
                             config=config,
                             services=DEFAULT_OAUTH2_SERVICES)
        if 'add_ons' in config:
            do_add_ons(config['add_ons'], self.entity.client_get("services"))

    def test_add_code_challenge_spec_values(self):
        auth_serv = self.entity.client_get("service", "authorization")
        request_args, _ = add_code_challenge({'state': 'state'}, auth_serv)
        assert set(request_args.keys()) == {
            'code_challenge', 'code_challenge_method', 'state'
        }
        assert request_args['code_challenge_method'] == 'S384'

        request_args = add_code_verifier({}, auth_serv, state='state')
        assert len(request_args['code_verifier']) == 128
Beispiel #3
0
    def create_request(self):
        self._iss = ISS
        client_config = {
            "redirect_uris": ["https://example.com/cli/authz_cb"],
            "issuer": self._iss,
            "requests_dir": "requests",
            "base_url": "https://example.com/cli/",
            "client_preferences": {
                "application_type": "web",
                "response_types": ["code"],
                "contacts": ["*****@*****.**"],
                "jwks_uri": "https://example.com/rp/static/jwks.json",
                "redirect_uris": ["{}/authz_cb".format(RP_BASEURL)],
                "token_endpoint_auth_method": "client_secret_basic",
                "grant_types": ["authorization_code"]
            }
        }
        services = {
            'registration': {
                'class': 'oidcrp.oidc.registration.Registration'
            },
            'read_registration': {
                'class': 'oidcrp.oidc.read_registration.RegistrationRead'
            }
        }

        self.entity = Entity(config=client_config, services=services)

        self.reg_service = self.entity.client_get("service", 'registration')
        self.read_service = self.entity.client_get("service",
                                                   'registration_read')
Beispiel #4
0
 def create_service(self):
     client_config = {
         'client_id': 'client_id',
         'client_secret': 'a longesh password',
         'redirect_uris': ['https://example.com/cli/authz_cb']
     }
     entity = Entity(config=client_config)
     self.token_service = entity.client_get("service", "accesstoken")
     auth_request = AuthorizationRequest(
         redirect_uri='https://example.com/cli/authz_cb', state='state')
     auth_response = AuthorizationResponse(code='access_code')
     _state = self.token_service.client_get("service_context").state
     _state.store_item(auth_request, 'auth_request', 'state')
     _state.store_item(auth_response, 'auth_response', 'state')
Beispiel #5
0
 def create_service(self):
     client_config = {
         'client_id': 'client_id',
         'client_secret': 'a longesh password',
         'redirect_uris': ['https://example.com/cli/authz_cb']
     }
     entity = Entity(config=client_config)
     self.refresh_service = entity.client_get("service", 'refresh_token')
     auth_response = AuthorizationResponse(code='access_code')
     token_response = AccessTokenResponse(access_token='bearer_token',
                                          refresh_token='refresh')
     _state = self.refresh_service.client_get("service_context").state
     _state.store_item(auth_response, 'auth_response', 'abcdef')
     _state.store_item(token_response, 'token_response', 'abcdef')
     self.refresh_service.endpoint = 'https://example.com/token'
Beispiel #6
0
    def rp_service_setup(self):
        entity_id = 'https://foodle.uninett.no'
        entity = Entity(config={
            'issuer': 'https://op.ntnu.no',
            'keys': {
                'key_defs': KEY_DEFS
            }
        })
        service_context = entity.get_service_context()

        http_cli = Publisher(os.path.join(BASE_PATH, 'base_data'))

        self.federation_entity = FederationEntity(
            entity_id,
            trusted_roots=ANCHOR,
            authority_hints=['https://ntnu.no'],
            httpd=http_cli,
            entity_type='openid_relying_party',
            opponent_entity_type='openid_provider',
            config={'keys': {
                'key_defs': KEY_DEFS
            }})

        # The test data collector
        self.federation_entity.collector = DummyCollector(
            trusted_roots=ANCHOR,
            httpd=http_cli,
            root_dir=os.path.join(BASE_PATH, 'base_data'))

        service_context.federation_entity = self.federation_entity
        service_context.redirect_uris = ['https://foodle.uninett.no/cb']
        # Note that the keys used for OIDC base protocol communication are separate from those used
        # in the federation context
        # service_context.keyjar = init_key_jar(key_defs=KEY_DEFS, issuer_id=entity_id)
        service_context.client_preferences = {
            "grant_types": ['authorization_code', 'implicit', 'refresh_token'],
            "id_token_signed_response_alg": "ES256",
            "token_endpoint_auth_method": "client_secret_basic",
            "federation_type": ['automatic']
        }

        self.service = {
            'discovery':
            FedProviderInfoDiscovery(client_get=entity.client_get),
            'registration': Registration(client_get=entity.client_get)
        }
Beispiel #7
0
    def create_service(self):
        client_config = {
            'client_id': 'client_id',
            'client_secret': 'another password'
        }
        services = {
            'token': {
                'class': 'oidcrp.oauth2.client_credentials.cc_access_token.CCAccessToken'
            },
            'refresh_token': {
                'class': 'oidcrp.oauth2.client_credentials.cc_refresh_access_token'
                         '.CCRefreshAccessToken'
            }
        }

        self.entity = Entity(config=client_config, services=services)

        self.entity.client_get("service",'accesstoken').endpoint = 'https://example.com/token'
        self.entity.client_get("service",'refresh_token').endpoint = 'https://example.com/token'
Beispiel #8
0
 def create_client(self):
     config = {
         'client_id': 'client_id',
         'client_secret': 'a longesh password',
         'redirect_uris': ['https://example.com/cli/authz_cb'],
         'add_ons': {
             "pkce": {
                 "function": "oidcrp.oauth2.add_on.pkce.add_support",
                 "kwargs": {
                     "code_challenge_length": 128,
                     "code_challenge_method": "S384"
                 }
             }
         }
     }
     self.entity = Entity(keyjar=CLI_KEY,
                          config=config,
                          services=DEFAULT_OAUTH2_SERVICES)
     if 'add_ons' in config:
         do_add_ons(config['add_ons'], self.entity.client_get("services"))
Beispiel #9
0
    def create_service(self):
        self._iss = 'https://example.com/as'

        client_config = {
            'client_id': 'client_id',
            'client_secret': 'a longesh password',
            "client_preferences": {
                "application_type": "web",
                "application_name": "rphandler",
                "contacts": ["*****@*****.**"],
                "response_types": ["code"],
                "scope": ["openid", "profile", "email", "address", "phone"],
                "token_endpoint_auth_method": "client_secret_basic",
            },
            'redirect_uris': ['https://example.com/cli/authz_cb'],
            'issuer': self._iss
        }
        entity = Entity(config=client_config)
        self.auth_service = entity.client_get("service", 'provider_info')
        self.auth_service.endpoint = '{}/.well-known/openid-configuration'.format(
            self._iss)
Beispiel #10
0
    def __init__(self,
                 client_authn_factory=None,
                 keyjar=None,
                 verify_ssl=True,
                 config=None,
                 httplib=None,
                 services=None,
                 jwks_uri='',
                 httpc_params=None):
        """

        :param client_authn_factory: Factory that this client can use to
            initiate a client authentication class.
        :param keyjar: A py:class:`oidcmsg.key_jar.KeyJar` instance
        :param config: Configuration information passed on to the
            :py:class:`oidcrp.service_context.ServiceContext`
            initialization
        :param httplib: A HTTP client to use
        :param services: A list of service definitions
        :param jwks_uri: A jwks_uri
        :param httpc_params: HTTP request arguments
        :return: Client instance
        """

        Entity.__init__(self,
                        client_authn_factory=client_authn_factory,
                        keyjar=keyjar,
                        config=config,
                        services=services,
                        jwks_uri=jwks_uri,
                        httpc_params=httpc_params)

        self.http = httplib or HTTPLib(httpc_params)

        if 'add_ons' in config:
            do_add_ons(config['add_ons'], self._service)

        # just ignore verify_ssl until it goes away
        self.verify_ssl = self.httpc_params.get("verify", True)
Beispiel #11
0
def test_conversation():
    config = {
        "client_preferences": {
            "application_type": "web",
            "application_name": "rphandler",
            "contacts": ["*****@*****.**"],
            "response_types": ["code"],
            "scope": ["openid", "profile", "email", "address", "phone"],
            "token_endpoint_auth_method": "client_secret_basic",
        },
        "redirect_uris": ["{}/authz_cb".format(RP_BASEURL)],
        "jwks_uri": "{}/static/jwks.json".format(RP_BASEURL)
    }

    service_spec = DEFAULT_OIDC_SERVICES.copy()
    service_spec['WebFinger'] = {'class': WebFinger}

    entity = Entity(config=config, services=service_spec, keyjar=RP_KEYJAR)

    assert set(entity.client_get("services").keys()) == {
        'accesstoken', 'authorization', 'webfinger', 'registration',
        'refresh_token', 'userinfo', 'provider_info'
    }
    service_context = entity.client_get("service_context")

    # ======================== WebFinger ========================

    webfinger_service = entity.client_get("service", 'webfinger')
    info = webfinger_service.get_request_parameters(
        request_args={'resource': '*****@*****.**'})

    assert info[
               'url'] == 'https://example.org/.well-known/webfinger?rel=http' \
                         '%3A%2F' \
                         '%2Fopenid.net%2Fspecs%2Fconnect%2F1.0%2Fissuer' \
                         '&resource' \
                         '=acct%3Afoobar%40example.org'

    webfinger_response = json.dumps({
        "subject":
        "acct:[email protected]",
        "links": [{
            "rel": "http://openid.net/specs/connect/1.0/issuer",
            "href": "https://example.org/op"
        }],
        "expires":
        "2018-02-04T11:08:41Z"
    })

    response = webfinger_service.parse_response(webfinger_response)

    assert isinstance(response, JRD)
    assert set(response.keys()) == {'subject', 'links', 'expires'}
    assert response['links'] == [
        Link(rel='http://openid.net/specs/connect/1.0/issuer',
             href='https://example.org/op')
    ]

    webfinger_service.update_service_context(resp=response)
    entity.client_get("service_context").issuer = OP_BASEURL

    # =================== Provider info discovery ====================
    provider_info_service = entity.client_get("service", 'provider_info')
    info = provider_info_service.get_request_parameters()

    assert info[
               'url'] == 'https://example.org/op/.well-known/openid' \
                         '-configuration'

    provider_info_response = json.dumps({
        "version":
        "3.0",
        "token_endpoint_auth_methods_supported": [
            "client_secret_post", "client_secret_basic", "client_secret_jwt",
            "private_key_jwt"
        ],
        "claims_parameter_supported":
        True,
        "request_parameter_supported":
        True,
        "request_uri_parameter_supported":
        True,
        "require_request_uri_registration":
        True,
        "grant_types_supported": [
            "authorization_code", "implicit",
            "urn:ietf:params:oauth:grant-type:jwt-bearer", "refresh_token"
        ],
        "response_types_supported": [
            "code", "id_token", "id_token token", "code id_token",
            "code token", "code id_token token"
        ],
        "response_modes_supported": ["query", "fragment", "form_post"],
        "subject_types_supported": ["public", "pairwise"],
        "claim_types_supported": ["normal", "aggregated", "distributed"],
        "claims_supported": [
            "birthdate", "address", "nickname", "picture", "website", "email",
            "gender", "sub", "phone_number_verified", "given_name", "profile",
            "phone_number", "updated_at", "middle_name", "name", "locale",
            "email_verified", "preferred_username", "zoneinfo", "family_name"
        ],
        "scopes_supported": [
            "openid", "profile", "email", "address", "phone", "offline_access",
            "openid"
        ],
        "userinfo_signing_alg_values_supported": [
            "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "HS256",
            "HS384", "HS512", "PS256", "PS384", "PS512", "none"
        ],
        "id_token_signing_alg_values_supported": [
            "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "HS256",
            "HS384", "HS512", "PS256", "PS384", "PS512", "none"
        ],
        "request_object_signing_alg_values_supported": [
            "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "HS256",
            "HS384", "HS512", "PS256", "PS384", "PS512", "none"
        ],
        "token_endpoint_auth_signing_alg_values_supported": [
            "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "HS256",
            "HS384", "HS512", "PS256", "PS384", "PS512"
        ],
        "userinfo_encryption_alg_values_supported": [
            "RSA1_5", "RSA-OAEP", "RSA-OAEP-256", "A128KW", "A192KW", "A256KW",
            "ECDH-ES", "ECDH-ES+A128KW", "ECDH-ES+A192KW", "ECDH-ES+A256KW"
        ],
        "id_token_encryption_alg_values_supported": [
            "RSA1_5", "RSA-OAEP", "RSA-OAEP-256", "A128KW", "A192KW", "A256KW",
            "ECDH-ES", "ECDH-ES+A128KW", "ECDH-ES+A192KW", "ECDH-ES+A256KW"
        ],
        "request_object_encryption_alg_values_supported": [
            "RSA1_5", "RSA-OAEP", "RSA-OAEP-256", "A128KW", "A192KW", "A256KW",
            "ECDH-ES", "ECDH-ES+A128KW", "ECDH-ES+A192KW", "ECDH-ES+A256KW"
        ],
        "userinfo_encryption_enc_values_supported": [
            "A128CBC-HS256", "A192CBC-HS384", "A256CBC-HS512", "A128GCM",
            "A192GCM", "A256GCM"
        ],
        "id_token_encryption_enc_values_supported": [
            "A128CBC-HS256", "A192CBC-HS384", "A256CBC-HS512", "A128GCM",
            "A192GCM", "A256GCM"
        ],
        "request_object_encryption_enc_values_supported": [
            "A128CBC-HS256", "A192CBC-HS384", "A256CBC-HS512", "A128GCM",
            "A192GCM", "A256GCM"
        ],
        "acr_values_supported": ["PASSWORD"],
        "issuer":
        OP_BASEURL,
        "jwks_uri":
        "{}/static/jwks_tE2iLbOAqXhe8bqh.json".format(OP_BASEURL),
        "authorization_endpoint":
        "{}/authorization".format(OP_BASEURL),
        "token_endpoint":
        "{}/token".format(OP_BASEURL),
        "userinfo_endpoint":
        "{}/userinfo".format(OP_BASEURL),
        "registration_endpoint":
        "{}/registration".format(OP_BASEURL),
        "end_session_endpoint":
        "{}/end_session".format(OP_BASEURL)
    })

    resp = provider_info_service.parse_response(provider_info_response)

    assert isinstance(resp, ProviderConfigurationResponse)
    provider_info_service.update_service_context(resp)

    _pi = entity.client_get("service_context").provider_info
    assert _pi['issuer'] == OP_BASEURL
    assert _pi[
        'authorization_endpoint'] == 'https://example.org/op/authorization'
    assert _pi[
        'registration_endpoint'] == 'https://example.org/op/registration'

    # =================== Client registration ====================
    registration_service = entity.client_get("service", 'registration')
    info = registration_service.get_request_parameters()

    assert info['url'] == 'https://example.org/op/registration'
    _body = json.loads(info['body'])
    assert _body == {
        "application_type": "web",
        "response_types": ["code"],
        "contacts": ["*****@*****.**"],
        "jwks_uri": "https://example.com/rp/static/jwks.json",
        "redirect_uris": ["{}/authz_cb".format(RP_BASEURL)],
        'token_endpoint_auth_method': 'client_secret_basic',
        "grant_types": ["authorization_code"]
    }
    assert info['headers'] == {'Content-Type': 'application/json'}

    now = int(time.time())

    op_client_registration_response = json.dumps({
        "client_id":
        "zls2qhN1jO6A",
        "client_secret":
        "c8434f28cf9375d9a7",
        "registration_access_token":
        "NdGrGR7LCuzNtixvBFnDphGXv7wRcONn",
        "registration_client_uri":
        "{}/registration?client_id=zls2qhN1jO6A".format(RP_BASEURL),
        "client_secret_expires_at":
        now + 3600,
        "client_id_issued_at":
        now,
        "application_type":
        "web",
        "response_types": ["code"],
        "contacts": ["*****@*****.**"],
        "redirect_uris": ["{}/authz_cb".format(RP_BASEURL)],
        "token_endpoint_auth_method":
        "client_secret_basic",
        "grant_types": ["authorization_code"]
    })

    response = registration_service.parse_response(
        op_client_registration_response)

    registration_service.update_service_context(response)

    assert service_context.client_id == 'zls2qhN1jO6A'
    assert service_context.client_secret == 'c8434f28cf9375d9a7'
    assert set(service_context.registration_response.keys()) == {
        'client_secret_expires_at', 'contacts', 'client_id',
        'token_endpoint_auth_method', 'redirect_uris', 'response_types',
        'client_id_issued_at', 'client_secret', 'application_type',
        'registration_client_uri', 'registration_access_token', 'grant_types'
    }

    # =================== Authorization ====================

    STATE = 'Oh3w3gKlvoM2ehFqlxI3HIK5'
    NONCE = 'UvudLKz287YByZdsY3AJoPAlEXQkJ0dK'

    auth_service = entity.client_get("service", 'authorization')
    _state_interface = service_context.state

    info = auth_service.get_request_parameters(request_args={
        'state': STATE,
        'nonce': NONCE
    })

    p = urlparse(info['url'])
    _query = parse_qs(p.query)
    assert set(_query.keys()) == {
        'state', 'nonce', 'response_type', 'scope', 'client_id', 'redirect_uri'
    }
    assert _query['scope'] == ['openid profile email address phone']
    assert _query['nonce'] == [NONCE]
    assert _query['state'] == [STATE]

    op_authz_resp = {
        'state': STATE,
        'scope': 'openid',
        'code': 'Z0FBQUFBQmFkdFFjUVpFWE81SHU5N1N4N01',
        'iss': OP_BASEURL,
        'client_id': 'zls2qhN1jO6A'
    }

    _authz_rep = AuthorizationResponse(**op_authz_resp)

    _resp = auth_service.parse_response(_authz_rep.to_urlencoded())
    auth_service.update_service_context(_resp, key=STATE)
    _item = _state_interface.get_item(AuthorizationResponse, 'auth_response',
                                      STATE)
    assert _item['code'] == 'Z0FBQUFBQmFkdFFjUVpFWE81SHU5N1N4N01'

    # =================== Access token ====================

    token_service = entity.client_get("service", 'accesstoken')
    request_args = {
        'state': STATE,
        'redirect_uri': service_context.redirect_uris[0]
    }

    info = token_service.get_request_parameters(request_args=request_args)

    assert info['url'] == 'https://example.org/op/token'
    _qp = parse_qs(info['body'])
    assert _qp == {
        'grant_type': ['authorization_code'],
        'redirect_uri': ['https://example.com/rp/authz_cb'],
        'client_id': ['zls2qhN1jO6A'],
        'state': ['Oh3w3gKlvoM2ehFqlxI3HIK5'],
        'code': ['Z0FBQUFBQmFkdFFjUVpFWE81SHU5N1N4N01']
    }
    assert info['headers'] == {
        'Authorization': 'Basic '
        'emxzMnFoTjFqTzZBOmM4NDM0ZjI4Y2Y5Mzc1ZDlhNw==',
        'Content-Type': 'application/x-www-form-urlencoded'
    }

    # create the IdToken
    _jwt = JWT(OP_KEYJAR,
               OP_BASEURL,
               lifetime=3600,
               sign=True,
               sign_alg='RS256')
    payload = {
        'sub': '1b2fc9341a16ae4e30082965d537',
        'acr': 'PASSWORD',
        'auth_time': 1517736988,
        'nonce': NONCE
    }
    _jws = _jwt.pack(payload=payload, recv='zls2qhN1jO6A')

    _resp = {
        "state": "Oh3w3gKlvoM2ehFqlxI3HIK5",
        "scope": "openid",
        "access_token": "Z0FBQUFBQmFkdFF",
        "token_type": "Bearer",
        'expires_in': 600,
        "id_token": _jws
    }

    service_context.issuer = OP_BASEURL
    _resp = token_service.parse_response(json.dumps(_resp), state=STATE)

    assert isinstance(_resp, AccessTokenResponse)
    assert set(_resp['__verified_id_token'].keys()) == {
        'iss', 'nonce', 'acr', 'auth_time', 'aud', 'iat', 'exp', 'sub'
    }

    token_service.update_service_context(_resp, key=STATE)

    _item = _state_interface.get_item(AccessTokenResponse, 'token_response',
                                      STATE)

    assert set(_item.keys()) == {
        'state', 'scope', 'access_token', 'token_type', 'id_token',
        '__verified_id_token', 'expires_in', '__expires_at'
    }

    assert _item['token_type'] == 'Bearer'
    assert _item['access_token'] == 'Z0FBQUFBQmFkdFF'

    # =================== User info ====================

    userinfo_service = entity.client_get("service", 'userinfo')
    info = userinfo_service.get_request_parameters(state=STATE)

    assert info['url'] == 'https://example.org/op/userinfo'
    assert info['headers'] == {'Authorization': 'Bearer Z0FBQUFBQmFkdFF'}

    op_resp = {"sub": "1b2fc9341a16ae4e30082965d537"}

    _resp = userinfo_service.parse_response(json.dumps(op_resp), state=STATE)
    userinfo_service.update_service_context(_resp, key=STATE)

    assert isinstance(_resp, OpenIDSchema)
    assert _resp.to_dict() == {'sub': '1b2fc9341a16ae4e30082965d537'}

    _item = _state_interface.get_item(OpenIDSchema, 'user_info', STATE)
    assert _item.to_dict() == {'sub': '1b2fc9341a16ae4e30082965d537'}
Beispiel #12
0
class TestRegistrationRead(object):
    @pytest.fixture(autouse=True)
    def create_request(self):
        self._iss = ISS
        client_config = {
            "redirect_uris": ["https://example.com/cli/authz_cb"],
            "issuer": self._iss,
            "requests_dir": "requests",
            "base_url": "https://example.com/cli/",
            "client_preferences": {
                "application_type": "web",
                "response_types": ["code"],
                "contacts": ["*****@*****.**"],
                "jwks_uri": "https://example.com/rp/static/jwks.json",
                "redirect_uris": ["{}/authz_cb".format(RP_BASEURL)],
                "token_endpoint_auth_method": "client_secret_basic",
                "grant_types": ["authorization_code"]
            }
        }
        services = {
            'registration': {
                'class': 'oidcrp.oidc.registration.Registration'
            },
            'read_registration': {
                'class': 'oidcrp.oidc.read_registration.RegistrationRead'
            }
        }

        self.entity = Entity(config=client_config, services=services)

        self.reg_service = self.entity.client_get("service", 'registration')
        self.read_service = self.entity.client_get("service",
                                                   'registration_read')

    def test_construct(self):
        self.reg_service.endpoint = "{}/registration".format(ISS)

        _param = self.reg_service.get_request_parameters()

        now = int(time.time())

        _client_registration_response = json.dumps({
            "client_id":
            "zls2qhN1jO6A",
            "client_secret":
            "c8434f28cf9375d9a7",
            "registration_access_token":
            "NdGrGR7LCuzNtixvBFnDphGXv7wRcONn",
            "registration_client_uri":
            "{}/registration_api?client_id=zls2qhN1jO6A".format(ISS),
            "client_secret_expires_at":
            now + 3600,
            "client_id_issued_at":
            now,
            "application_type":
            "web",
            "response_types": ["code"],
            "contacts": ["*****@*****.**"],
            "redirect_uris": ["{}/authz_cb".format(RP_BASEURL)],
            "token_endpoint_auth_method":
            "client_secret_basic",
            "grant_types": ["authorization_code"]
        })

        with responses.RequestsMock() as rsps:
            rsps.add(_param["method"],
                     _param["url"],
                     body=_client_registration_response,
                     status=200)
            _resp = requests.request(_param["method"],
                                     _param["url"],
                                     data=as_bytes(_param["body"]),
                                     headers=_param["headers"],
                                     verify=False)

        resp = self.reg_service.parse_response(_resp.text)
        self.reg_service.update_service_context(resp)

        assert resp

        _read_param = self.read_service.get_request_parameters()
        with responses.RequestsMock() as rsps:
            rsps.add(_param["method"],
                     _param["url"],
                     body=_client_registration_response,
                     adding_headers={"Content-Type": "application/json"},
                     status=200)
            _resp = requests.request(_param["method"],
                                     _param["url"],
                                     headers=_param["headers"],
                                     verify=False)

        read_resp = self.reg_service.parse_response(_resp.text)
        assert isinstance(read_resp, RegistrationResponse)
Beispiel #13
0
from urllib.parse import parse_qs, unquote_plus, urlsplit

from oidcrp.entity import Entity
import pytest
from oidcmsg.exception import MissingRequiredAttribute
from oidcmsg.oidc import JRD, Link

from oidcrp.oidc import OIC_ISSUER
from oidcrp.oidc.webfinger import WebFinger
from oidcrp.service_context import ServiceContext

__author__ = 'Roland Hedberg'

SERVICE_CONTEXT = ServiceContext()

ENTITY = Entity(config={})


def test_query():
    rel = 'http%3A%2F%2Fopenid.net%2Fspecs%2Fconnect%2F1.0%2Fissuer'
    pattern = 'https://{}/.well-known/webfinger?rel={}&resource={}'
    example_oidc = {
        'example.com': ('example.com', rel, 'acct%3Aexample.com'),
        '*****@*****.**': ('example.com', rel, 'acct%3Ajoe%40example.com'),
        'example.com/joe':
        ('example.com', rel, 'https%3A%2F%2Fexample.com%2Fjoe'),
        'example.com:8080':
        ('example.com:8080', rel, 'https%3A%2F%2Fexample.com%3A8080'),
        '*****@*****.**':
        ('example.com', rel, 'acct%3AJane.Doe%40example.com'),
        '[email protected]:8080': ('[email protected]:8080', rel,
Beispiel #14
0
class TestRP():
    @pytest.fixture(autouse=True)
    def create_service(self):
        client_config = {
            'client_id': 'client_id',
            'client_secret': 'another password'
        }
        services = {
            'token': {
                'class': 'oidcrp.oauth2.client_credentials.cc_access_token.CCAccessToken'
            },
            'refresh_token': {
                'class': 'oidcrp.oauth2.client_credentials.cc_refresh_access_token'
                         '.CCRefreshAccessToken'
            }
        }

        self.entity = Entity(config=client_config, services=services)

        self.entity.client_get("service",'accesstoken').endpoint = 'https://example.com/token'
        self.entity.client_get("service",'refresh_token').endpoint = 'https://example.com/token'

    def test_token_get_request(self):
        request_args = {'grant_type': 'client_credentials'}
        _srv = self.entity.client_get("service",'accesstoken')
        _info = _srv.get_request_parameters(request_args=request_args)
        assert _info['method'] == 'POST'
        assert _info['url'] == 'https://example.com/token'
        assert _info['body'] == 'grant_type=client_credentials'
        assert _info['headers'] == {
            'Authorization': 'Basic Y2xpZW50X2lkOmFub3RoZXIrcGFzc3dvcmQ=',
            'Content-Type': 'application/x-www-form-urlencoded'
        }

    def test_token_parse_response(self):
        request_args = {'grant_type': 'client_credentials'}
        _srv = self.entity.client_get("service",'accesstoken')
        _request_info = _srv.get_request_parameters(request_args=request_args)

        response = AccessTokenResponse(**{
            "access_token": "2YotnFZFEjr1zCsicMWpAA",
            "token_type": "example",
            "expires_in": 3600,
            "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
            "example_parameter": "example_value"
        })

        _response = _srv.parse_response(response.to_json(), sformat="json")
        # since no state attribute is involved, a key is minted
        _key = rndstr(16)
        _srv.update_service_context(_response, key=_key)
        info = _srv.client_get("service_context").state.get_item(AccessTokenResponse, 'token_response', _key)
        assert '__expires_at' in info

    def test_refresh_token_get_request(self):
        _srv = self.entity.client_get("service",'accesstoken')
        _srv.update_service_context({
            "access_token": "2YotnFZFEjr1zCsicMWpAA",
            "token_type": "example",
            "expires_in": 3600,
            "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
            "example_parameter": "example_value"
        })
        _srv = self.entity.client_get("service",'refresh_token')
        _id = rndstr(16)
        _info = _srv.get_request_parameters(state_id=_id)
        assert _info['method'] == 'POST'
        assert _info['url'] == 'https://example.com/token'
        assert _info[
                   'body'] == 'grant_type=refresh_token'
        assert _info['headers'] == {
            'Authorization': 'Bearer tGzv3JOkF0XG5Qx2TlKWIA',
            'Content-Type': 'application/x-www-form-urlencoded'
        }

    def test_refresh_token_parse_response(self):
        request_args = {'grant_type': 'client_credentials'}
        _srv = self.entity.client_get("service",'accesstoken')
        _request_info = _srv.get_request_parameters(request_args=request_args)

        response = AccessTokenResponse(**{
            "access_token": "2YotnFZFEjr1zCsicMWpAA",
            "token_type": "example",
            "expires_in": 3600,
            "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
            "example_parameter": "example_value"
        })

        _response = _srv.parse_response(response.to_json(), sformat="json")
        # since no state attribute is involved, a key is minted
        _key = rndstr(16)
        _srv.update_service_context(_response, key=_key)
        info = _srv.client_get("service_context").state.get_item(AccessTokenResponse, 'token_response', _key)
        assert '__expires_at' in info

        # Move from token to refresh token service

        _srv = self.entity.client_get("service",'refresh_token')
        _request_info = _srv.get_request_parameters(request_args=request_args, state=_key)

        refresh_response = AccessTokenResponse(**{
            "access_token": 'wy4R01DmMoB5xkI65nNkVv1l',
            "token_type": "example",
            "expires_in": 3600,
            "refresh_token": 'lhNX9LSG8w1QuD6tSgc6CPfJ',
        })

        _response = _srv.parse_response(refresh_response.to_json(), sformat="json")
        _srv.update_service_context(_response, key=_key)
        info = _srv.client_get("service_context").state.get_item(AccessTokenResponse, 'token_response', _key)
        assert '__expires_at' in info

    def test_2nd_refresh_token_parse_response(self):
        request_args = {'grant_type': 'client_credentials'}
        _srv = self.entity.client_get("service",'accesstoken')
        _request_info = _srv.get_request_parameters(request_args=request_args)

        response = AccessTokenResponse(**{
            "access_token": "2YotnFZFEjr1zCsicMWpAA",
            "token_type": "example",
            "expires_in": 3600,
            "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
            "example_parameter": "example_value"
        })

        _response = _srv.parse_response(response.to_json(), sformat="json")
        # since no state attribute is involved, a key is minted
        _key = rndstr(16)
        _srv.update_service_context(_response, key=_key)
        info = _srv.client_get("service_context").state.get_item(AccessTokenResponse, 'token_response', _key)
        assert '__expires_at' in info

        # Move from token to refresh token service

        _srv = self.entity.client_get("service",'refresh_token')
        _request_info = _srv.get_request_parameters(request_args=request_args, state=_key)

        refresh_response = AccessTokenResponse(**{
            "access_token": 'wy4R01DmMoB5xkI65nNkVv1l',
            "token_type": "example",
            "expires_in": 3600,
            "refresh_token": 'lhNX9LSG8w1QuD6tSgc6CPfJ',
        })

        _response = _srv.parse_response(refresh_response.to_json(), sformat="json")
        _srv.update_service_context(_response, key=_key)
        info = _srv.client_get("service_context").state.get_item(AccessTokenResponse, 'token_response', _key)
        assert '__expires_at' in info

        _request_info = _srv.get_request_parameters(request_args=request_args, state=_key)
        assert _request_info['headers'] == {
            'Authorization': 'Bearer {}'.format(refresh_response["refresh_token"]),
            'Content-Type': 'application/x-www-form-urlencoded'
        }
Beispiel #15
0
    def create_setup(self):
        # First the RP
        entity = Entity(
            config={
                'behaviour': {
                    'federation_types_supported': ['automatic']
                },
                'issuer': "https://op.ntnu.no",
                'keys': {
                    'key_defs': KEYSPEC
                }
            })
        service_context = entity.get_service_context()

        self.rp_federation_entity = FederationEntity(
            entity_id=RP_ENTITY_ID,
            trusted_roots=ANCHOR,
            authority_hints=['https://ntnu.no'],
            entity_type='openid_relying_party',
            opponent_entity_type='openid_provider')

        self.rp_federation_entity.keyjar.import_jwks(read_info(
            os.path.join(ROOT_DIR, 'foodle.uninett.no'), 'foodle.uninett.no',
            'jwks'),
                                                     issuer_id=RP_ENTITY_ID)

        self.rp_federation_entity.collector = DummyCollector(
            trusted_roots=ANCHOR, root_dir=ROOT_DIR)

        # add the federation part to the service context
        service_context.federation_entity = self.rp_federation_entity

        # The RP has/supports 3 services

        self.discovery_service = FedProviderInfoDiscovery(entity.client_get)
        self.registration_service = RPRegistration(entity.client_get)
        # self.authorization_service = FedAuthorization(entity.client_get)

        # and now for the OP
        op_entity_id = "https://op.ntnu.no"
        conf = {
            "issuer": op_entity_id,
            "password": "******",
            "token_handler_args": {
                "jwks_def": {
                    "private_path":
                    "private/token_jwks.json",
                    "read_only":
                    False,
                    "key_defs": [
                        {
                            "type": "oct",
                            "bytes": 24,
                            "use": ["enc"],
                            "kid": "code"
                        },
                        {
                            "type": "oct",
                            "bytes": 24,
                            "use": ["enc"],
                            "kid": "refresh"
                        },
                    ],
                },
                "code": {
                    "lifetime": 600
                },
                "token": {
                    "class": "oidcop.token.jwt_token.JWTToken",
                    "kwargs": {
                        "lifetime":
                        3600,
                        "add_claims": [
                            "email",
                            "email_verified",
                            "phone_number",
                            "phone_number_verified",
                        ],
                        "add_claim_by_scope":
                        True,
                        "aud": ["https://example.org/appl"]
                    },
                },
                "refresh": {
                    "lifetime": 86400
                },
            },
            "claims_interface": {
                "class": "oidcop.session.claims.ClaimsInterface",
                "kwargs": {}
            },
            "verify_ssl": False,
            "capabilities": CAPABILITIES,
            "keys": {
                "uri_path": "static/jwks.json",
                "key_defs": KEYSPEC
            },
            "id_token": {
                "class": IDToken,
                "kwargs": {
                    "default_claims": {
                        "email": {
                            "essential": True
                        },
                        "email_verified": {
                            "essential": True
                        },
                    }
                },
            },
            "endpoint": {
                "provider_config": {
                    "path": ".well-known/openid-configuration",
                    "class": ProviderConfiguration,
                    "kwargs": {},
                },
                "registration": {
                    "path": "registration",
                    "class": OPRegistration,
                    "kwargs": {},
                },
                "authorization": {
                    "path": "authorization",
                    "class": Authorization,
                    "kwargs": {
                        "response_types_supported":
                        [" ".join(x) for x in RESPONSE_TYPES_SUPPORTED],
                        "response_modes_supported":
                        ["query", "fragment", "form_post"],
                        "claims_parameter_supported":
                        True,
                        "request_parameter_supported":
                        True,
                        "request_uri_parameter_supported":
                        True,
                    },
                },
                "pushed_authorization": {
                    "path": "pushed_authorization",
                    "class": PushedAuthorization,
                    "kwargs": {
                        "client_authn_method": [
                            "client_secret_post",
                            "client_secret_basic",
                            "client_secret_jwt",
                            "private_key_jwt",
                        ]
                    },
                },
            },
            "authentication": {
                "anon": {
                    "acr": "http://www.swamid.se/policy/assurance/al1",
                    "class": "oidcop.user_authn.user.NoAuthn",
                    "kwargs": {
                        "user": "******"
                    },
                }
            },
            "template_dir": "template",
            "cookie_handler": {
                "class": CookieHandler,
                "kwargs": {
                    "keys": {
                        "key_defs": COOKIE_KEYDEFS
                    },
                    "name": {
                        "session": "oidc_op",
                        "register": "oidc_op_reg",
                        "session_management": "oidc_op_sman"
                    }
                },
            },
            'add_on': {
                "automatic_registration": {
                    "function":
                    "fedservice.op.add_on.automatic_registration.add_support",
                    "kwargs": {
                        "new_id": False,  # default False
                        "where": ["pushed_authorization"]
                    }
                }
            }
        }
        server = Server(conf)
        endpoint_context = server.get_endpoint_context()
        _clients = yaml.safe_load(io.StringIO(client_yaml))
        # endpoint_context.cdb = _clients["oidc_clients"]
        endpoint_context.keyjar.import_jwks(
            endpoint_context.keyjar.export_jwks(True, ""), conf["issuer"])

        self.pushed_authorization_endpoint = server.server_get(
            "endpoint", "pushed_authorization")
        self.authorization_endpoint = server.server_get(
            "endpoint", "authorization")
        self.registration_endpoint = server.server_get("endpoint",
                                                       "registration")

        federation_entity = FederationEntity(
            op_entity_id,
            trusted_roots=ANCHOR,
            authority_hints=['https://ntnu.no'],
            entity_type='openid_relying_party',
            httpd=Publisher(ROOT_DIR),
            opponent_entity_type='openid_relying_party')

        # federation_entity.keyjar.import_jwks(
        #     read_info(os.path.join(ROOT_DIR, 'op.ntnu.no'),
        #               'op.ntnu.no', 'jwks'),
        #     issuer_id=op_entity_id)

        federation_entity.collector = DummyCollector(httpd=Publisher(ROOT_DIR),
                                                     trusted_roots=ANCHOR,
                                                     root_dir=ROOT_DIR)

        self.authorization_endpoint.server_get(
            "endpoint_context").federation_entity = federation_entity
Beispiel #16
0
class TestPKCE256:
    @pytest.fixture(autouse=True)
    def create_client(self):
        config = {
            'client_id': 'client_id',
            'client_secret': 'a longesh password',
            'redirect_uris': ['https://example.com/cli/authz_cb'],
            'behaviour': {
                'response_types': ['code']
            },
            'add_ons': {
                "pkce": {
                    "function": "oidcrp.oauth2.add_on.pkce.add_support",
                    "kwargs": {
                        "code_challenge_length": 64,
                        "code_challenge_method": "S256"
                    }
                }
            }
        }
        self.entity = Entity(keyjar=CLI_KEY,
                             config=config,
                             services=DEFAULT_OAUTH2_SERVICES)

        if 'add_ons' in config:
            do_add_ons(config['add_ons'], self.entity.client_get("services"))

    def test_add_code_challenge_default_values(self):
        auth_serv = self.entity.client_get("service", "authorization")
        _state_key = self.entity.client_get(
            "service_context").state.create_state(iss="Issuer")
        request_args, _ = add_code_challenge({'state': _state_key}, auth_serv)

        # default values are length:64 method:S256
        assert set(request_args.keys()) == {
            'code_challenge', 'code_challenge_method', 'state'
        }
        assert request_args['code_challenge_method'] == 'S256'

        request_args = add_code_verifier({}, auth_serv, state=_state_key)
        assert len(request_args['code_verifier']) == 64

    def test_authorization_and_pkce(self):
        auth_serv = self.entity.client_get("service", "authorization")
        _state = self.entity.client_get("service_context").state.create_state(
            iss='Issuer')

        request = auth_serv.construct_request({
            "state": _state,
            "response_type": "code"
        })
        assert set(request.keys()) == {
            'client_id', 'code_challenge', 'code_challenge_method', 'state',
            'redirect_uri', 'response_type'
        }

    def test_access_token_and_pkce(self):
        authz_service = self.entity.client_get("service", "authorization")
        request = authz_service.construct_request({
            "state": 'state',
            "response_type": "code"
        })
        _state = request['state']
        auth_response = AuthorizationResponse(code='access code')
        self.entity.client_get("service_context").state.store_item(
            auth_response, 'auth_response', _state)

        token_service = self.entity.client_get("service", "accesstoken")
        request = token_service.construct_request(state=_state)
        assert set(request.keys()) == {
            'client_id', 'redirect_uri', 'grant_type', 'client_secret',
            'code_verifier', 'code', 'state'
        }
Beispiel #17
0
    def create_endpoint(self):
        # First the RP
        entity = Entity(
            config={
                'behaviour': {
                    'federation_types_supported': ['explicit']
                },
                'issuer': "https://op.ntnu.no",
                'keys': {
                    'key_defs': KEYSPEC
                }
            })
        service_context = entity.client_get("service_context")

        # the federation part of the RP
        self.rp_federation_entity = FederationEntity(
            entity_id=ENTITY_ID,
            trusted_roots=ANCHOR,
            authority_hints=['https://ntnu.no'],
            entity_type='openid_relying_party',
            opponent_entity_type='openid_provider')

        self.rp_federation_entity.collector = DummyCollector(
            trusted_roots=ANCHOR, root_dir=ROOT_DIR)

        self.rp_federation_entity.keyjar.import_jwks(read_info(
            os.path.join(ROOT_DIR, 'foodle.uninett.no'), 'foodle.uninett.no',
            'jwks'),
                                                     issuer_id=ENTITY_ID)

        # add the federation part to the service context
        service_context.federation_entity = self.rp_federation_entity

        # The RP has/supports 3 services
        self.service = {
            'discovery': FedProviderInfoDiscovery(entity.client_get),
            'registration': Registration(entity.client_get),
            'authorization': FedAuthorization(entity.client_get),
        }

        # and now for the OP
        op_entity_id = "https://op.ntnu.no"
        conf = {
            "issuer": op_entity_id,
            "password": "******",
            "token_expires_in": 600,
            "grant_expires_in": 300,
            "refresh_token_expires_in": 86400,
            "httpc_param": {
                'verify': False,
                "timeout": 2
            },
            "claims_interface": {
                "class": "oidcop.session.claims.ClaimsInterface",
                "kwargs": {}
            },
            "cookie_handler": {
                "class": CookieHandler,
                "kwargs": {
                    "keys": {
                        "key_defs": COOKIE_KEYDEFS
                    },
                    "name": {
                        "session": "oidc_op",
                        "register": "oidc_op_reg",
                        "session_management": "oidc_op_sman"
                    }
                },
            },
            "endpoint": {
                'provider_info': {
                    'path': '.well-known/openid-federation',
                    'class': provider_config.ProviderConfiguration,
                    'kwargs': {
                        'client_authn_method': None
                    }
                },
                'registration': {
                    'path': 'fed_registration',
                    'class': registration.Registration,
                    'kwargs': {
                        'client_authn_method': None
                    }
                },
                'authorization': {
                    'path': 'authorization',
                    'class': authorization.Authorization,
                    'kwargs': {
                        "response_modes_supported":
                        ['query', 'fragment', 'form_post'],
                        "claims_parameter_supported":
                        True,
                        "request_parameter_supported":
                        True,
                        "request_uri_parameter_supported":
                        True,
                        "client_authn_method": ['request_param']
                    }
                }
            },
            "keys": {
                "private_path": "own/jwks.json",
                "uri_path": "static/jwks.json",
                "key_defs": KEYSPEC
            },
            "authentication": {
                "anon": {
                    'acr': UNSPECIFIED,
                    "class": NoAuthn,
                    "kwargs": {
                        "user": "******"
                    }
                }
            },
            'template_dir': 'template'
        }
        server = Server(conf)

        self.registration_endpoint = server.server_get("endpoint",
                                                       "registration")
        self.authorization_endpoint = server.server_get(
            "endpoint", "authorization")
        self.provider_endpoint = server.server_get("endpoint",
                                                   "provider_config")

        # === Federation stuff =======
        federation_entity = FederationEntity(
            op_entity_id,
            trusted_roots=ANCHOR,
            authority_hints=['https://ntnu.no'],
            entity_type='openid_relying_party',
            httpd=Publisher(ROOT_DIR),
            opponent_entity_type='openid_relying_party')

        federation_entity.keyjar.import_jwks(read_info(
            os.path.join(ROOT_DIR, 'op.ntnu.no'), 'op.ntnu.no', 'jwks'),
                                             issuer_id=op_entity_id)

        federation_entity.collector = DummyCollector(httpd=Publisher(ROOT_DIR),
                                                     trusted_roots=ANCHOR,
                                                     root_dir=ROOT_DIR)

        self.registration_endpoint.server_get(
            "endpoint_context").federation_entity = federation_entity
def entity():
    return Entity(config=CLIENT_CONF)
Beispiel #19
0
 def create_service(self):
     self.entity = Entity(config=CLIENT_CONF)
     self.auth_service = self.entity.client_get("service", 'authorization')
Beispiel #20
0
class TestAuthorization(object):
    @pytest.fixture(autouse=True)
    def create_service(self):
        self.entity = Entity(config=CLIENT_CONF)
        self.auth_service = self.entity.client_get("service", 'authorization')

    def test_construct(self):
        req_args = {'foo': 'bar'}
        _req = self.auth_service.construct(request_args=req_args,
                                           state='state')
        assert isinstance(_req, AuthorizationRequest)
        assert set(
            _req.keys()) == {'client_id', 'redirect_uri', 'foo', 'state'}
        _context = self.entity.client_get("service_context")
        assert _context.state.get_state('state')
        _item = _context.state.get_item(AuthorizationRequest, 'auth_request',
                                        'state')
        assert _item.to_dict() == {
            'foo': 'bar',
            'redirect_uri': 'https://example.com/cli/authz_cb',
            'state': 'state',
            'client_id': 'client_id'
        }

    def test_get_request_parameters(self):
        req_args = {'response_type': 'code'}
        self.auth_service.endpoint = 'https://example.com/authorize'
        _info = self.auth_service.get_request_parameters(request_args=req_args,
                                                         state='state')
        assert set(_info.keys()) == {'url', 'method', 'request'}
        msg = AuthorizationRequest().from_urlencoded(
            self.auth_service.get_urlinfo(_info['url']))
        assert msg.to_dict() == {
            'client_id': 'client_id',
            'redirect_uri': 'https://example.com/cli/authz_cb',
            'response_type': 'code',
            'state': 'state'
        }

    def test_request_init(self):
        req_args = {'response_type': 'code', 'state': "state"}
        self.auth_service.endpoint = 'https://example.com/authorize'
        _info = self.auth_service.get_request_parameters(request_args=req_args)
        assert set(_info.keys()) == {'url', 'method', 'request'}
        msg = AuthorizationRequest().from_urlencoded(
            self.auth_service.get_urlinfo(_info['url']))
        assert msg.to_dict() == {
            'client_id': 'client_id',
            'redirect_uri': 'https://example.com/cli/authz_cb',
            'response_type': 'code',
            'state': 'state'
        }

    def test_response(self):
        _state = "today"
        req_args = {'response_type': 'code', 'state': _state}
        self.auth_service.endpoint = 'https://example.com/authorize'
        _info = self.auth_service.get_request_parameters(request_args=req_args)
        assert set(_info.keys()) == {'url', 'method', 'request'}
        msg = AuthorizationRequest().from_urlencoded(
            self.auth_service.get_urlinfo(_info['url']))
        self.auth_service.client_get("service_context").state.store_item(
            msg, "auth_request", _state)

        resp1 = AuthorizationResponse(code="auth_grant", state=_state)
        response = self.auth_service.parse_response(resp1.to_urlencoded(),
                                                    "urlencoded",
                                                    state=_state)
        self.auth_service.update_service_context(response, key=_state)
        assert self.auth_service.client_get("service_context").state.get_state(
            _state)