diff options
-rw-r--r-- | example/idp2/htdocs/login.mako | 2 | ||||
-rwxr-xr-x | example/idp2/idp.py | 68 | ||||
-rwxr-xr-x | example/idp2/idp_uwsgi.py | 109 | ||||
-rwxr-xr-x | example/idp2_repoze/idp.py | 21 | ||||
-rwxr-xr-x | setup.py | 4 | ||||
-rw-r--r-- | src/saml2/__init__.py | 26 | ||||
-rw-r--r-- | src/saml2/authn.py | 16 | ||||
-rw-r--r-- | src/saml2/client.py | 6 | ||||
-rw-r--r-- | src/saml2/client_base.py | 43 | ||||
-rw-r--r-- | src/saml2/entity.py | 10 | ||||
-rw-r--r-- | src/saml2/response.py | 8 | ||||
-rw-r--r-- | src/saml2/s_utils.py | 26 | ||||
-rw-r--r-- | src/saml2/sigver.py | 21 | ||||
-rw-r--r-- | tests/test_30_mdstore.py | 49 | ||||
-rw-r--r-- | tests/test_30_mdstore_old.py | 48 | ||||
-rw-r--r-- | tests/test_51_client.py | 6 | ||||
-rw-r--r-- | tests/test_88_nsprefix.py | 45 | ||||
-rwxr-xr-x | tools/make_metadata.py | 2 |
18 files changed, 313 insertions, 197 deletions
diff --git a/example/idp2/htdocs/login.mako b/example/idp2/htdocs/login.mako index 6f236732..7555deb7 100644 --- a/example/idp2/htdocs/login.mako +++ b/example/idp2/htdocs/login.mako @@ -14,7 +14,7 @@ <label for="login">Username</label> </div> <div> - <input type="text" name="login" value="${login}"/><br/> + <input type="text" name="login" value="${login}" autofocus><br/> </div> <div class="label"> diff --git a/example/idp2/idp.py b/example/idp2/idp.py index 48c88895..08c0e0c0 100755 --- a/example/idp2/idp.py +++ b/example/idp2/idp.py @@ -20,6 +20,7 @@ from saml2 import BINDING_HTTP_REDIRECT from saml2 import BINDING_HTTP_POST from saml2 import server from saml2 import time_util +from saml2.authn import is_equal from saml2.authn_context import AuthnBroker from saml2.authn_context import PASSWORD @@ -131,20 +132,20 @@ class Service(object): else: # saml_msg may also contain Signature and SigAlg if "Signature" in saml_msg: - args = {"signature": saml_msg["signature"], + kwargs = {"signature": saml_msg["signature"], "sigalg": saml_msg["SigAlg"]} else: - args = {} + kwargs = {} try: _encrypt_cert = encrypt_cert_from_item( saml_msg["req_info"].message) return self.do(saml_msg["SAMLRequest"], binding, saml_msg["RelayState"], - encrypt_cert=_encrypt_cert, **args) + encrypt_cert=_encrypt_cert, **kwargs) except KeyError: - # Can live with no relay state # TODO or can we, for inacademia? + # Can live with no relay state return self.do(saml_msg["SAMLRequest"], binding, - saml_msg["RelayState"], **args) + saml_msg["RelayState"], **kwargs) def artifact_operation(self, saml_msg): if not saml_msg: @@ -210,10 +211,13 @@ class Service(object): def not_authn(self, key, requested_authn_context): ruri = geturl(self.environ, query=False) - return do_authentication(self.environ, self.start_response, - authn_context=requested_authn_context, - key=key, redirect_uri=ruri) + kwargs = dict(authn_context=requested_authn_context, key=key, redirect_uri=ruri) + # Clear cookie, if it already exists + kaka = delete_cookie(self.environ, "idpauthn") + if kaka: + kwargs["headers"] = [kaka] + return do_authentication(self.environ, self.start_response, **kwargs) # ----------------------------------------------------------------------------- @@ -421,7 +425,8 @@ class SSO(Service): saml_msg["SAMLRequest"], BINDING_HTTP_POST) _req = self.req_info.message if self.user: - if _req.force_authn: + if _req.force_authn is not None and \ + _req.force_authn.lower() == 'true': saml_msg["req_info"] = self.req_info key = self._store_request(saml_msg) return self.not_authn(key, _req.requested_authn_context) @@ -449,18 +454,21 @@ class SSO(Service): try: authz_info = self.environ["HTTP_AUTHORIZATION"] if authz_info.startswith("Basic "): - _info = base64.b64decode(authz_info[6:]) - logger.debug("Authz_info: %s" % _info) try: - (user, passwd) = _info.split(":") - if PASSWD[user] != passwd: - resp = Unauthorized() - self.user = user - self.environ[ - "idp.authn"] = AUTHN_BROKER.get_authn_by_accr( - PASSWORD) - except ValueError: + _info = base64.b64decode(authz_info[6:]) + except TypeError: resp = Unauthorized() + else: + try: + (user, passwd) = _info.split(":") + if is_equal(PASSWD[user], passwd): + resp = Unauthorized() + self.user = user + self.environ[ + "idp.authn"] = AUTHN_BROKER.get_authn_by_accr( + PASSWORD) + except ValueError: + resp = Unauthorized() else: resp = Unauthorized() except KeyError: @@ -482,7 +490,7 @@ class SSO(Service): def do_authentication(environ, start_response, authn_context, key, - redirect_uri): + redirect_uri, headers=None): """ Display the login form """ @@ -492,7 +500,7 @@ def do_authentication(environ, start_response, authn_context, key, if len(auth_info): method, reference = auth_info[0] logger.debug("Authn chosen: %s (ref=%s)" % (method, reference)) - return method(environ, start_response, reference, key, redirect_uri) + return method(environ, start_response, reference, key, redirect_uri, headers) else: resp = Unauthorized("No usable authentication method") return resp(environ, start_response) @@ -509,15 +517,17 @@ PASSWD = { def username_password_authn(environ, start_response, reference, key, - redirect_uri): + redirect_uri, headers=None): """ Display the login form """ logger.info("The login page") - headers = [] - resp = Response(mako_template="login.mako", template_lookup=LOOKUP, - headers=headers) + kwargs = dict(mako_template="login.mako", template_lookup=LOOKUP) + if headers: + kwargs["headers"] = headers + + resp = Response(**kwargs) argv = { "action": "/verify", @@ -831,7 +841,7 @@ def info_from_cookie(kaka): try: key, ref = base64.b64decode(morsel.value).split(":") return IDP.cache.uid2user[key], ref - except KeyError: + except (KeyError, TypeError): return None, None else: logger.debug("No idpauthn cookie") @@ -927,12 +937,16 @@ def metadata(environ, start_response): def staticfile(environ, start_response): try: - path = args.path + path = args.path[:] if path is None or len(path) == 0: path = os.path.dirname(os.path.abspath(__file__)) if path[-1] != "/": path += "/" path += environ.get('PATH_INFO', '').lstrip('/') + path = os.path.realpath(path) + if not path.startswith(args.path): + resp = Unauthorized() + return resp(environ, start_response) start_response('200 OK', [('Content-Type', "text/xml")]) return open(path, 'r').read() except Exception as ex: diff --git a/example/idp2/idp_uwsgi.py b/example/idp2/idp_uwsgi.py index 01d338b6..ca8d105d 100755 --- a/example/idp2/idp_uwsgi.py +++ b/example/idp2/idp_uwsgi.py @@ -10,6 +10,7 @@ from hashlib import sha1 from urlparse import parse_qs from Cookie import SimpleCookie import os +from saml2.authn import is_equal from saml2.profile import ecp from saml2 import server @@ -73,12 +74,14 @@ def get_eptid(idp, req_info, session): req_info.sender(), session["permanent_id"], session["authn_auth"]) + # ----------------------------------------------------------------------------- def dict2list_of_tuples(d): return [(k, v) for k, v in d.items()] + # ----------------------------------------------------------------------------- @@ -95,7 +98,7 @@ class Service(object): return dict([(k, v[0]) for k, v in parse_qs(_qs).items()]) else: return None - + def unpack_post(self): _dict = parse_qs(get_post(self.environ)) logger.debug("unpack_post:: %s" % _dict) @@ -103,14 +106,14 @@ class Service(object): return dict([(k, v[0]) for k, v in _dict.items()]) except Exception: return None - + def unpack_soap(self): try: query = get_post(self.environ) return {"SAMLRequest": query, "RelayState": ""} except Exception: return None - + def unpack_either(self): if self.environ["REQUEST_METHOD"] == "GET": _dict = self.unpack_redirect() @@ -292,7 +295,7 @@ class SSO(Service): if not _resp: identity = USERS[self.user].copy() - #identity["eduPersonTargetedID"] = get_eptid(IDP, query, session) + # identity["eduPersonTargetedID"] = get_eptid(IDP, query, session) logger.info("Identity: %s" % (identity,)) if REPOZE_ID_EQUIVALENT: @@ -357,7 +360,8 @@ class SSO(Service): _req = self.req_info.message - if "SigAlg" in saml_msg and "Signature" in saml_msg: # Signed request + if "SigAlg" in saml_msg and "Signature" in saml_msg: # Signed + # request issuer = _req.issuer.text _certs = IDP.metadata.certs(issuer, "any", "signing") verified_ok = False @@ -405,7 +409,7 @@ class SSO(Service): return self.not_authn(key, _req.requested_authn_context) # def artifact(self): - # # Can be either by HTTP_Redirect or HTTP_POST + # # Can be either by HTTP_Redirect or HTTP_POST # _req = self._store_request(self.unpack_either()) # if isinstance(_req, basestring): # return self.not_authn(_req) @@ -419,18 +423,21 @@ class SSO(Service): try: authz_info = self.environ["HTTP_AUTHORIZATION"] if authz_info.startswith("Basic "): - _info = base64.b64decode(authz_info[6:]) - logger.debug("Authz_info: %s" % _info) try: - (user, passwd) = _info.split(":") - if PASSWD[user] != passwd: - resp = Unauthorized() - self.user = user - self.environ[ - "idp.authn"] = AUTHN_BROKER.get_authn_by_accr( - PASSWORD) - except ValueError: + _info = base64.b64decode(authz_info[6:]) + except TypeError: resp = Unauthorized() + else: + try: + (user, passwd) = _info.split(":") + if is_equal(PASSWD[user], passwd): + resp = Unauthorized() + self.user = user + self.environ[ + "idp.authn"] = AUTHN_BROKER.get_authn_by_accr( + PASSWORD) + except ValueError: + resp = Unauthorized() else: resp = Unauthorized() except KeyError: @@ -445,6 +452,7 @@ class SSO(Service): self.op_type = "ecp" return self.operation(_dict, BINDING_SOAP) + # ----------------------------------------------------------------------------- # === Authentication ==== # ----------------------------------------------------------------------------- @@ -470,11 +478,11 @@ def do_authentication(environ, start_response, authn_context, key, # ----------------------------------------------------------------------------- PASSWD = { - "daev0001": "qwerty", - "haho0032": "qwerty", - "roland": "dianakra", - "babs": "howes", - "upper": "crust"} + "daev0001": "qwerty", + "haho0032": "qwerty", + "roland": "dianakra", + "babs": "howes", + "upper": "crust"} def username_password_authn(environ, start_response, reference, key, @@ -548,7 +556,7 @@ def not_found(environ, start_response): # === Single log out === # ----------------------------------------------------------------------------- -#def _subject_sp_info(req_info): +# def _subject_sp_info(req_info): # # look for the subject # subject = req_info.subject_id() # subject = subject.text.strip() @@ -566,7 +574,7 @@ class SLO(Service): logger.error("Bad request: %s" % exc) resp = BadRequest("%s" % exc) return resp(self.environ, self.start_response) - + msg = req_info.message if msg.name_id: lid = IDP.ident.find_local_id(msg.name_id) @@ -583,16 +591,16 @@ class SLO(Service): logger.error("ServiceError: %s" % exc) resp = ServiceError("%s" % exc) return resp(self.environ, self.start_response) - + resp = IDP.create_logout_response(msg, [binding]) - + try: hinfo = IDP.apply_binding(binding, "%s" % resp, "", relay_state) except Exception as exc: logger.error("ServiceError: %s" % exc) resp = ServiceError("%s" % exc) return resp(self.environ, self.start_response) - + #_tlh = dict2list_of_tuples(hinfo["headers"]) delco = delete_cookie(self.environ, "idpauthn") if delco: @@ -600,35 +608,36 @@ class SLO(Service): logger.info("Header: %s" % (hinfo["headers"],)) resp = Response(hinfo["data"], headers=hinfo["headers"]) return resp(self.environ, self.start_response) - + + # ---------------------------------------------------------------------------- # Manage Name ID service # ---------------------------------------------------------------------------- class NMI(Service): - def do(self, query, binding, relay_state="", encrypt_cert=None): logger.info("--- Manage Name ID Service ---") req = IDP.parse_manage_name_id_request(query, binding) request = req.message - + # Do the necessary stuff name_id = IDP.ident.handle_manage_name_id_request( request.name_id, request.new_id, request.new_encrypted_id, request.terminate) - + logger.debug("New NameID: %s" % name_id) - + _resp = IDP.create_manage_name_id_response(request) - + # It's using SOAP binding hinfo = IDP.apply_binding(BINDING_SOAP, "%s" % _resp, "", relay_state, response=True) - + resp = Response(hinfo["data"], headers=hinfo["headers"]) return resp(self.environ, self.start_response) - + + # ---------------------------------------------------------------------------- # === Assertion ID request === # ---------------------------------------------------------------------------- @@ -644,9 +653,9 @@ class AIDR(Service): except Unknown: resp = NotFound(aid) return resp(self.environ, self.start_response) - + hinfo = IDP.apply_binding(BINDING_URI, "%s" % assertion, response=True) - + logger.debug("HINFO: %s" % hinfo) resp = Response(hinfo["data"], headers=hinfo["headers"]) return resp(self.environ, self.start_response) @@ -676,6 +685,7 @@ class ARS(Service): resp = Response(hinfo["data"], headers=hinfo["headers"]) return resp(self.environ, self.start_response) + # ---------------------------------------------------------------------------- # === Authn query service === # ---------------------------------------------------------------------------- @@ -730,6 +740,7 @@ class ATTR(Service): resp = Response(hinfo["data"], headers=hinfo["headers"]) return resp(self.environ, self.start_response) + # ---------------------------------------------------------------------------- # Name ID Mapping service # When an entity that shares an identifier for a principal with an identity @@ -753,17 +764,17 @@ class NIM(Service): except PolicyError: resp = BadRequest("Unknown entity") return resp(self.environ, self.start_response) - + info = IDP.response_args(request) _resp = IDP.create_name_id_mapping_response(name_id, **info) - + # Only SOAP hinfo = IDP.apply_binding(BINDING_SOAP, "%s" % _resp, "", "", response=True) - + resp = Response(hinfo["data"], headers=hinfo["headers"]) return resp(self.environ, self.start_response) - + # ---------------------------------------------------------------------------- # Cookie handling @@ -777,7 +788,7 @@ def info_from_cookie(kaka): try: key, ref = base64.b64decode(morsel.value).split(":") return IDP.cache.uid2user[key], ref - except KeyError: + except (TypeError, KeyError): return None, None else: logger.debug("No idpauthn cookie") @@ -858,10 +869,10 @@ def metadata(environ, start_response): try: path = args.path if path is None or len(path) == 0: - path = os.path.dirname(os.path.abspath( __file__ )) + path = os.path.dirname(os.path.abspath(__file__)) if path[-1] != "/": path += "/" - metadata = create_metadata_string(path+args.config, IDP.config, + metadata = create_metadata_string(path + args.config, IDP.config, args.valid, args.cert, args.keyfile, args.id, args.name, args.sign) start_response('200 OK', [('Content-Type', "text/xml")]) @@ -870,6 +881,7 @@ def metadata(environ, start_response): logger.error("An error occured while creating metadata:" + ex.message) return not_found(environ, start_response) + def staticfile(environ, start_response): try: path = args.path @@ -878,12 +890,17 @@ def staticfile(environ, start_response): if path[-1] != "/": path += "/" path += environ.get('PATH_INFO', '').lstrip('/') + path = os.path.realpath(path) + if not path.startswith(args.path): + resp = Unauthorized() + return resp(environ, start_response) start_response('200 OK', [('Content-Type', "text/xml")]) return open(path, 'r').read() except Exception as ex: logger.error("An error occured while creating metadata:" + ex.message) return not_found(environ, start_response) + def application(environ, start_response): """ The main WSGI application. Dispatch the current request to @@ -920,7 +937,6 @@ def application(environ, start_response): except KeyError: user = None - url_patterns = AUTHN_URLS if not user: logger.info("-- No USER --") @@ -952,7 +968,7 @@ def application(environ, start_response): # by moving some initialization out of __name__ == '__main__' section. # uwsgi -s 0.0.0.0:8088 --protocol http --callable application --module idp -args = type('Config', (object,), { }) +args = type('Config', (object,), {}) args.config = 'idp_conf' args.mako_root = './' args.path = None @@ -980,7 +996,8 @@ if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('-p', dest='path', help='Path to configuration file.') parser.add_argument('-v', dest='valid', - help="How long, in days, the metadata is valid from the time of creation") + help="How long, in days, the metadata is valid from " + "the time of creation") parser.add_argument('-c', dest='cert', help='certificate') parser.add_argument('-i', dest='id', help="The ID of the entities descriptor") diff --git a/example/idp2_repoze/idp.py b/example/idp2_repoze/idp.py index 4729392b..685fc0ba 100755 --- a/example/idp2_repoze/idp.py +++ b/example/idp2_repoze/idp.py @@ -19,6 +19,7 @@ from saml2 import BINDING_SOAP from saml2 import BINDING_HTTP_REDIRECT from saml2 import BINDING_HTTP_POST from saml2 import time_util +from saml2.authn import is_equal from saml2.authn_context import AuthnBroker from saml2.authn_context import PASSWORD @@ -406,15 +407,19 @@ class SSO(Service): try: authz_info = self.environ["HTTP_AUTHORIZATION"] if authz_info.startswith("Basic "): - _info = base64.b64decode(authz_info[6:]) - logger.debug("Authz_info: %s" % _info) try: - (user, passwd) = _info.split(":") - if PASSWD[user] != passwd: - resp = Unauthorized() - self.user = user - except ValueError: + _info = base64.b64decode(authz_info[6:]) + except TypeError: resp = Unauthorized() + else: + logger.debug("Authz_info: %s" % _info) + try: + (user, passwd) = _info.split(":") + if is_equal(PASSWD[user], passwd): + resp = Unauthorized() + self.user = user + except (ValueError, TypeError): + resp = Unauthorized() else: resp = Unauthorized() except KeyError: @@ -758,7 +763,7 @@ def info_from_cookie(kaka): try: key, ref = base64.b64decode(morsel.value).split(":") return IDP.cache.uid2user[key], ref - except KeyError: + except (KeyError, TypeError): return None, None else: logger.debug("No idpauthn cookie") @@ -51,8 +51,8 @@ if sys.version_info < (2, 7): setup( name='pysaml2', - version='2.2.1beta', - description='Python implementation of SAML Version 2 to be used in a WSGI environment', + version='2.4.0beta', + description='Python implementation of SAML Version 2', # long_description = read("README"), author='Roland Hedberg', author_email='roland.hedberg@adm.umu.se', diff --git a/src/saml2/__init__.py b/src/saml2/__init__.py index 7a73aba2..db055476 100644 --- a/src/saml2/__init__.py +++ b/src/saml2/__init__.py @@ -541,6 +541,23 @@ class SamlBase(ExtensionContainer): self._add_members_to_element_tree(new_tree) return new_tree + def register_prefix(self, nspair): + """ + Register with ElementTree a set of namespaces + + :param nspair: A dictionary of prefixes and uris to use when + constructing the text representation. + :return: + """ + for prefix, uri in nspair.items(): + try: + ElementTree.register_namespace(prefix, uri) + except AttributeError: + # Backwards compatibility with ET < 1.3 + ElementTree._namespace_map[uri] = prefix + except ValueError: + pass + def to_string(self, nspair=None): """Converts the Saml object to a string containing XML. @@ -552,14 +569,7 @@ class SamlBase(ExtensionContainer): nspair = self.c_ns_prefix if nspair: - for prefix, uri in nspair.items(): - try: - ElementTree.register_namespace(prefix, uri) - except AttributeError: - # Backwards compatibility with ET < 1.3 - ElementTree._namespace_map[uri] = prefix - except ValueError: - pass + self.register_prefix(nspair) return ElementTree.tostring(self._to_element_tree(), encoding="UTF-8") diff --git a/src/saml2/authn.py b/src/saml2/authn.py index 8c6e2183..804feee6 100644 --- a/src/saml2/authn.py +++ b/src/saml2/authn.py @@ -39,6 +39,16 @@ class UserAuthnMethod(object): raise NotImplemented +def is_equal(a, b): + if len(a) != len(b): + return False + + result = 0 + for x, y in zip(a, b): + result |= x ^ y + return result == 0 + + def url_encode_params(params=None): if not isinstance(params, dict): raise EncodeError("You must pass in a dictionary!") @@ -137,7 +147,7 @@ class UsernamePasswordMako(UserAuthnMethod): return resp def _verify(self, pwd, user): - assert pwd == self.passwd[user] + assert is_equal(pwd, self.passwd[user]) def verify(self, request, **kwargs): """ @@ -149,7 +159,7 @@ class UsernamePasswordMako(UserAuthnMethod): wants the user after authentication. """ - logger.debug("verify(%s)" % request) + #logger.debug("verify(%s)" % request) if isinstance(request, basestring): _dict = parse_qs(request) elif isinstance(request, dict): @@ -157,8 +167,6 @@ class UsernamePasswordMako(UserAuthnMethod): else: raise ValueError("Wrong type of input") - logger.debug("dict: %s" % _dict) - logger.debug("passwd: %s" % self.passwd) # verify username and password try: self._verify(_dict["password"][0], _dict["login"][0]) diff --git a/src/saml2/client.py b/src/saml2/client.py index ca83bf9a..d64bd806 100644 --- a/src/saml2/client.py +++ b/src/saml2/client.py @@ -342,7 +342,7 @@ class Saml2Client(Base): attribute=None, sp_name_qualifier=None, name_qualifier=None, nameid_format=None, real_id=None, consent=None, extensions=None, - sign=False, binding=BINDING_SOAP): + sign=False, binding=BINDING_SOAP, nsprefix=None): """ Does a attribute request to an attribute authority, this is by default done over SOAP. @@ -359,6 +359,8 @@ class Saml2Client(Base): :param real_id: The identifier which is the key to this entity in the identity database :param binding: Which binding to use + :param nsprefix: Namespace prefixes preferred before those automatically + produced. :return: The attributes returned if BINDING_SOAP was used. HTTP args if BINDING_HTT_POST was used. """ @@ -393,7 +395,7 @@ class Saml2Client(Base): mid = sid() query = self.create_attribute_query(destination, subject_id, attribute, mid, consent, - extensions, sign) + extensions, sign, nsprefix) self.state[query.id] = {"entity_id": entityid, "operation": "AttributeQuery", "subject_id": subject_id, diff --git a/src/saml2/client_base.py b/src/saml2/client_base.py index 6fc1effc..a0e5e109 100644 --- a/src/saml2/client_base.py +++ b/src/saml2/client_base.py @@ -306,6 +306,11 @@ class Base(Entity): pass args["name_id_policy"] = name_id_policy + try: + nsprefix = kwargs["nsprefix"] + except KeyError: + nsprefix = None + if kwargs: _args, extensions = self._filter_args(AuthnRequest(), extensions, **kwargs) @@ -328,11 +333,11 @@ class Base(Entity): return self._message(AuthnRequest, destination, message_id, consent, extensions, sign, sign_prepare, protocol_binding=binding, - scoping=scoping, **args) + scoping=scoping, nsprefix=nsprefix, **args) return self._message(AuthnRequest, destination, message_id, consent, extensions, sign, sign_prepare, protocol_binding=binding, - scoping=scoping, **args) + scoping=scoping, nsprefix=nsprefix, **args) def create_attribute_query(self, destination, name_id=None, attribute=None, message_id=0, consent=None, @@ -386,9 +391,14 @@ class Base(Entity): if attribute: attribute = do_attributes(attribute) + try: + nsprefix = kwargs["nsprefix"] + except KeyError: + nsprefix = None + return self._message(AttributeQuery, destination, message_id, consent, extensions, sign, sign_prepare, subject=subject, - attribute=attribute) + attribute=attribute, nsprefix=nsprefix) # MUST use SOAP for # AssertionIDRequest, SubjectQuery, @@ -422,7 +432,7 @@ class Base(Entity): subject=None, message_id=0, consent=None, extensions=None, - sign=False): + sign=False, nsprefix=None): """ Makes an authz decision query based on a previously received Assertion. @@ -449,7 +459,7 @@ class Base(Entity): return self.create_authz_decision_query( destination, _action, saml.Evidence(assertion=assertion), resource, subject, message_id=message_id, consent=consent, - extensions=extensions, sign=sign) + extensions=extensions, sign=sign, nsprefix=nsprefix) @staticmethod def create_assertion_id_request(assertion_id_refs, **kwargs): @@ -466,7 +476,7 @@ class Base(Entity): def create_authn_query(self, subject, destination=None, authn_context=None, session_index="", message_id=0, consent=None, - extensions=None, sign=False): + extensions=None, sign=False, nsprefix=None): """ :param subject: The subject its all about as a <Subject> instance @@ -479,15 +489,18 @@ class Base(Entity): :param sign: Whether the request should be signed or not. :return: """ - return self._message(AuthnQuery, destination, message_id, consent, extensions, - sign, subject=subject, session_index=session_index, - requested_authn_context=authn_context) + return self._message(AuthnQuery, destination, message_id, consent, + extensions, sign, subject=subject, + session_index=session_index, + requested_authn_context=authn_context, + nsprefix=nsprefix) def create_name_id_mapping_request(self, name_id_policy, name_id=None, base_id=None, encrypted_id=None, destination=None, - message_id=0, consent=None, extensions=None, - sign=False): + message_id=0, consent=None, + extensions=None, sign=False, + nsprefix=None): """ :param name_id_policy: @@ -508,16 +521,18 @@ class Base(Entity): if name_id: return self._message(NameIDMappingRequest, destination, message_id, consent, extensions, sign, - name_id_policy=name_id_policy, name_id=name_id) + name_id_policy=name_id_policy, name_id=name_id, + nsprefix=nsprefix) elif base_id: return self._message(NameIDMappingRequest, destination, message_id, consent, extensions, sign, - name_id_policy=name_id_policy, base_id=base_id) + name_id_policy=name_id_policy, base_id=base_id, + nsprefix=nsprefix) else: return self._message(NameIDMappingRequest, destination, message_id, consent, extensions, sign, name_id_policy=name_id_policy, - encrypted_id=encrypted_id) + encrypted_id=encrypted_id, nsprefix=nsprefix) # ======== response handling =========== diff --git a/src/saml2/entity.py b/src/saml2/entity.py index 9781310c..be5977fb 100644 --- a/src/saml2/entity.py +++ b/src/saml2/entity.py @@ -151,7 +151,6 @@ class Entity(HTTPBase): self.metadata = self.config.metadata self.config.setup_logger() self.debug = self.config.debug - self.seed = rndstr(32) self.sec = security_context(self.config) @@ -285,7 +284,7 @@ class Entity(HTTPBase): def message_args(self, message_id=0): if not message_id: - message_id = sid(self.seed) + message_id = sid() return {"id": message_id, "version": VERSION, "issue_instant": instant(), "issuer": self._issuer()} @@ -421,7 +420,7 @@ class Entity(HTTPBase): def _message(self, request_cls, destination=None, message_id=0, consent=None, extensions=None, sign=False, sign_prepare=False, - **kwargs): + nsprefix=None, **kwargs): """ Some parameters appear in all requests so simplify by doing it in one place @@ -438,7 +437,7 @@ class Entity(HTTPBase): request_cls """ if not message_id: - message_id = sid(self.seed) + message_id = sid() for key, val in self.message_args(message_id).items(): if key not in kwargs: @@ -456,6 +455,9 @@ class Entity(HTTPBase): if extensions: req.extensions = extensions + if nsprefix: + req.register_prefix(nsprefix) + if sign: return reqid, self.sign(req, sign_prepare=sign_prepare) else: diff --git a/src/saml2/response.py b/src/saml2/response.py index c40997a3..8c6332c8 100644 --- a/src/saml2/response.py +++ b/src/saml2/response.py @@ -850,9 +850,13 @@ class AuthnResponse(StatusResponse): """ try: - self._verify() - except AssertionError: + res = self._verify() + except AssertionError as err: + logger.error("Verification error on the response: %s" % err) raise + else: + if res is None: + return None if not isinstance(self.response, samlp.Response): return self diff --git a/src/saml2/s_utils.py b/src/saml2/s_utils.py index e17c2b56..47f47c98 100644 --- a/src/saml2/s_utils.py +++ b/src/saml2/s_utils.py @@ -7,6 +7,7 @@ import time import base64 import sys import hmac +import string # from python 2.5 import imp @@ -154,31 +155,28 @@ def deflate_and_base64_encode(string_val): return base64.b64encode(zlib.compress(string_val)[2:-4]) -def rndstr(size=16): +def rndstr(size=16, alphabet=""): """ Returns a string of random ascii characters or digits :param size: The length of the string :return: string """ - _basech = string.ascii_letters + string.digits - return "".join([random.choice(_basech) for _ in range(size)]) + rng = random.SystemRandom() + if not alphabet: + alphabet = string.letters[0:52] + string.digits + return str().join(rng.choice(alphabet) for _ in range(size)) -def sid(seed=""): - """The hash of the server time + seed makes an unique SID for each session. - 128-bits long so it fulfills the SAML2 requirements which states +def sid(): + """creates an unique SID for each session. + 160-bits long so it fulfills the SAML2 requirements which states 128-160 bits - :param seed: A seed string - :return: The hex version of the digest, prefixed by 'id-' to make it + :return: A random string prefix with 'id-' to make it compliant with the NCName specification """ - ident = md5() - ident.update(repr(time.time())) - if seed: - ident.update(seed) - return "id-" + ident.hexdigest() + return "id-" + rndstr(17) def parse_attribute_map(filenames): @@ -469,7 +467,7 @@ def rec_factory(cls, **kwargs): except Exception: continue else: - setattr(_inst, key, val) + setattr(_inst, _inst.c_attributes[key][0], val) elif key in _inst.c_child_order: for tag, _cls in _inst.c_children.values(): if tag == key: diff --git a/src/saml2/sigver.py b/src/saml2/sigver.py index e598781b..0f2d1fbb 100644 --- a/src/saml2/sigver.py +++ b/src/saml2/sigver.py @@ -33,7 +33,7 @@ from saml2 import saml from saml2 import ExtensionElement from saml2 import VERSION -from saml2.s_utils import sid +from saml2.s_utils import sid, rndstr from saml2.s_utils import Unsupported from saml2.time_util import instant @@ -322,18 +322,13 @@ def signed_instance_factory(instance, seccont, elements_to_sign=None): # -------------------------------------------------------------------------- - - -def create_id(): - """ Create a string of 40 random characters from the set [a-p], - can be used as a unique identifier of objects. - - :return: The string of random characters - """ - ret = "" - for _ in range(40): - ret += chr(random.randint(0, 15) + ord('a')) - return ret +# def create_id(): +# """ Create a string of 40 random characters from the set [a-p], +# can be used as a unique identifier of objects. +# +# :return: The string of random characters +# """ +# return rndstr(40, "abcdefghijklmonp") def make_temp(string, suffix="", decode=True, delete=True): diff --git a/tests/test_30_mdstore.py b/tests/test_30_mdstore.py index 8e02f288..21819a59 100644 --- a/tests/test_30_mdstore.py +++ b/tests/test_30_mdstore.py @@ -240,30 +240,31 @@ def test_metadata_file(): assert len(mds.keys()) == 560 -def test_mdx_service(): - sec_config.xmlsec_binary = sigver.get_xmlsec_binary(["/opt/local/bin"]) - http = HTTPBase(verify=False, ca_bundle=None) - - mdx = MetaDataMDX(quote_plus, ONTS.values(), ATTRCONV, - "http://pyff-test.nordu.net", - sec_config, None, http) - foo = mdx.service("https://idp.umu.se/saml2/idp/metadata.php", - "idpsso_descriptor", "single_sign_on_service") - - assert len(foo) == 1 - assert foo.keys()[0] == BINDING_HTTP_REDIRECT - - -def test_mdx_certs(): - sec_config.xmlsec_binary = sigver.get_xmlsec_binary(["/opt/local/bin"]) - http = HTTPBase(verify=False, ca_bundle=None) - - mdx = MetaDataMDX(quote_plus, ONTS.values(), ATTRCONV, - "http://pyff-test.nordu.net", - sec_config, None, http) - foo = mdx.certs("https://idp.umu.se/saml2/idp/metadata.php", "idpsso") - - assert len(foo) == 1 +# pyff-test not available +# def test_mdx_service(): +# sec_config.xmlsec_binary = sigver.get_xmlsec_binary(["/opt/local/bin"]) +# http = HTTPBase(verify=False, ca_bundle=None) +# +# mdx = MetaDataMDX(quote_plus, ONTS.values(), ATTRCONV, +# "http://pyff-test.nordu.net", +# sec_config, None, http) +# foo = mdx.service("https://idp.umu.se/saml2/idp/metadata.php", +# "idpsso_descriptor", "single_sign_on_service") +# +# assert len(foo) == 1 +# assert foo.keys()[0] == BINDING_HTTP_REDIRECT +# +# +# def test_mdx_certs(): +# sec_config.xmlsec_binary = sigver.get_xmlsec_binary(["/opt/local/bin"]) +# http = HTTPBase(verify=False, ca_bundle=None) +# +# mdx = MetaDataMDX(quote_plus, ONTS.values(), ATTRCONV, +# "http://pyff-test.nordu.net", +# sec_config, None, http) +# foo = mdx.certs("https://idp.umu.se/saml2/idp/metadata.php", "idpsso") +# +# assert len(foo) == 1 def test_load_local_dir(): diff --git a/tests/test_30_mdstore_old.py b/tests/test_30_mdstore_old.py index 0f3d3d04..b847e115 100644 --- a/tests/test_30_mdstore_old.py +++ b/tests/test_30_mdstore_old.py @@ -230,30 +230,30 @@ def test_metadata_file(): assert len(mds.keys()) == 560 -def test_mdx_service(): - sec_config.xmlsec_binary = sigver.get_xmlsec_binary(["/opt/local/bin"]) - http = HTTPBase(verify=False, ca_bundle=None) - - mdx = MetaDataMDX(quote_plus, ONTS.values(), ATTRCONV, - "http://pyff-test.nordu.net", - sec_config, None, http) - foo = mdx.service("https://idp.umu.se/saml2/idp/metadata.php", - "idpsso_descriptor", "single_sign_on_service") - - assert len(foo) == 1 - assert foo.keys()[0] == BINDING_HTTP_REDIRECT - - -def test_mdx_certs(): - sec_config.xmlsec_binary = sigver.get_xmlsec_binary(["/opt/local/bin"]) - http = HTTPBase(verify=False, ca_bundle=None) - - mdx = MetaDataMDX(quote_plus, ONTS.values(), ATTRCONV, - "http://pyff-test.nordu.net", - sec_config, None, http) - foo = mdx.certs("https://idp.umu.se/saml2/idp/metadata.php", "idpsso") - - assert len(foo) == 1 +# def test_mdx_service(): +# sec_config.xmlsec_binary = sigver.get_xmlsec_binary(["/opt/local/bin"]) +# http = HTTPBase(verify=False, ca_bundle=None) +# +# mdx = MetaDataMDX(quote_plus, ONTS.values(), ATTRCONV, +# "http://pyff-test.nordu.net", +# sec_config, None, http) +# foo = mdx.service("https://idp.umu.se/saml2/idp/metadata.php", +# "idpsso_descriptor", "single_sign_on_service") +# +# assert len(foo) == 1 +# assert foo.keys()[0] == BINDING_HTTP_REDIRECT +# +# +# def test_mdx_certs(): +# sec_config.xmlsec_binary = sigver.get_xmlsec_binary(["/opt/local/bin"]) +# http = HTTPBase(verify=False, ca_bundle=None) +# +# mdx = MetaDataMDX(quote_plus, ONTS.values(), ATTRCONV, +# "http://pyff-test.nordu.net", +# sec_config, None, http) +# foo = mdx.certs("https://idp.umu.se/saml2/idp/metadata.php", "idpsso") +# +# assert len(foo) == 1 def test_load_local_dir(): diff --git a/tests/test_51_client.py b/tests/test_51_client.py index 5e4c0b23..f9089d35 100644 --- a/tests/test_51_client.py +++ b/tests/test_51_client.py @@ -473,7 +473,7 @@ class TestClient: response = sigver.response_factory( in_response_to="_012345", - destination="https://www.example.com", + destination="http://lingon.catalogix.se:8087/", status=s_utils.success_status_factory(), issuer=self.server._issuer(), encrypted_assertion=EncryptedAssertion() @@ -616,7 +616,7 @@ class TestClientWithDummy(): {sid: "/"}) ac = resp.assertion.authn_statement[0].authn_context assert ac.authenticating_authority[0].text == \ - 'http://www.example.com/login' + 'http://www.example.com/login' assert ac.authn_context_class_ref.text == INTERNETPROTOCOLPASSWORD @@ -628,4 +628,4 @@ class TestClientWithDummy(): if __name__ == "__main__": tc = TestClient() tc.setup_class() - tc.test_signed_redirect() + tc.test_sign_then_encrypt_assertion2()
\ No newline at end of file diff --git a/tests/test_88_nsprefix.py b/tests/test_88_nsprefix.py new file mode 100644 index 00000000..4f652a54 --- /dev/null +++ b/tests/test_88_nsprefix.py @@ -0,0 +1,45 @@ +from saml2.saml import NAMEID_FORMAT_TRANSIENT +from saml2.client import Saml2Client +from saml2 import config, BINDING_HTTP_POST +from saml2 import saml +from saml2 import samlp + +__author__ = 'roland' + + +def test_nsprefix(): + status_message = samlp.StatusMessage() + status_message.text = "OK" + + txt = "%s" % status_message + + assert "ns0:StatusMessage" in txt + + status_message.register_prefix({"saml2": saml.NAMESPACE, + "saml2p": samlp.NAMESPACE}) + + txt = "%s" % status_message + + assert "saml2p:StatusMessage" in txt + + +def test_nsprefix2(): + conf = config.SPConfig() + conf.load_file("servera_conf") + client = Saml2Client(conf) + + selected_idp = "urn:mace:example.com:saml:roland:idp" + + destination = client._sso_location(selected_idp, BINDING_HTTP_POST) + + reqid, req = client.create_authn_request( + destination, nameid_format=NAMEID_FORMAT_TRANSIENT, + nsprefix={"saml2": saml.NAMESPACE, "saml2p": samlp.NAMESPACE}) + + txt = "%s" % req + + assert "saml2p:AuthnRequest" in txt + assert "saml2:Issuer" in txt + +if __name__ == "__main__": + test_nsprefix2()
\ No newline at end of file diff --git a/tools/make_metadata.py b/tools/make_metadata.py index eff71d2d..d9aea502 100755 --- a/tools/make_metadata.py +++ b/tools/make_metadata.py @@ -66,7 +66,7 @@ conf.xmlsec_binary = args.xmlsec secc = security_context(conf) if args.id: - desc = entities_descriptor(eds, valid_for, args.name, args.id, + desc, xmldoc = entities_descriptor(eds, valid_for, args.name, args.id, args.sign, secc) valid_instance(desc) print desc.to_string(nspair) |