Skip to content

Commit 7888526

Browse files
author
Roland Hedberg
committed
Merge branch 'master' of github.com:rohe/pysaml2
2 parents 9e89aaf + 357d073 commit 7888526

File tree

10 files changed

+166
-26
lines changed

10 files changed

+166
-26
lines changed

doc/conf.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# serve to show the default.
1313

1414
import sys, os
15+
import alabaster
1516

1617
# If extensions (or modules to document with autodoc) are in another directory,
1718
# add these directories to sys.path here. If the directory is relative to the
@@ -22,6 +23,7 @@
2223

2324
# Add any Sphinx extension module names here, as strings. They can be extensions
2425
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
26+
2527
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.coverage']
2628

2729
# Add any paths that contain templates here, relative to this directory.
@@ -91,6 +93,7 @@
9193

9294
# The theme to use for HTML and HTML Help pages. Major themes that come with
9395
# Sphinx are currently 'default' and 'sphinxdoc'.
96+
html_theme_path = [alabaster.get_path()]
9497
html_theme = 'alabaster'
9598
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
9699

example/idp2/idp.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,8 @@ def do(self, query, binding_in, relay_state="", encrypt_cert=None,
336336
_resp = IDP.create_authn_response(
337337
identity, userid=self.user,
338338
encrypt_cert=encrypt_cert,
339+
encrypt_assertion_self_contained=True,
340+
encrypted_advice_attributes=True,
339341
**resp_args)
340342
except Exception as excp:
341343
logging.error(exception_trace(excp))
@@ -400,9 +402,9 @@ def redirect(self):
400402
return resp(self.environ, self.start_response)
401403

402404
if self.user:
405+
saml_msg["req_info"] = self.req_info
403406
if _req.force_authn is not None and \
404407
_req.force_authn.lower() == 'true':
405-
saml_msg["req_info"] = self.req_info
406408
key = self._store_request(saml_msg)
407409
return self.not_authn(key, _req.requested_authn_context)
408410
else:
@@ -1014,6 +1016,7 @@ def application(environ, start_response):
10141016
if isinstance(callback, tuple):
10151017
cls = callback[0](environ, start_response, user)
10161018
func = getattr(cls, callback[1])
1019+
10171020
return func()
10181021
return callback(environ, start_response, user)
10191022

example/sp-wsgi/sp.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22
import logging
33
import re
44
import argparse
5+
from saml2.extension.pefim import SPCertEnc
56
import service_conf
67

78
from Cookie import SimpleCookie
89
from urlparse import parse_qs
910
import sys
1011

11-
from saml2 import BINDING_HTTP_REDIRECT
12+
from saml2 import BINDING_HTTP_REDIRECT, element_to_extension_element
1213
from saml2 import BINDING_SOAP
1314
from saml2 import time_util
1415
from saml2 import ecp
@@ -33,6 +34,8 @@
3334
from saml2.s_utils import sid
3435
from saml2.s_utils import rndstr
3536
#from srtest import exception_trace
37+
from saml2.md import Extensions
38+
import xmldsig as ds
3639

3740
logger = logging.getLogger("")
3841
hdlr = logging.FileHandler('spx.log')
@@ -152,6 +155,7 @@ def __init__(self):
152155
self.uid2user = {}
153156
self.cookie_name = "spauthn"
154157
self.outstanding_queries = {}
158+
self.outstanding_certs = {}
155159
self.relay_state = {}
156160
self.user = {}
157161
self.result = {}
@@ -348,7 +352,7 @@ def do(self, response, binding, relay_state="", mtype="response"):
348352

349353
try:
350354
self.response = self.sp.parse_authn_request_response(
351-
response, binding, self.outstanding_queries)
355+
response, binding, self.outstanding_queries, self.cache.outstanding_certs)
352356
except UnknownPrincipal, excp:
353357
logger.error("UnknownPrincipal: %s" % (excp,))
354358
resp = ServiceError("UnknownPrincipal: %s" % (excp,))
@@ -551,13 +555,31 @@ def redirect_to_auth(self, _cli, entity_id, came_from):
551555
"assertion_consumer_service"]
552556
# just pick one
553557
endp, return_binding = acs[0]
558+
559+
extensions = None
560+
cert = None
561+
if _cli.config.generate_cert_func is not None:
562+
cert_str, req_key_str = _cli.config.generate_cert_func()
563+
cert = {
564+
"cert": cert_str,
565+
"key": req_key_str
566+
}
567+
spcertenc = SPCertEnc(x509_data=ds.X509Data(
568+
x509_certificate=ds.X509Certificate(text=cert_str)))
569+
extensions = Extensions(extension_elements=[
570+
element_to_extension_element(spcertenc)])
571+
554572
req_id, req = _cli.create_authn_request(destination,
555-
binding=return_binding)
573+
binding=return_binding, extensions=extensions)
556574
_rstate = rndstr()
557575
self.cache.relay_state[_rstate] = came_from
558576
ht_args = _cli.apply_binding(_binding, "%s" % req, destination,
559577
relay_state=_rstate)
560578
_sid = req_id
579+
580+
if cert is not None:
581+
self.cache.outstanding_certs[_sid] = cert
582+
561583
except Exception, exc:
562584
logger.exception(exc)
563585
resp = ServiceError(

setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def run_tests(self):
2727
'paste',
2828
'zope.interface',
2929
'repoze.who',
30-
'pycrypto >= 2.2', # 'Crypto'
30+
'pycrypto >= 2.5', # 'Crypto'
3131
'pytz',
3232
'pyOpenSSL',
3333
'python-dateutil',
@@ -37,7 +37,7 @@ def run_tests(self):
3737
tests_require = [
3838
'mongodict',
3939
'pyasn1',
40-
'pymongo',
40+
'pymongo==3.0.1',
4141
'python-memcached == 1.51',
4242
'pytest',
4343
'mako',

src/saml2/aes.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ def encrypt(self, msg, iv=None, alg="aes_128_cbc", padding="PKCS#7",
8787
return cmsg
8888

8989

90-
def decrypt(self, msg, iv=None, padding="PKCS#7", b64dec=True):
90+
def decrypt(self, msg, iv=None, alg="aes_128_cbc", padding="PKCS#7", b64dec=True):
9191
"""
9292
:param key: The encryption key
9393
:param iv: init vector
@@ -102,7 +102,7 @@ def decrypt(self, msg, iv=None, padding="PKCS#7", b64dec=True):
102102
_iv = data[:AES.block_size]
103103
if iv:
104104
assert iv == _iv
105-
cipher, iv = self.build_cipher(iv)
105+
cipher, iv = self.build_cipher(iv, alg=alg)
106106
res = cipher.decrypt(data)[AES.block_size:]
107107
if padding in ["PKCS#5", "PKCS#7"]:
108108
res = res[:-ord(res[-1])]

src/saml2/client.py

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,20 +64,72 @@ def prepare_for_authenticate(self, entityid=None, relay_state="",
6464
:return: session id and AuthnRequest info
6565
"""
6666

67-
destination = self._sso_location(entityid, binding)
67+
reqid, negotiated_binding, info = self.prepare_for_negotiated_authenticate(
68+
entityid=entityid,
69+
relay_state=relay_state,
70+
binding=binding,
71+
vorg=vorg,
72+
nameid_format=nameid_format,
73+
scoping=scoping,
74+
consent=consent,
75+
extensions=extensions,
76+
sign=sign,
77+
response_binding=response_binding,
78+
**kwargs)
79+
80+
assert negotiated_binding == binding
6881

69-
reqid, req = self.create_authn_request(destination, vorg, scoping,
70-
response_binding, nameid_format,
71-
consent=consent,
72-
extensions=extensions, sign=sign,
73-
**kwargs)
74-
_req_str = "%s" % req
82+
return reqid, info
7583

76-
logger.info("AuthNReq: %s" % _req_str)
84+
def prepare_for_negotiated_authenticate(self, entityid=None, relay_state="",
85+
binding=None, vorg="",
86+
nameid_format=None,
87+
scoping=None, consent=None, extensions=None,
88+
sign=None,
89+
response_binding=saml2.BINDING_HTTP_POST,
90+
**kwargs):
91+
""" Makes all necessary preparations for an authentication request that negotiates
92+
which binding to use for authentication.
7793
78-
info = self.apply_binding(binding, _req_str, destination, relay_state)
94+
:param entityid: The entity ID of the IdP to send the request to
95+
:param relay_state: To where the user should be returned after
96+
successfull log in.
97+
:param binding: Which binding to use for sending the request
98+
:param vorg: The entity_id of the virtual organization I'm a member of
99+
:param scoping: For which IdPs this query are aimed.
100+
:param consent: Whether the principal have given her consent
101+
:param extensions: Possible extensions
102+
:param sign: Whether the request should be signed or not.
103+
:param response_binding: Which binding to use for receiving the response
104+
:param kwargs: Extra key word arguments
105+
:return: session id and AuthnRequest info
106+
"""
79107

80-
return reqid, info
108+
expected_binding = binding
109+
110+
for binding in [BINDING_HTTP_REDIRECT, BINDING_HTTP_POST]:
111+
if expected_binding and binding != expected_binding:
112+
continue
113+
114+
destination = self._sso_location(entityid, binding)
115+
logger.info("destination to provider: %s" % destination)
116+
117+
reqid, request = self.create_authn_request(
118+
destination, vorg, scoping, response_binding, nameid_format,
119+
consent=consent,
120+
extensions=extensions, sign=sign,
121+
**kwargs)
122+
123+
_req_str = str(request)
124+
125+
logger.info("AuthNReq: %s" % _req_str)
126+
127+
http_info = self.apply_binding(binding, _req_str, destination,
128+
relay_state)
129+
130+
return reqid, binding, http_info
131+
else:
132+
raise SignOnError("No supported bindings available for authentication")
81133

82134
def global_logout(self, name_id, reason="", expire=None, sign=None):
83135
""" More or less a layer of indirection :-/

src/saml2/client_base.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ class VerifyError(SAMLError):
7171
pass
7272

7373

74+
class SignOnError(SAMLError):
75+
pass
76+
77+
7478
class LogoutError(SAMLError):
7579
pass
7680

src/saml2/mdbcache.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
#!/usr/bin/env python
22
import logging
3+
from pymongo.mongo_client import MongoClient
34

45
__author__ = 'rolandh'
56

6-
from pymongo import Connection
77
#import cjson
88
import time
99
from datetime import datetime
@@ -18,9 +18,9 @@
1818
class Cache(object):
1919
def __init__(self, server=None, debug=0, db=None):
2020
if server:
21-
connection = Connection(server)
21+
connection = MongoClient(server)
2222
else:
23-
connection = Connection()
23+
connection = MongoClient()
2424

2525
if db:
2626
self._db = connection[db]

src/saml2/sigver.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1820,10 +1820,11 @@ def pre_encrypt_assertion(response):
18201820
assertion = response.assertion
18211821
response.assertion = None
18221822
response.encrypted_assertion = EncryptedAssertion()
1823-
if isinstance(assertion, list):
1824-
response.encrypted_assertion.add_extension_elements(assertion)
1825-
else:
1826-
response.encrypted_assertion.add_extension_element(assertion)
1823+
if assertion is not None:
1824+
if isinstance(assertion, list):
1825+
response.encrypted_assertion.add_extension_elements(assertion)
1826+
else:
1827+
response.encrypted_assertion.add_extension_element(assertion)
18271828
# txt = "%s" % response
18281829
# _ass = "%s" % assertion
18291830
# _ass = rm_xmltag(_ass)

tests/test_51_client.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,26 @@ def test_do_authn(self):
635635
resp_args = self.server.response_args(req.message, [response_binding])
636636
assert resp_args["binding"] == response_binding
637637

638+
def test_do_negotiated_authn(self):
639+
binding = BINDING_HTTP_REDIRECT
640+
response_binding = BINDING_HTTP_POST
641+
sid, auth_binding, http_args = self.client.prepare_for_negotiated_authenticate(
642+
IDP, "http://www.example.com/relay_state",
643+
binding=binding, response_binding=response_binding)
644+
645+
assert binding == auth_binding
646+
assert isinstance(sid, basestring)
647+
assert len(http_args) == 4
648+
assert http_args["headers"][0][0] == "Location"
649+
assert http_args["data"] == []
650+
redirect_url = http_args["headers"][0][1]
651+
_, _, _, _, qs, _ = urlparse.urlparse(redirect_url)
652+
qs_dict = urlparse.parse_qs(qs)
653+
req = self.server.parse_authn_request(qs_dict["SAMLRequest"][0],
654+
binding)
655+
resp_args = self.server.response_args(req.message, [response_binding])
656+
assert resp_args["binding"] == response_binding
657+
638658
def test_do_attribute_query(self):
639659
response = self.client.do_attribute_query(
640660
IDP, "_e7b68a04488f715cda642fbdd90099f5",
@@ -699,6 +719,41 @@ def test_post_sso(self):
699719
'http://www.example.com/login'
700720
assert ac.authn_context_class_ref.text == INTERNETPROTOCOLPASSWORD
701721

722+
def test_negotiated_post_sso(self):
723+
binding = BINDING_HTTP_POST
724+
response_binding = BINDING_HTTP_POST
725+
sid, auth_binding, http_args = self.client.prepare_for_negotiated_authenticate(
726+
"urn:mace:example.com:saml:roland:idp", relay_state="really",
727+
binding=binding, response_binding=response_binding)
728+
_dic = unpack_form(http_args["data"][3])
729+
730+
assert binding == auth_binding
731+
732+
req = self.server.parse_authn_request(_dic["SAMLRequest"], binding)
733+
resp_args = self.server.response_args(req.message, [response_binding])
734+
assert resp_args["binding"] == response_binding
735+
736+
# Normally a response would now be sent back to the users web client
737+
# Here I fake what the client will do
738+
# create the form post
739+
740+
http_args["data"] = urllib.urlencode(_dic)
741+
http_args["method"] = "POST"
742+
http_args["dummy"] = _dic["SAMLRequest"]
743+
http_args["headers"] = [('Content-type',
744+
'application/x-www-form-urlencoded')]
745+
746+
response = self.client.send(**http_args)
747+
print response.text
748+
_dic = unpack_form(response.text[3], "SAMLResponse")
749+
resp = self.client.parse_authn_request_response(_dic["SAMLResponse"],
750+
BINDING_HTTP_POST,
751+
{sid: "/"})
752+
ac = resp.assertion.authn_statement[0].authn_context
753+
assert ac.authenticating_authority[0].text == \
754+
'http://www.example.com/login'
755+
assert ac.authn_context_class_ref.text == INTERNETPROTOCOLPASSWORD
756+
702757

703758
# if __name__ == "__main__":
704759
# tc = TestClient()
@@ -708,4 +763,4 @@ def test_post_sso(self):
708763
if __name__ == "__main__":
709764
tc = TestClient()
710765
tc.setup_class()
711-
tc.test_sign_then_encrypt_assertion_advice()
766+
tc.test_sign_then_encrypt_assertion_advice()

0 commit comments

Comments
 (0)