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]
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'}