summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--example/idp2/htdocs/login.mako2
-rwxr-xr-xexample/idp2/idp.py68
-rwxr-xr-xexample/idp2/idp_uwsgi.py109
-rwxr-xr-xexample/idp2_repoze/idp.py21
-rwxr-xr-xsetup.py4
-rw-r--r--src/saml2/__init__.py26
-rw-r--r--src/saml2/authn.py16
-rw-r--r--src/saml2/client.py6
-rw-r--r--src/saml2/client_base.py43
-rw-r--r--src/saml2/entity.py10
-rw-r--r--src/saml2/response.py8
-rw-r--r--src/saml2/s_utils.py26
-rw-r--r--src/saml2/sigver.py21
-rw-r--r--tests/test_30_mdstore.py49
-rw-r--r--tests/test_30_mdstore_old.py48
-rw-r--r--tests/test_51_client.py6
-rw-r--r--tests/test_88_nsprefix.py45
-rwxr-xr-xtools/make_metadata.py2
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")
diff --git a/setup.py b/setup.py
index 3c952e6d..4e1aa41a 100755
--- a/setup.py
+++ b/setup.py
@@ -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)