def test_client_info_init():
    config = {
        'client_id': 'client_id', 'issuer': 'issuer',
        'client_secret': 'client_secret_wordplay',
        'base_url': 'https://example.com',
        'requests_dir': 'requests'
    }
    ci = ServiceContext(config=config)
    for attr in config.keys():
        try:
            val = getattr(ci, attr)
        except AttributeError:
            val = ci.get(attr)

        assert val == config[attr]
示例#2
0
def test_request_object_encryption():
    msg = AuthorizationRequest(state='ABCDE',
                               redirect_uri='https://example.com/cb',
                               response_type='code')

    conf = {
        'redirect_uris': ['https://example.com/cli/authz_cb'],
        'client_id': 'client_1',
        'client_secret': 'abcdefghijklmnop',
    }
    service_context = ServiceContext(keyjar=KEYJAR, config=conf)
    _behav = service_context.get('behaviour')
    _behav["request_object_encryption_alg"] = 'RSA1_5'
    _behav["request_object_encryption_enc"] = "A128CBC-HS256"
    service_context.set('behaviour', _behav)

    _jwe = request_object_encryption(msg.to_json(),
                                     service_context,
                                     target=RECEIVER)
    assert _jwe

    _decryptor = factory(_jwe)

    assert _decryptor.jwt.verify_headers(alg='RSA1_5', enc='A128CBC-HS256')
class Client(object):
    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:`oidcservice.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
        """

        if httpc_params is None:
            httpc_params = {"verify": True}

        self.http = httplib or HTTPLib(httpc_params)

        # db_conf = config.get('db_conf')
        # if db_conf:
        #     _storage_cls_name = db_conf.get('abstract_storage_cls')
        #     self._storage_cls = importer(_storage_cls_name)
        #     self.db = self._storage_cls(db_conf.get('default'))
        #     if not keyjar:
        #         key_db_conf = db_conf.get('keyjar', db_conf.get('default'))
        #         keyjar = KeyJar(abstract_storage_cls=self._storage_cls, storage_conf=key_db_conf)
        #         keyjar.verify_ssl = verify_ssl

        self.events = None
        self.service_context = ServiceContext(keyjar,
                                              config=config,
                                              jwks_uri=jwks_uri,
                                              httpc_params=httpc_params)

        self.session_interface = StateInterface(self.service_context.state_db)

        if self.service_context.get('client_id'):
            self.client_id = self.service_context.get('client_id')

        _cam = client_authn_factory or ca_factory

        _srvs = services or DEFAULT_SERVICES

        self.service = init_services(_srvs, self.service_context, _cam)

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

        self.service_context.service = self.service
        # just ignore verify_ssl until it goes away
        self.verify_ssl = httpc_params.get("verify", True)

    def do_request(self,
                   request_type,
                   response_body_type="",
                   request_args=None,
                   **kwargs):

        _srv = self.service[request_type]

        _info = _srv.get_request_parameters(request_args=request_args,
                                            **kwargs)

        if not response_body_type:
            response_body_type = _srv.response_body_type

        logger.debug('do_request info: {}'.format(_info))

        try:
            _state = kwargs['state']
        except:
            _state = ''
        return self.service_request(_srv,
                                    response_body_type=response_body_type,
                                    state=_state,
                                    **_info)

    def set_client_id(self, client_id):
        self.client_id = client_id
        self.service_context.set('client_id', client_id)

    def get_response(self,
                     service,
                     url,
                     method="GET",
                     body=None,
                     response_body_type="",
                     headers=None,
                     **kwargs):
        """

        :param url:
        :param method:
        :param body:
        :param response_body_type:
        :param headers:
        :param kwargs:
        :return:
        """
        try:
            resp = self.http(url, method, data=body, headers=headers)
        except Exception as err:
            logger.error('Exception on request: {}'.format(err))
            raise

        if 300 <= resp.status_code < 400:
            return {'http_response': resp}

        if resp.status_code < 300:
            if "keyjar" not in kwargs:
                kwargs["keyjar"] = service.service_context.keyjar
            if not response_body_type:
                response_body_type = service.response_body_type

            if response_body_type == 'html':
                return resp.text

            if body:
                kwargs['request_body'] = body

        return self.parse_request_response(service, resp, response_body_type,
                                           **kwargs)

    def service_request(self,
                        service,
                        url,
                        method="GET",
                        body=None,
                        response_body_type="",
                        headers=None,
                        **kwargs):
        """
        The method that sends the request and handles the response returned.
        This assumes that the response arrives in the HTTP response.

        :param url: The URL to which the request should be sent
        :param method: Which HTTP method to use
        :param body: A message body if any
        :param response_body_type: The expected format of the body of the
            return message
        :param httpc_params: Arguments for the HTTP client
        :return: A cls or ResponseMessage instance or the HTTP response
            instance if no response body was expected.
        """

        if headers is None:
            headers = {}

        logger.debug(REQUEST_INFO.format(url, method, body, headers))

        try:
            response = service.get_response_ext(url, method, body,
                                                response_body_type, headers,
                                                **kwargs)
        except AttributeError:
            response = self.get_response(service, url, method, body,
                                         response_body_type, headers, **kwargs)

        if 'error' in response:
            pass
        else:
            try:
                kwargs['key'] = kwargs['state']
            except KeyError:
                pass

            service.update_service_context(response, **kwargs)
        return response

    def parse_request_response(self,
                               service,
                               reqresp,
                               response_body_type='',
                               state="",
                               **kwargs):
        """
        Deal with a self.http response. The response are expected to
        follow a special pattern, having the attributes:

            - headers (list of tuples with headers attributes and their values)
            - status_code (integer)
            - text (The text version of the response)
            - url (The calling URL)

        :param service: A :py:class:`oidcservice.service.Service` instance
        :param reqresp: The HTTP request response
        :param response_body_type: If response in body one of 'json', 'jwt' or
            'urlencoded'
        :param state: Session identifier
        :param kwargs: Extra keyword arguments
        :return:
        """

        # if not response_body_type:
        #     response_body_type = self.response_body_type

        if reqresp.status_code in SUCCESSFUL:
            logger.debug('response_body_type: "{}"'.format(response_body_type))
            _deser_method = get_deserialization_method(reqresp)

            if _deser_method != response_body_type:
                logger.warning('Not the body type I expected: {} != {}'.format(
                    _deser_method, response_body_type))
            if _deser_method in ['json', 'jwt', 'urlencoded']:
                value_type = _deser_method
            else:
                value_type = response_body_type

            logger.debug('Successful response: {}'.format(reqresp.text))

            try:
                return service.parse_response(reqresp.text, value_type, state,
                                              **kwargs)
            except Exception as err:
                logger.error(err)
                raise
        elif reqresp.status_code in [302, 303]:  # redirect
            return reqresp
        elif reqresp.status_code == 500:
            logger.error("(%d) %s" % (reqresp.status_code, reqresp.text))
            raise ParseError("ERROR: Something went wrong: %s" % reqresp.text)
        elif 400 <= reqresp.status_code < 500:
            logger.error('Error response ({}): {}'.format(
                reqresp.status_code, reqresp.text))
            # expecting an error response
            _deser_method = get_deserialization_method(reqresp)
            if not _deser_method:
                _deser_method = 'json'

            try:
                err_resp = service.parse_response(reqresp.text, _deser_method)
            except FormatError:
                if _deser_method != response_body_type:
                    try:
                        err_resp = service.parse_response(
                            reqresp.text, response_body_type)
                    except (OidcServiceError, FormatError):
                        raise OidcServiceError(
                            "HTTP ERROR: %s [%s] on %s" %
                            (reqresp.text, reqresp.status_code, reqresp.url))
                else:
                    raise OidcServiceError(
                        "HTTP ERROR: %s [%s] on %s" %
                        (reqresp.text, reqresp.status_code, reqresp.url))
            except JSONDecodeError:  # So it's not JSON assume text then
                err_resp = {'error': reqresp.text}

            err_resp['status_code'] = reqresp.status_code
            return err_resp
        else:
            logger.error('Error response ({}): {}'.format(
                reqresp.status_code, reqresp.text))
            raise OidcServiceError(
                "HTTP ERROR: %s [%s] on %s" %
                (reqresp.text, reqresp.status_code, reqresp.url))
def test_conversation():
    service_context = ServiceContext(
        RP_KEYJAR, {
            "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_SERVICES.copy()
    service_spec['WebFinger'] = {'class': WebFinger}

    service = init_services(service_spec, service_context=service_context)

    assert set(service.keys()) == {
        'accesstoken', 'authorization', 'webfinger', 'registration',
        'refresh_token', 'userinfo', 'provider_info'
    }

    service_context.service = service

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

    info = service['webfinger'].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 = service['webfinger'].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')
    ]

    service['webfinger'].update_service_context(resp=response)
    service_context.set('issuer', OP_BASEURL)

    # =================== Provider info discovery ====================

    info = service['provider_info'].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 = service['provider_info'].parse_response(provider_info_response)

    assert isinstance(resp, ProviderConfigurationResponse)
    service['provider_info'].update_service_context(resp)

    _pi = service_context.get('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 ====================

    info = service['registration'].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 = service['registration'].parse_response(
        op_client_registration_response)

    service['registration'].update_service_context(response)
    assert service_context.get('client_id') == 'zls2qhN1jO6A'
    assert service_context.get('client_secret') == 'c8434f28cf9375d9a7'
    assert set(service_context.get('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'

    info = service['authorization'].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 = service['authorization'].parse_response(_authz_rep.to_urlencoded())
    service['authorization'].update_service_context(_resp, key=STATE)
    _item = service['authorization'].get_item(AuthorizationResponse,
                                              'auth_response', STATE)
    assert _item['code'] == 'Z0FBQUFBQmFkdFFjUVpFWE81SHU5N1N4N01'

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

    request_args = {
        'state': STATE,
        'redirect_uri': service_context.get('redirect_uris')[0]
    }

    info = service['accesstoken'].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.set('issuer', OP_BASEURL)
    _resp = service['accesstoken'].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'
    }

    service['accesstoken'].update_service_context(_resp, key=STATE)

    _item = service['authorization'].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 ====================

    info = service['userinfo'].get_request_parameters(state=STATE)

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

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

    _resp = service['userinfo'].parse_response(json.dumps(op_resp),
                                               state=STATE)
    service['userinfo'].update_service_context(_resp, key=STATE)

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

    _item = service['authorization'].get_item(OpenIDSchema, 'user_info', STATE)
    assert _item.to_dict() == {'sub': '1b2fc9341a16ae4e30082965d537'}