def acb(self, op_hash='', **kwargs): logger.debug('Callback kwargs: {}'.format(kwargs)) rp = self.get_rp(op_hash) try: session_info = self.rph.session_interface.get_state( kwargs['state']) except KeyError: raise cherrypy.HTTPError(400, 'Unknown state') logger.debug('Session info: {}'.format(session_info)) # rp.service_context.provider_info['issuer'] != state_info['iss']: # raise cherrypy.HTTPError(400, 'Wrong Issuer') res = self.rph.finalize(session_info['iss'], kwargs) if is_error_message(res): raise cherrypy.HTTPError(400, res['error']) else: fname = os.path.join(self.html_home, 'opresult.html') _pre_html = open(fname, 'r').read() _html = _pre_html.format(result=create_result_page( userinfo=res['userinfo'], access_token=res['token'], client=rp)) return as_bytes(_html)
def refresh_access_token(self, state, client=None, scope=''): """ Refresh an access token using a refresh_token. When asking for a new access token the RP can ask for another scope for the new token. :param client: A Client instance :param state: The state key (the state parameter in the authorization request) :param scope: What the returned token should be valid for. :return: A :py:class:`oidcmsg.oidc.AccessTokenResponse` instance """ if scope: req_args = {'scope': scope} else: req_args = {} if client is None: client = self.get_client_from_session_key(state) try: tokenresp = client.do_request( 'refresh_token', authn_method=self.get_client_authn_method(client, "token_endpoint"), state=state, request_args=req_args ) except Exception as err: message = traceback.format_exception(*sys.exc_info()) logger.error(message) raise else: if is_error_message(tokenresp): raise OidcServiceError(tokenresp['error']) return tokenresp
def service_endpoint(self, name, **kwargs): logger.info(kwargs) logger.info('At the {} endpoint'.format(name)) endpoint = self.endpoint_context.endpoint[name] try: authn = cherrypy.request.headers['Authorization'] except KeyError: pr_args = {} else: pr_args = {'auth': authn} if endpoint.request_placement == 'body': if cherrypy.request.process_request_body is True: _request = cherrypy.request.body.read() else: raise cherrypy.HTTPError(400, 'Missing HTTP body') if not _request: _request = kwargs req_args = endpoint.parse_request(_request, **pr_args) else: req_args = endpoint.parse_request(kwargs, **pr_args) logger.info('request: {}'.format(req_args)) if is_error_message(req_args): return as_bytes(req_args.to_json()) args = endpoint.process_request(req_args) return self.do_response(endpoint, req_args, **args)
def get_user_info(self, state, client=None, access_token='', **kwargs): """ use the access token previously acquired to get some userinfo :param client: A Client instance :param state: The state value, this is the key into the session data store :param access_token: An access token :param kwargs: Extra keyword arguments :return: A :py:class:`oidcmsg.oidc.OpenIDSchema` instance """ if not access_token: _arg = self.session_interface.multiple_extend_request_args( {}, state, ['access_token'], ['auth_response', 'token_response', 'refresh_token_response']) request_args = {'access_token': access_token} if client is None: client = self.get_client_from_session_key(state) resp = client.do_request('userinfo', state=state, request_args=request_args, **kwargs) if is_error_message(resp): raise OidcServiceError(resp['error']) return resp
def get_access_and_id_token(self, authorization_response=None, state='', client=None): """ There are a number of services where access tokens and ID tokens can occur in the response. This method goes through the possible places based on the response_type the client uses. :param authorization_response: The Authorization response :param state: The state key (the state parameter in the authorization request) :return: A dictionary with 2 keys: **access_token** with the access token as value and **id_token** with a verified ID Token if one was returned otherwise None. """ if client is None: client = self.get_client_from_session_key(state) if authorization_response is None: if state: authorization_response = client.session_interface.get_item( AuthorizationResponse, 'auth_response', state) else: raise ValueError( 'One of authorization_response or state must be provided') if not state: state = authorization_response['state'] authreq = client.session_interface.get_item(AuthorizationRequest, 'auth_request', state) _resp_type = set(authreq['response_type']) access_token = None id_token = None if _resp_type in [{'id_token'}, {'id_token', 'token'}, {'code', 'id_token', 'token'}]: id_token = authorization_response['__verified_id_token'] if _resp_type in [{'token'}, {'id_token', 'token'}, {'code', 'token'}, {'code', 'id_token', 'token'}]: access_token = authorization_response["access_token"] elif _resp_type in [{'code'}, {'code', 'id_token'}]: # get the access token token_resp = self.get_access_token(state, client=client) if is_error_message(token_resp): return False, "Invalid response %s." % token_resp["error"] access_token = token_resp["access_token"] try: id_token = token_resp['__verified_id_token'] except KeyError: pass return {'access_token': access_token, 'id_token': id_token}
def finalize(self, issuer, response): """ The third of the high level methods that a user of this Class should know about. Once the consumer has redirected the user back to the callback URL there might be a number of services that the client should use. Which one those are are defined by the client configuration. :param issuer: Who sent the response :param response: The Authorization response as a dictionary :returns: A dictionary with two claims: **state** The key under which the session information is stored in the data store and **error** and encountered error or **userinfo** The collected user information """ client = self.issuer2rp[issuer] authorization_response = self.finalize_auth(client, issuer, response) if is_error_message(authorization_response): return { 'state': authorization_response['state'], 'error': authorization_response['error'] } _state = authorization_response['state'] token = self.get_access_and_id_token(authorization_response, state=_state, client=client) if 'userinfo' in client.service and token['access_token']: inforesp = self.get_user_info( state=authorization_response['state'], client=client, access_token=token['access_token']) if isinstance(inforesp, ResponseMessage) and 'error' in inforesp: return { 'error': "Invalid response %s." % inforesp["error"], 'state': _state } elif token['id_token']: # look for it in the ID Token inforesp = self.userinfo_in_id_token(token['id_token']) else: inforesp = {} logger.debug("UserInfo: %s", inforesp) return { 'userinfo': inforesp, 'state': authorization_response['state'], 'token': token['access_token'] }
def test_error_message(self): err = ResponseMessage(error="invalid_request", error_description="Something was missing", error_uri="http://example.com/error_message.html") ue_str = err.to_urlencoded() del err["error_uri"] ueo_str = err.to_urlencoded() assert ue_str != ueo_str assert "error_message" not in ueo_str assert "error_message" in ue_str assert is_error_message(err)
def finalize_auth(self, client, issuer: str, response: dict, behaviour_args: Optional[dict] = None): """ Given the response returned to the redirect_uri, parse and verify it. :param behaviour_args: For fine tuning behaviour :param client: A Client instance :param issuer: An Issuer ID :param response: The authorization response as a dictionary :return: An :py:class:`oidcmsg.oidc.AuthorizationResponse` or :py:class:`oidcmsg.oauth2.AuthorizationResponse` instance. """ logger.debug(20 * "*" + " finalize_auth " + 20 * "*") _srv = client.get_service('authorization') try: authorization_response = _srv.parse_response( response, sformat='dict', behaviour_args=behaviour_args) except Exception as err: logger.error('Parsing authorization_response: {}'.format(err)) message = traceback.format_exception(*sys.exc_info()) logger.error(message) raise else: logger.debug('Authz response: {}'.format( authorization_response.to_dict())) if is_error_message(authorization_response): return authorization_response _context = client.client_get("service_context") try: _iss = _context.state.get_iss(authorization_response['state']) except KeyError: raise KeyError('Unknown state value') if _iss != issuer: logger.error('Issuer problem: {} != {}'.format(_iss, issuer)) # got it from the wrong bloke raise ValueError('Impersonator {}'.format(issuer)) _srv.update_service_context(authorization_response, key=authorization_response['state']) _context.state.store_item(authorization_response, "auth_response", authorization_response['state']) return authorization_response
def get_tokens(self, state, client: Optional[Client] = None): """ Use the 'accesstoken' service to get an access token from the OP/AS. :param state: The state key (the state parameter in the authorization request) :param client: A Client instance :return: A :py:class:`oidcmsg.oidc.AccessTokenResponse` or :py:class:`oidcmsg.oauth2.AuthorizationResponse` """ logger.debug(20 * "*" + " get_tokens " + 20 * "*") if client is None: client = self.get_client_from_session_key(state) _context = client.client_get("service_context") authorization_response = _context.state.get_item( AuthorizationResponse, 'auth_response', state) authorization_request = _context.state.get_item( AuthorizationRequest, 'auth_request', state) req_args = { 'code': authorization_response['code'], 'state': state, 'redirect_uri': authorization_request['redirect_uri'], 'grant_type': 'authorization_code', 'client_id': client.get_client_id(), 'client_secret': _context.get('client_secret') } logger.debug('request_args: {}'.format(req_args)) try: tokenresp = client.do_request( 'accesstoken', request_args=req_args, authn_method=self.get_client_authn_method( client, "token_endpoint"), state=state) except Exception as err: message = traceback.format_exception(*sys.exc_info()) logger.error(message) raise else: if is_error_message(tokenresp): raise OidcServiceError(tokenresp['error']) return tokenresp
def repost_fragment(self, **kwargs): logger.debug('repost_fragment kwargs: {}'.format(kwargs)) args = compact(parse_qs(kwargs['url_fragment'])) op_hash = kwargs['op_hash'] rp = self.get_rp(op_hash) x = rp.service_context.state_db[args['state']] logger.debug('State info: {}'.format(x)) res = self.rph.finalize(x['as'], args) if is_error_message(res): raise cherrypy.HTTPError(400, res['error']) else: fname = os.path.join(self.html_home, 'opresult.html') _pre_html = open(fname, 'r').read() _html = _pre_html.format(result=create_result_page(userinfo=res[ 'userinfo'], access_token=res['token'], client=rp)) return as_bytes(_html)
def finalize_auth(self, client, issuer, response): """ Given the response returned to the redirect_uri, parse and verify it. :param client: A Client instance :param issuer: An Issuer ID :param response: The authorization response as a dictionary :return: An :py:class:`oidcmsg.oidc.AuthorizationResponse` or :py:class:`oidcmsg.oauth2.AuthorizationResponse` instance. """ _srv = client.service['authorization'] try: authorization_response = _srv.parse_response(response, sformat='dict') except Exception as err: logger.error('Parsing authorization_response: {}'.format(err)) message = traceback.format_exception(*sys.exc_info()) logger.error(message) raise else: logger.debug('Authz response: {}'.format( authorization_response.to_dict())) if is_error_message(authorization_response): return authorization_response try: _iss = self.session_interface.get_iss( authorization_response['state']) except KeyError: raise KeyError('Unknown state value') if _iss != issuer: logger.error('Issuer problem: {} != {}'.format(_iss, issuer)) # got it from the wrong bloke raise ValueError('Impersonator {}'.format(issuer)) _srv.update_service_context(authorization_response, state=authorization_response['state']) self.session_interface.store_item(authorization_response, "auth_response", authorization_response['state']) return authorization_response
def dynamic_provider_info_discovery(client): """ This is about performing dynamic Provider Info discovery :param client: A :py:class:`oidcservice.oidc.Client` instance """ try: client.service['provider_info'] except KeyError: raise ConfigurationError('Can not do dynamic provider info discovery') else: try: client.service_context.set( 'issuer', client.service_context.config['srv_discovery_url']) except KeyError: pass response = client.do_request('provider_info') if is_error_message(response): raise OidcServiceError(response['error'])
def dynamic_provider_info_discovery(client: Client, behaviour_args: Optional[dict] = None): """ This is about performing dynamic Provider Info discovery :param behaviour_args: :param client: A :py:class:`oidcrp.oidc.Client` instance """ try: client.get_service('provider_info') except KeyError: raise ConfigurationError('Can not do dynamic provider info discovery') else: _context = client.client_get("service_context") try: _context.set('issuer', _context.config['srv_discovery_url']) except KeyError: pass response = client.do_request('provider_info', behaviour_args=behaviour_args) if is_error_message(response): raise OidcServiceError(response['error'])
def finalize(self, issuer, response, behaviour_args: Optional[dict] = None): """ The third of the high level methods that a user of this Class should know about. Once the consumer has redirected the user back to the callback URL there might be a number of services that the client should use. Which one those are are defined by the client configuration. :param behaviour_args: For fine tuning :param issuer: Who sent the response :param response: The Authorization response as a dictionary :returns: A dictionary with two claims: **state** The key under which the session information is stored in the data store and **error** and encountered error or **userinfo** The collected user information """ client = self.issuer2rp[issuer] if behaviour_args: logger.debug(f"Finalize behaviour args: {behaviour_args}") authorization_response = self.finalize_auth(client, issuer, response) if is_error_message(authorization_response): return { 'state': authorization_response['state'], 'error': authorization_response['error'] } _state = authorization_response['state'] token = self.get_access_and_id_token(authorization_response, state=_state, client=client, behaviour_args=behaviour_args) _id_token = token.get("id_token") logger.debug(f"ID Token: {_id_token}") if client.client_get("service", "userinfo") and token['access_token']: inforesp = self.get_user_info( state=authorization_response['state'], client=client, access_token=token['access_token']) if isinstance(inforesp, ResponseMessage) and 'error' in inforesp: return { 'error': "Invalid response %s." % inforesp["error"], 'state': _state } elif _id_token: # look for it in the ID Token inforesp = self.userinfo_in_id_token(_id_token) else: inforesp = {} logger.debug("UserInfo: %s", inforesp) _context = client.client_get("service_context") try: _sid_support = _context.get( 'provider_info')['backchannel_logout_session_supported'] except KeyError: try: _sid_support = _context.get( 'provider_info')['frontchannel_logout_session_supported'] except: _sid_support = False if _sid_support and _id_token: try: sid = _id_token['sid'] except KeyError: pass else: _context.state.store_sid2state(sid, _state) if _id_token: _context.state.store_sub2state(_id_token['sub'], _state) else: _context.state.store_sub2state(inforesp['sub'], _state) return { 'userinfo': inforesp, 'state': authorization_response['state'], 'token': token['access_token'], 'id_token': _id_token, 'session_state': authorization_response.get('session_state', '') }
def test_auth_error_message(self): resp = AuthorizationResponse( error="invalid_request", error_description="Something was missing" ) assert is_error_message(resp)
def parse_response(self, info, sformat="", state="", **kwargs): """ This the start of a pipeline that will: 1 Deserializes a response into it's response message class. Or :py:class:`oidcmsg.oauth2.ErrorResponse` if it's an error message 2 verifies the correctness of the response by running the verify method belonging to the message class used. 3 runs the do_post_parse_response method iff the response was not an error response. :param info: The response, can be either in a JSON or an urlencoded format :param sformat: Which serialization that was used :param state: The state :param kwargs: Extra key word arguments :return: The parsed and to some extend verified response """ if not sformat: sformat = self.response_body_type LOGGER.debug('response format: %s', sformat) if sformat in ['jose', 'jws', 'jwe']: resp = self.post_parse_response(info, state=state) if not resp: LOGGER.error('Missing or faulty response') raise ResponseError("Missing or faulty response") return resp # If format is urlencoded 'info' may be a URL # in which case I have to get at the query/fragment part if sformat == "urlencoded": info = self.get_urlinfo(info) if sformat == 'jwt': info = self._do_jwt(info) sformat = "dict" LOGGER.debug('response_cls: %s', self.response_cls.__name__) resp = self._do_response(info, sformat, **kwargs) LOGGER.debug('Initial response parsing => "%s"', resp.to_dict()) # is this an error message if is_error_message(resp): LOGGER.debug('Error response: %s', resp) else: vargs = self.gather_verify_arguments() LOGGER.debug("Verify response with %s", vargs) try: # verify the message. If something is wrong an exception is # thrown resp.verify(**vargs) except Exception as err: LOGGER.error( 'Got exception while verifying response: %s', err) raise resp = self.post_parse_response(resp, state=state) if not resp: LOGGER.error('Missing or faulty response') raise ResponseError("Missing or faulty response") return resp
def get_access_and_id_token(self, authorization_response=None, state: Optional[str] = '', client: Optional[object] = None, behaviour_args: Optional[dict] = None): """ There are a number of services where access tokens and ID tokens can occur in the response. This method goes through the possible places based on the response_type the client uses. :param behaviour_args: For fine tuning behaviour :param authorization_response: The Authorization response :param state: The state key (the state parameter in the authorization request) :return: A dictionary with 2 keys: **access_token** with the access token as value and **id_token** with a verified ID Token if one was returned otherwise None. """ logger.debug(20 * "*" + " get_access_and_id_token " + 20 * "*") if client is None: client = self.get_client_from_session_key(state) _context = client.client_get("service_context") if authorization_response is None: if state: authorization_response = _context.state.get_item( AuthorizationResponse, 'auth_response', state) else: raise ValueError( 'One of authorization_response or state must be provided') if not state: state = authorization_response['state'] authreq = _context.state.get_item(AuthorizationRequest, 'auth_request', state) _resp_type = set(authreq['response_type']) access_token = None id_token = None if _resp_type in [{'id_token'}, {'id_token', 'token'}, {'code', 'id_token', 'token'}]: id_token = authorization_response['__verified_id_token'] if _resp_type in [{'token'}, {'id_token', 'token'}, {'code', 'token'}, {'code', 'id_token', 'token'}]: access_token = authorization_response["access_token"] if behaviour_args: if behaviour_args.get("collect_tokens", False): # get what you can from the token endpoint token_resp = self.get_tokens(state, client=client) if is_error_message(token_resp): return False, "Invalid response %s." % token_resp[ "error"] # Now which access_token should I use access_token = token_resp["access_token"] # May or may not get an ID Token id_token = token_resp.get('__verified_id_token') elif _resp_type in [{'code'}, {'code', 'id_token'}]: # get the access token token_resp = self.get_tokens(state, client=client) if is_error_message(token_resp): return False, "Invalid response %s." % token_resp["error"] access_token = token_resp["access_token"] # May or may not get an ID Token id_token = token_resp.get('__verified_id_token') return {'access_token': access_token, 'id_token': id_token}