def get_userinfo_claims(self, access_token, endpoint, method="POST", schema_class=OpenIDSchema, **kwargs): uir = UserInfoRequest(access_token=access_token) h_args = dict([(k, v) for k, v in kwargs.items() if k in HTTP_ARGS]) if "authn_method" in kwargs: http_args = self.init_authentication_method(**kwargs) else: # If nothing defined this is the default http_args = self.init_authentication_method( uir, "bearer_header", **kwargs) h_args.update(http_args) path, body, kwargs = self.get_or_post(endpoint, method, uir, **kwargs) try: resp = self.http_request(path, method, data=body, **h_args) except oauth2.MissingRequiredAttribute: raise if resp.status_code == 200: assert "application/json" in resp.headers["content-type"] elif resp.status_code == 500: raise PyoidcError("ERROR: Something went wrong: %s" % resp.text) else: raise PyoidcError("ERROR: Something went wrong [%s]" % resp.status_code) return schema_class().from_json(txt=resp.text)
def handle_response(self, r, csi): data = self.conv.events.last_item(EV_REQUEST) state = data['state'] if 300 < r.status_code < 400: resp = self.conv.entity.parse_response(self.response, info=r.headers['location'], sformat="urlencoded", state=state) elif r.status_code == 200: if "response_mode" in csi and csi["response_mode"] == "form_post": resp = self.response() forms = BeautifulSoup(r.content).findAll('form') for inp in forms[0].find_all("input"): resp[inp.attrs["name"]] = inp.attrs["value"] else: resp = self.conv.entity.parse_response( self.response, info=r.headers['location'], sformat="urlencoded", state=state) resp.verify(keyjar=self.conv.entity.keyjar) else: resp = r if not isinstance(resp, Response): # Not a HTTP response try: try: _id_token = resp["id_token"] except KeyError: pass else: if "kid" not in _id_token.jws_header and not \ _id_token.jws_header["alg"] == "HS256": for key, value in \ self.conv.entity.keyjar.issuer_keys.items(): if not key == "" and (len(value) > 1 or len( list(value[0].keys())) > 1): raise PyoidcError( "No 'kid' in id_token header!") if self.req_args['nonce'] != _id_token['nonce']: raise PyoidcError("invalid nonce! {} != {}".format( self.req_args['nonce'], _id_token['nonce'])) if not same_issuer(self.conv.info["issuer"], _id_token["iss"]): raise IssuerMismatch(" {} != {}".format( self.conv.info["issuer"], _id_token["iss"])) self.conv.entity.id_token = _id_token except KeyError: pass return resp
def do_user_info_request(self, method="POST", state="", scope="openid", request="openid", **kwargs): kwargs["request"] = request path, body, method, h_args = self.user_info_request( method, state, scope, **kwargs) logger.debug("[do_user_info_request] PATH:%s BODY:%s H_ARGS: %s" % (path, body, h_args)) try: resp = self.http_request(path, method, data=body, **h_args) except oauth2.MissingRequiredAttribute: raise if resp.status_code == 200: try: assert "application/json" in resp.headers["content-type"] sformat = "json" except AssertionError: assert "application/jwt" in resp.headers["content-type"] sformat = "jwt" elif resp.status_code == 500: raise PyoidcError("ERROR: Something went wrong: %s" % resp.text) else: raise PyoidcError("ERROR: Something went wrong [%s]: %s" % (resp.status_code, resp.text)) try: _schema = kwargs["user_info_schema"] except KeyError: _schema = OpenIDSchema logger.debug("Reponse text: '%s'" % resp.text) if sformat == "json": return _schema().from_json(txt=resp.text) else: algo = self.client_prefs["userinfo_signed_response_alg"] _kty = alg2keytype(algo) # Keys of the OP ? try: keys = self.keyjar.get_signing_key(_kty, self.kid["sig"][_kty]) except KeyError: keys = self.keyjar.get_signing_key(_kty) return _schema().from_jwt(resp.text, keys)
def _run(self): if self.skip: return self.conv.trace.info( "Access Token Request with op_args: {}, req_args: {}".format( self.op_args, self.req_args)) atr = self.conv.entity.do_access_token_request( request_args=self.req_args, **self.op_args) if "error" in atr: self.conv.trace.response("Access Token response: {}".format(atr)) return False try: _jws_alg = atr["id_token"].jws_header["alg"] except (KeyError, AttributeError): pass else: if _jws_alg == "none": pass elif "kid" not in atr[ "id_token"].jws_header and not _jws_alg == "HS256": keys = self.conv.entity.keyjar.keys_by_alg_and_usage( self.conv.info["issuer"], _jws_alg, "ver") if len(keys) > 1: raise PyoidcError("No 'kid' in id_token header!") if not same_issuer(self.conv.info["issuer"], atr["id_token"]["iss"]): raise IssuerMismatch(" {} != {}".format(self.conv.info["issuer"], atr["id_token"]["iss"])) self.conv.trace.response(atr) assert isinstance(atr, AccessTokenResponse)
def provider_config(self, issuer, keys=True, endpoints=True, response_cls=ProviderConfigurationResponse, serv_pattern=OIDCONF_PATTERN): if issuer.endswith("/"): _issuer = issuer[:-1] else: _issuer = issuer url = serv_pattern % _issuer pcr = None r = self.http_request(url) if r.status_code == 200: pcr = response_cls().from_json(r.text) elif r.status_code == 302: while r.status_code == 302: r = self.http_request(r.headers["location"]) if r.status_code == 200: pcr = response_cls().from_json(r.text) break if pcr is None: raise PyoidcError("Trying '%s', status %s" % (url, r.status_code)) self.handle_provider_config(pcr, issuer, keys, endpoints) return pcr
def _parse_request(self, request, data, sformat, client_id=None): if sformat == "json": request = request().from_json(data) elif sformat == "jwt": request = request().from_jwt(data, keyjar=self.keyjar) elif sformat == "urlencoded": if '?' in data: parts = urlparse.urlparse(data) scheme, netloc, path, params, query, fragment = parts[:6] else: query = data request = request().from_urlencoded(query) else: raise PyoidcError("Unknown package format: '%s'" % sformat, request) # get the verification keys if client_id: keys = self.keyjar.verify_keys(client_id) else: try: keys = self.keyjar.verify_keys(request["client_id"]) except KeyError: keys = None request.verify(key=keys, keyjar=self.keyjar) return request
def complete(self, state): """ Do the access token request, the last step in a code flow. If Implicit flow was used then this method is never used. """ args = {"redirect_uri": self.redirect_uris[0]} if "password" in self.config and self.config["password"]: logger.info("basic auth") http_args = {"password": self.config["password"]} elif self.client_secret: logger.info("request_body auth") http_args = {} args.update({ "client_secret": self.client_secret, "client_id": self.client_id, "secret_type": self.secret_type }) else: raise PyoidcError("Nothing to authenticate with") resp = self.do_access_token_request(state=state, request_args=args, http_args=http_args) logger.info("Access Token Response: %s" % resp) if resp.type() == "ErrorResponse": raise TokenError(resp.error, resp) #self._backup(self.sdb["seed:%s" % _cli.seed]) self._backup(state) return resp
def claims_ser(val, sformat="urlencoded", lev=0): # everything in c_extension if isinstance(val, six.string_types): item = val elif isinstance(val, list): item = val[0] else: item = val if isinstance(item, Message): return item.serialize(method=sformat, lev=lev + 1) if sformat == "urlencoded": res = urllib.urlencode(item) elif sformat == "json": if lev: res = item else: res = json.dumps(item) elif sformat == "dict": if isinstance(item, dict): res = item else: raise MessageException("Wrong type: %s" % type(item)) else: raise PyoidcError("Unknown sformat: %s" % sformat, val) return res
def handle_registration_info(self, response): if response.status_code == 200: resp = ClientInfoResponse().deserialize(response.text, "json") self.store_registration_info(resp) else: err = ErrorResponse().deserialize(response.text, "json") raise PyoidcError("Registration failed: %s" % err.get_json()) return resp
def factory(msgtype): try: return MSG[msgtype] except KeyError: if msgtype == "ErrorResponse": return message.ErrorResponse elif msgtype == "Message": return message.Message else: raise PyoidcError("Unknown message type: %s" % msgtype)
def handle_registration_info(self, response): if response.status_code in SUCCESSFUL: resp = ClientInfoResponse().deserialize(response.text, "json") self.store_response(resp, response.text) self.store_registration_info(resp) else: resp = ErrorResponse().deserialize(response.text, "json") try: resp.verify() self.store_response(resp, response.text) except Exception: raise PyoidcError('Registration failed: {}'.format( response.text)) return resp
def handle_registration_info(self, response): if response.status_code in SUCCESSFUL: resp = self.message_factory.get_response_type( "registration_endpoint" )().deserialize(response.text, "json") self.store_response(resp, response.text) self.store_registration_info(resp) else: resp = ErrorResponse().deserialize(response.text, "json") try: resp.verify() self.store_response(resp, response.text) except Exception: raise PyoidcError("Registration failed: {}".format(response.text)) return resp
def handle_provider_config(self, pcr, issuer, keys=True, endpoints=True): """ Deal with Provider Config Response :param pcr: The ProviderConfigResponse instance :param issuer: The one I thought should be the issuer of the config :param keys: Should I deal with keys :param endpoints: Should I deal with endpoints, that is store them as attributes in self. """ if "issuer" in pcr: _pcr_issuer = pcr["issuer"] if pcr["issuer"].endswith("/"): if issuer.endswith("/"): _issuer = issuer else: _issuer = issuer + "/" else: if issuer.endswith("/"): _issuer = issuer[:-1] else: _issuer = issuer try: _ = self.allow["issuer_mismatch"] except KeyError: try: assert _issuer == _pcr_issuer except AssertionError: raise PyoidcError( "provider info issuer mismatch '%s' != '%s'" % (_issuer, _pcr_issuer)) self.provider_info[_pcr_issuer] = pcr else: _pcr_issuer = issuer if endpoints: for key, val in pcr.items(): if key.endswith("_endpoint"): setattr(self, key, val) if keys: if self.keyjar is None: self.keyjar = KeyJar() self.keyjar.load_keys(pcr, _pcr_issuer)
def msg_ser(inst, sformat, lev=0): if sformat in ["urlencoded", "json"]: if isinstance(inst, dict) or isinstance(inst, Message): res = inst.serialize(sformat, lev) else: res = inst elif sformat == "dict": if isinstance(inst, Message): res = inst.serialize(sformat, lev) elif isinstance(inst, dict): res = inst else: raise ValueError("%s" % type(inst)) else: raise PyoidcError("Unknown sformat", inst) return res
def msg_ser(inst, sformat, lev=0): if sformat in ["urlencoded", "json"]: if isinstance(inst, dict) or isinstance(inst, Message): res = inst.serialize(sformat, lev) else: res = inst elif sformat == "dict": if isinstance(inst, Message): res = inst.serialize(sformat, lev) elif isinstance(inst, dict): res = inst elif isinstance(inst, six.string_types): # Iff ID Token res = inst else: raise MessageException("Wrong type: %s" % type(inst)) else: raise PyoidcError("Unknown sformat", inst) return res
def construct_UserInfoRequest(self, request=UserInfoRequest, request_args=None, extra_args=None, **kwargs): if request_args is None: request_args = {} if "access_token" in request_args: pass else: if "scope" not in kwargs: kwargs["scope"] = "openid" token = self.get_token(**kwargs) if token is None: raise PyoidcError("No valid token available") request_args["access_token"] = token.access_token return self.construct_request(request, request_args, extra_args)
def _run(self): if self.skip: return self.conv.events.store(EV_REQUEST, "op_args: {}, req_args: {}".format( self.op_args, self.req_args), direction=OUTGOING) atr = self.conv.entity.do_access_token_request( request_args=self.req_args, **self.op_args) if "error" in atr: self.conv.events.store(EV_PROTOCOL_RESPONSE, atr, direction=INCOMING) return False try: _jws_alg = atr["id_token"].jws_header["alg"] except (KeyError, AttributeError): pass else: if _jws_alg == "none": pass elif "kid" not in atr[ "id_token"].jws_header and not _jws_alg == "HS256": keys = self.conv.entity.keyjar.keys_by_alg_and_usage( self.conv.info["issuer"], _jws_alg, "ver") if len(keys) > 1: raise PyoidcError("No 'kid' in id_token header!") if not same_issuer(self.conv.info["issuer"], atr["id_token"]["iss"]): raise IssuerMismatch(" {} != {}".format(self.conv.info["issuer"], atr["id_token"]["iss"])) #assert isinstance(atr, AccessTokenResponse) return atr
def _id_token_based(self, request, request_args=None, extra_args=None, **kwargs): if request_args is None: request_args = {} try: _prop = kwargs["prop"] except KeyError: _prop = "id_token" if _prop in request_args: pass else: id_token = self._get_id_token(**kwargs) if id_token is None: raise PyoidcError("No valid id token available") request_args[_prop] = id_token return self.construct_request(request, request_args, extra_args)
def do_user_info_request(self, method="POST", state="", scope="openid", request="openid", **kwargs): kwargs["request"] = request path, body, method, h_args = self.user_info_request( method, state, scope, **kwargs) logger.debug( "[do_user_info_request] PATH:{} BODY:{} H_ARGS: {}".format( path, body, h_args)) if self.events: self.events.store('Request', {'body': body}) self.events.store('request_url', path) self.events.store('request_http_args', h_args) try: resp = self.http_request(path, method, data=body, **h_args) except MissingRequiredAttribute: raise if resp.status_code == 200: try: assert "application/json" in resp.headers["content-type"] sformat = "json" except AssertionError: assert "application/jwt" in resp.headers["content-type"] sformat = "jwt" elif resp.status_code == 500: raise PyoidcError("ERROR: Something went wrong: %s" % resp.text) elif 400 <= resp.status_code < 500: # the response text might be a OIDC message try: res = ErrorResponse().from_json(resp.text) except Exception: raise RequestError(resp.text) else: self.store_response(res, resp.text) return res else: raise PyoidcError("ERROR: Something went wrong [%s]: %s" % (resp.status_code, resp.text)) try: _schema = kwargs["user_info_schema"] except KeyError: _schema = OpenIDSchema logger.debug("Reponse text: '{}'".format(resp.text)) _txt = resp.text if sformat == "json": res = _schema().from_json(txt=_txt) else: verify = kwargs.get('verify', True) res = _schema().from_jwt(_txt, keyjar=self.keyjar, sender=self.provider_info["issuer"], verify=verify) if 'error' in res: # Error response res = UserInfoErrorResponse(**res.to_dict()) # TODO verify issuer:sub against what's returned in the ID Token self.store_response(res, _txt) return res
def begin(self, scope="", response_type="", use_nonce=False, path="", **kwargs): """ Begin the OIDC flow :param scope: Defines which user info claims is wanted :param response_type: Controls the parameters returned in the response from the Authorization Endpoint :param use_nonce: If not implicit flow nonce is optional. This defines if it should be used anyway. :param path: The path part of the redirect URL :return: A 2-tuple, session identifier and URL to which the user should be redirected """ _log_info = logger.info if self.debug: _log_info("- begin -") _page = self.config["authz_page"] if not path.endswith("/"): if _page.startswith("/"): self.redirect_uris = [path + _page] else: self.redirect_uris = ["%s/%s" % (path, _page)] else: if _page.startswith("/"): self.redirect_uris = [path + _page[1:]] else: self.redirect_uris = ["%s/%s" % (path, _page)] # Put myself in the dictionary of sessions, keyed on session-id if not self.seed: self.seed = rndstr() if not scope: scope = self.config["scope"] if not response_type: response_type = self.config["response_type"] sid = stateID(path, self.seed) self.grant[sid] = Grant(seed=self.seed) self._backup(sid) self.sdb["seed:%s" % self.seed] = sid args = { "client_id": self.client_id, "state": sid, "response_type": response_type, "scope": scope, } # nonce is REQUIRED in implicit flow, # OPTIONAL on code flow. if "token" in response_type or use_nonce: self.nonce = rndstr(12) args["nonce"] = self.nonce if "max_age" in self.config: args["max_age"] = self.config["max_age"] _claims = None if "user_info" in self.config: _claims = ClaimsRequest(userinfo=Claims( **self.config["user_info"])) if "id_token" in self.config: if _claims: _claims["id_token"] = Claims(**self.config["id_token"]) else: _claims = ClaimsRequest(id_token=Claims( **self.config["id_token"])) if _claims: args["claims"] = _claims if "request_method" in self.config: areq = self.construct_AuthorizationRequest(request_args=args, extra_args=None, request_param="request") if self.config["request_method"] == "file": id_request = areq["request"] del areq["request"] _filedir = self.config["temp_dir"] _webpath = self.config["temp_path"] _name = rndstr(10) filename = os.path.join(_filedir, _name) while os.path.exists(filename): _name = rndstr(10) filename = os.path.join(_filedir, _name) fid = open(filename, mode="w") fid.write(id_request) fid.close() _webname = "%s%s%s" % (path, _webpath, _name) areq["request_uri"] = _webname self.request_uri = _webname self._backup(sid) else: if "userinfo_claims" in args: # can only be carried in an IDRequest raise PyoidcError("Need a request method") areq = self.construct_AuthorizationRequest(AuthorizationRequest, request_args=args) location = areq.request(self.authorization_endpoint) if self.debug: _log_info("Redirecting to: %s" % location) return sid, location
def do_user_info_request(self, method="POST", state="", scope="openid", request="openid", **kwargs): kwargs["request"] = request path, body, method, h_args = self.user_info_request( method, state, scope, **kwargs) logger.debug( "[do_user_info_request] PATH:{} BODY:{} H_ARGS: {}".format( path, body, h_args)) if self.events: self.events.store('Request', {'body': body}) self.events.store('request_url', path) self.events.store('request_http_args', h_args) try: resp = self.http_request(path, method, data=body, **h_args) except MissingRequiredAttribute: raise if resp.status_code == 200: try: assert "application/json" in resp.headers["content-type"] sformat = "json" except AssertionError: assert "application/jwt" in resp.headers["content-type"] sformat = "jwt" elif resp.status_code == 500: raise PyoidcError("ERROR: Something went wrong: %s" % resp.text) elif resp.status_code == 401: try: www_auth_header = resp.headers["WWW-Authenticate"] res = ErrorResponse( error="invalid_token", error_description= "Server returned 401 Unauthorized. WWW-Authenticate header:{}" .format(www_auth_header)) except KeyError: #no www-authenticate header. https://tools.ietf.org/html/rfc6750#section-3 reads: #the resource server MUST include the HTTP "WWW-Authenticate" response header field res = ErrorResponse( error="invalid_token", error_description= "Server returned 401 Unauthorized without a WWW-Authenticate header which is required as per https://tools.ietf.org/html/rfc6750#section-3" ) self.store_response(res, resp.text) return res elif 400 <= resp.status_code < 500: # the response text might be a OIDC message try: res = ErrorResponse().from_json(resp.text) except Exception: raise RequestError(resp.text) else: self.store_response(res, resp.text) return res else: raise PyoidcError("ERROR: Something went wrong [%s]: %s" % (resp.status_code, resp.text)) try: _schema = kwargs["user_info_schema"] except KeyError: _schema = OpenIDSchema logger.debug("Reponse text: '{}'".format(resp.text)) _txt = resp.text if sformat == "json": res = _schema().from_json(txt=_txt) else: verify = kwargs.get('verify', True) res = _schema().from_jwt(_txt, keyjar=self.keyjar, sender=self.provider_info["issuer"], verify=verify) if 'error' in res: # Error response res = UserInfoErrorResponse(**res.to_dict()) # TODO verify issuer:sub against what's returned in the ID Token self.store_response(res, _txt) return res