Fixed artifact handling

This commit is contained in:
Roland Hedberg 2013-01-18 14:59:00 +01:00
parent 21880631d9
commit 13f327c0bc
12 changed files with 151 additions and 72 deletions

View File

@ -204,7 +204,7 @@ def slo(environ, start_response, user):
bindings, "spsso", response) bindings, "spsso", response)
http_args = IDP.apply_binding(binding, "%s" % response, destination, http_args = IDP.apply_binding(binding, "%s" % response, destination,
query["RelayState"], "SAMLResponse") query["RelayState"], response=True)
except Exception, exc: except Exception, exc:
resp = BadRequest('%s' % exc) resp = BadRequest('%s' % exc)

View File

@ -180,7 +180,7 @@ def _sso(environ, start_response, user, query, binding, relay_state=""):
binding, destination = IDP.pick_binding("assertion_consumer_service", binding, destination = IDP.pick_binding("assertion_consumer_service",
entity_id=_authn_req.issuer.text) entity_id=_authn_req.issuer.text)
http_args = IDP.apply_binding(binding, "%s" % authn_resp, destination, http_args = IDP.apply_binding(binding, "%s" % authn_resp, destination,
relay_state, "SAMLResponse") relay_state, response=True)
resp = Response(http_args["data"], headers=http_args["headers"]) resp = Response(http_args["data"], headers=http_args["headers"])
return resp(environ, start_response) return resp(environ, start_response)
@ -495,7 +495,7 @@ def assertion_id_request(environ, start_response, user=None):
resp_args = IDP.response_args(req_info.message, _binding, "spsso") resp_args = IDP.response_args(req_info.message, _binding, "spsso")
response = IDP.create_assertion_id_request_response(asids, **resp_args) response = IDP.create_assertion_id_request_response(asids, **resp_args)
hinfo = IDP.apply_binding(_binding, "%s" % response, "","","SAMLResponse") hinfo = IDP.apply_binding(_binding, "%s" % response, "","",response=True)
resp = Response(hinfo["data"], headers=hinfo["headers"]) resp = Response(hinfo["data"], headers=hinfo["headers"])
return resp(environ, start_response) return resp(environ, start_response)
@ -522,7 +522,7 @@ def artifact_resolve_service(environ, start_response, user=None):
msg = IDP.create_artifact_response(_req, _req.artifact.text) msg = IDP.create_artifact_response(_req, _req.artifact.text)
hinfo = IDP.apply_binding(_binding, "%s" % msg, "","","SAMLResponse") hinfo = IDP.apply_binding(_binding, "%s" % msg, "","",response=True)
resp = Response(hinfo["data"], headers=hinfo["headers"]) resp = Response(hinfo["data"], headers=hinfo["headers"])
return resp(environ, start_response) return resp(environ, start_response)
@ -553,7 +553,7 @@ def authn_query_service(environ, start_response, user=None):
_query.session_index) _query.session_index)
logger.debug("response: %s" % msg) logger.debug("response: %s" % msg)
hinfo = IDP.apply_binding(_binding, "%s" % msg, "","","SAMLResponse") hinfo = IDP.apply_binding(_binding, "%s" % msg, "","",response=True)
resp = Response(hinfo["data"], headers=hinfo["headers"]) resp = Response(hinfo["data"], headers=hinfo["headers"])
return resp(environ, start_response) return resp(environ, start_response)
@ -585,7 +585,7 @@ def _nim(environ, start_response, user, query, binding, relay_state=""):
_resp = IDP.create_manage_name_id_response(name_id, **info) _resp = IDP.create_manage_name_id_response(name_id, **info)
# It's using SOAP binding # It's using SOAP binding
hinfo = IDP.apply_binding(binding, "%s" % _resp, "", "", "SAMLResponse") hinfo = IDP.apply_binding(binding, "%s" % _resp, "", "", response=True)
resp = Response(hinfo["data"], resp = Response(hinfo["data"],
headers=dict2list_of_tuples(hinfo["headers"])) headers=dict2list_of_tuples(hinfo["headers"]))
@ -629,8 +629,8 @@ def nim_art(environ, start_response, user):
_dict = unpack_redirect(environ) _dict = unpack_redirect(environ)
if not _dict: if not _dict:
_dict = unpack_post() _dict = unpack_post(environ)
return _mni return _artifact_oper(environ, start_response, user, _dict, _mni)
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------

View File

@ -138,7 +138,7 @@ PREFERRED_BINDING={
"attribute_service": [BINDING_SOAP], "attribute_service": [BINDING_SOAP],
"authz_service": [BINDING_SOAP], "authz_service": [BINDING_SOAP],
"assertion_id_request_service": [BINDING_URI], "assertion_id_request_service": [BINDING_URI],
"artifact_resolution_service": [BINDING_HTTP_REDIRECT, BINDING_HTTP_POST], "artifact_resolution_service": [BINDING_SOAP],
"attribute_consuming_service": _RPA "attribute_consuming_service": _RPA
} }

View File

@ -4,7 +4,7 @@ from hashlib import sha1
from saml2.metadata import ENDPOINTS from saml2.metadata import ENDPOINTS
from saml2.soap import parse_soap_enveloped_saml_artifact_resolve from saml2.soap import parse_soap_enveloped_saml_artifact_resolve
from saml2 import samlp, saml, response, BINDING_URI from saml2 import samlp, saml, response, BINDING_URI, BINDING_HTTP_ARTIFACT
from saml2 import request from saml2 import request
from saml2 import soap from saml2 import soap
from saml2 import element_to_extension_element from saml2 import element_to_extension_element
@ -69,7 +69,6 @@ def create_artifact(entity_id, message_handle, endpoint_index = 0):
return base64.b64encode(ter) return base64.b64encode(ter)
class Entity(HTTPBase): class Entity(HTTPBase):
def __init__(self, entity_type, config=None, config_file="", def __init__(self, entity_type, config=None, config_file="",
virtual_organization=""): virtual_organization=""):
@ -121,7 +120,7 @@ class Entity(HTTPBase):
format=NAMEID_FORMAT_ENTITY) format=NAMEID_FORMAT_ENTITY)
def apply_binding(self, binding, msg_str, destination="", relay_state="", def apply_binding(self, binding, msg_str, destination="", relay_state="",
typ="SAMLRequest"): response=False):
""" """
Construct the necessary HTTP arguments dependent on Binding Construct the necessary HTTP arguments dependent on Binding
@ -129,9 +128,15 @@ class Entity(HTTPBase):
:param msg_str: The return message as a string (XML) :param msg_str: The return message as a string (XML)
:param destination: Where to send the message :param destination: Where to send the message
:param relay_state: Relay_state if provided :param relay_state: Relay_state if provided
:param typ: Which type of message this is :param response: Which type of message this is
:return: A dictionary :return: A dictionary
""" """
# unless if BINDING_HTTP_ARTIFACT
if response:
typ = "SAMLResponse"
else:
typ = "SAMLRequest"
if binding == BINDING_HTTP_POST: if binding == BINDING_HTTP_POST:
logger.info("HTTP POST") logger.info("HTTP POST")
info = self.use_http_form_post(msg_str, destination, info = self.use_http_form_post(msg_str, destination,
@ -147,6 +152,14 @@ class Entity(HTTPBase):
info = self.use_soap(msg_str, destination) info = self.use_soap(msg_str, destination)
elif binding == BINDING_URI: elif binding == BINDING_URI:
info = self.use_http_uri(msg_str, typ, destination) info = self.use_http_uri(msg_str, typ, destination)
elif binding == BINDING_HTTP_ARTIFACT:
typ = "SAMLart"
if response:
info = self.use_http_artifact(msg_str, destination, relay_state)
info["method"] = "GET"
info["status"] = 302
else:
info = self.use_http_artifact(msg_str, destination, relay_state)
else: else:
raise Exception("Unknown binding type: %s" % binding) raise Exception("Unknown binding type: %s" % binding)

View File

@ -2,6 +2,7 @@ import calendar
import cookielib import cookielib
import copy import copy
import re import re
import urllib
import urlparse import urlparse
import requests import requests
import time import time
@ -206,7 +207,19 @@ class HTTPBase(object):
return http_redirect_message(message, destination, relay_state, typ) return http_redirect_message(message, destination, relay_state, typ)
def use_http_uri(self, message, typ, destination=""): def use_http_artifact(self, message, destination="", relay_state=""):
if relay_state:
query = urllib.urlencode({"SAMLart": message,
"RelayState": relay_state})
else:
query = urllib.urlencode({"SAMLart": message})
info = {
"data": "",
"url": "%s?%s" % (destination, query)
}
return info
def use_http_uri(self, message, typ, destination="", relay_state=""):
if typ == "SAMLResponse": if typ == "SAMLResponse":
info = { info = {
"data": message.split("\n")[1], "data": message.split("\n")[1],
@ -218,9 +231,14 @@ class HTTPBase(object):
} }
elif typ == "SAMLRequest": elif typ == "SAMLRequest":
# msg should be an identifier # msg should be an identifier
if relay_state:
query = urllib.urlencode({"ID": message,
"RelayState": relay_state})
else:
query = urllib.urlencode({"ID": message})
info = { info = {
"data": "", "data": "",
"url": "%s?ID=%s" % (destination, message) "url": "%s?%s" % (destination, query)
} }
else: else:
raise NotImplemented raise NotImplemented

View File

@ -124,7 +124,7 @@ def http_redirect_message(message, location, relay_state="", typ="SAMLRequest"):
glue_char = "&" if urlparse.urlparse(location).query else "?" glue_char = "&" if urlparse.urlparse(location).query else "?"
login_url = glue_char.join([location, urllib.urlencode(args)]) login_url = glue_char.join([location, urllib.urlencode(args)])
headers = [('Location', login_url)] headers = [('Location', login_url)]
body = [""] body = []
return {"headers":headers, "data":body} return {"headers":headers, "data":body}

View File

@ -125,6 +125,7 @@ def parse_soap_enveloped_saml_thingy(text, expected_tags):
""" """
envelope = ElementTree.fromstring(text) envelope = ElementTree.fromstring(text)
# Make sure it's a SOAP message
assert envelope.tag == '{%s}Envelope' % soapenv.NAMESPACE assert envelope.tag == '{%s}Envelope' % soapenv.NAMESPACE
assert len(envelope) >= 1 assert len(envelope) >= 1

View File

@ -458,8 +458,8 @@ class TestServerLogout():
None, "spsso", request) None, "spsso", request)
http_args = server.apply_binding(binding, "%s" % response, destination, http_args = server.apply_binding(binding, "%s" % response, destination,
"relay_state", "SAMLResponse") "relay_state", response=True)
assert len(http_args) == 4 assert len(http_args) == 4
assert http_args["headers"][0][0] == "Location" assert http_args["headers"][0][0] == "Location"
assert http_args["data"] == [''] assert http_args["data"] == []

View File

@ -349,7 +349,7 @@ class TestClientWithDummy():
assert isinstance(id, basestring) assert isinstance(id, basestring)
assert len(http_args) == 4 assert len(http_args) == 4
assert http_args["headers"][0][0] == "Location" assert http_args["headers"][0][0] == "Location"
assert http_args["data"] == [""] assert http_args["data"] == []
def test_do_attribute_query(self): def test_do_attribute_query(self):
response = self.client.do_attribute_query(IDP, response = self.client.do_attribute_query(IDP,

View File

@ -1,10 +1,11 @@
import base64 import base64
from hashlib import sha1 from hashlib import sha1
import urlparse from urlparse import urlparse
from urlparse import parse_qs
from saml2.saml import AUTHN_PASSWORD from saml2.saml import AUTHN_PASSWORD
from saml2 import BINDING_HTTP_REDIRECT from saml2 import BINDING_HTTP_ARTIFACT
from saml2 import BINDING_SOAP
from saml2 import BINDING_HTTP_POST from saml2 import BINDING_HTTP_POST
from saml2.pack import http_redirect_message
from saml2.client import Saml2Client from saml2.client import Saml2Client
from saml2.entity import create_artifact from saml2.entity import create_artifact
@ -14,6 +15,34 @@ from saml2.server import Server
__author__ = 'rolandh' __author__ = 'rolandh'
TAG1 = "name=\"SAMLRequest\" value="
def get_msg(hinfo, binding, response=False):
if binding == BINDING_SOAP:
msg = hinfo["data"]
elif binding == BINDING_HTTP_POST:
_inp = hinfo["data"][3]
i = _inp.find(TAG1)
i += len(TAG1) + 1
j = _inp.find('"', i)
msg = _inp[i:j]
elif binding == BINDING_HTTP_ARTIFACT:
# either by POST or by redirect
if hinfo["data"]:
_inp = hinfo["data"][3]
i = _inp.find(TAG1)
i += len(TAG1) + 1
j = _inp.find('"', i)
msg = _inp[i:j]
else:
parts = urlparse(hinfo["url"])
msg = parse_qs(parts.query)["SAMLart"][0]
else: # BINDING_HTTP_REDIRECT
parts = urlparse(hinfo["headers"][0][1])
msg = parse_qs(parts.query)["SAMLRequest"][0]
return msg
def test_create_artifact(): def test_create_artifact():
b64art = create_artifact("http://sp.example.com/saml.xml", b64art = create_artifact("http://sp.example.com/saml.xml",
"aabbccddeeffgghhiijj") "aabbccddeeffgghhiijj")
@ -60,64 +89,71 @@ def test_create_artifact_resolve():
assert ar.artifact.text == b64art assert ar.artifact.text == b64art
def test_artifact_flow(): def test_artifact_flow():
SP = 'urn:mace:example.com:saml:roland:sp'
sp = Saml2Client(config_file="servera_conf") sp = Saml2Client(config_file="servera_conf")
idp = Server(config_file="idp_all_conf") idp = Server(config_file="idp_all_conf")
# ======= SP ==========
# original request # original request
srvs = sp.metadata.single_sign_on_service(idp.config.entityid,
BINDING_HTTP_REDIRECT)
destination=srvs[0]["location"] binding, destination = sp.pick_binding("single_sign_on_service",
req = sp.create_authn_request(destination, id = "id1") entity_id=idp.config.entityid)
relay_state = "RS0"
req = sp.create_authn_request(destination, id="id1")
# create the artifact
artifact = sp.use_artifact(req, 1) artifact = sp.use_artifact(req, 1)
# HTTP args for sending the message with the artifact
args = http_redirect_message(artifact, destination, "really", "SAMLart")
# ====== IDP ========= binding, destination = sp.pick_binding("single_sign_on_service",
# simulating the IDP receiver [BINDING_HTTP_ARTIFACT],
artifact2 = None entity_id=idp.config.entityid)
for item, val in args["headers"]:
if item == "Location": hinfo = sp.apply_binding(binding, "%s" % artifact, destination, relay_state)
part = urlparse.urlparse(val)
query = urlparse.parse_qs(part.query) # ========== @IDP ============
artifact2 = query["SAMLart"][0]
artifact2 = get_msg(hinfo, binding)
assert artifact == artifact2
# The IDP now wants to replace the artifact with the real request
# Got an artifact, now want to get the original request
destination = idp.artifact2destination(artifact2, "spsso") destination = idp.artifact2destination(artifact2, "spsso")
msg = idp.create_artifact_resolve(artifact2, destination, sid()) msg = idp.create_artifact_resolve(artifact2, destination, sid())
args = idp.use_soap(msg, destination, None, False) hinfo = idp.use_soap(msg, destination, None, False)
# ======== SP ========== # ======== @SP ==========
ar = sp.parse_artifact_resolve(args["data"]) msg = get_msg(hinfo, BINDING_SOAP)
print ar ar = sp.parse_artifact_resolve(msg)
assert ar.artifact.text == artifact assert ar.artifact.text == artifact
# The SP picks the request out of the repository with the artifact as the key
oreq = sp.artifact[ar.artifact.text] oreq = sp.artifact[ar.artifact.text]
# Should be the same as req above # Should be the same as req above
# Returns the information over the existing SOAP connection so
# no transport information needed
msg = sp.create_artifact_response(ar, ar.artifact.text) msg = sp.create_artifact_response(ar, ar.artifact.text)
args = sp.use_soap(msg, destination) hinfo = sp.use_soap(msg, destination)
# ========== IDP ============ # ========== @IDP ============
spreq = idp.parse_artifact_resolve_response(args["data"]) msg = get_msg(hinfo, BINDING_SOAP)
# The IDP untangles the request from the artifact resolve response
spreq = idp.parse_artifact_resolve_response(msg)
# should be the same as req above # should be the same as req above
print spreq
assert spreq.id == req.id assert spreq.id == req.id
# That was one way # That was one way, the Request from the SP
# ------------------------------------ # ---------------------------------------------#
# Now for the other # Now for the other, the response from the IDP
name_id = idp.ident.transient_nameid(sp.config.entityid, "derek") name_id = idp.ident.transient_nameid(sp.config.entityid, "derek")
@ -134,44 +170,54 @@ def test_artifact_flow():
print response print response
artifact = idp.use_artifact(response, 1) # with the response in hand create an artifact
args = http_redirect_message(artifact, resp_args["destination"], "really2",
"SAMLart")
artifact2=None artifact = idp.use_artifact(response, 1)
for item, val in args["headers"]:
if item == "Location": binding, destination = sp.pick_binding("single_sign_on_service",
part = urlparse.urlparse(val) [BINDING_HTTP_ARTIFACT],
query = urlparse.parse_qs(part.query) entity_id=idp.config.entityid)
artifact2 = query["SAMLart"][0]
hinfo = sp.apply_binding(binding, "%s" % artifact, destination, relay_state,
response=True)
# ========== SP ========= # ========== SP =========
destination = sp.artifact2destination(artifact2, "idpsso") artifact3 = get_msg(hinfo, binding)
msg = sp.create_artifact_resolve(artifact2, destination, sid()) assert artifact == artifact3
destination = sp.artifact2destination(artifact3, "idpsso")
# Got an artifact want to replace it with the real message
msg = sp.create_artifact_resolve(artifact3, destination, sid())
print msg print msg
args = sp.use_soap(msg, destination, None, False) hinfo = sp.use_soap(msg, destination, None, False)
# ======== IDP ========== # ======== IDP ==========
ar = idp.parse_artifact_resolve(args["data"]) msg = get_msg(hinfo, BINDING_SOAP)
ar = idp.parse_artifact_resolve(msg)
print ar print ar
assert ar.artifact.text == artifact assert ar.artifact.text == artifact3
# The IDP retrieves the response from the database using the artifact as the key
oreq = idp.artifact[ar.artifact.text] oreq = idp.artifact[ar.artifact.text]
# Should be the same as req above
msg = idp.create_artifact_response(ar, ar.artifact.text) binding, destination = idp.pick_binding("artifact_resolution_service",
args = idp.use_soap(msg, destination) entity_id=sp.config.entityid)
resp = idp.create_artifact_response(ar, ar.artifact.text)
hinfo = idp.use_soap(resp, destination)
# ========== SP ============ # ========== SP ============
sp_resp = sp.parse_artifact_resolve_response(args["data"]) msg = get_msg(hinfo, BINDING_SOAP)
sp_resp = sp.parse_artifact_resolve_response(msg)
assert sp_resp.id == response.id assert sp_resp.id == response.id

View File

@ -119,7 +119,8 @@ def test_flow():
print p_res print p_res
hinfo = idp.apply_binding(binding, "%s" % p_res, "", "state2", "SAMLResponse") hinfo = idp.apply_binding(binding, "%s" % p_res, "", "state2",
response=True)
# ------- @SP ---------- # ------- @SP ----------

View File

@ -91,7 +91,7 @@ def test_basic_flow():
resp = idp.create_assertion_id_request_response(aid) resp = idp.create_assertion_id_request_response(aid)
hinfo = idp.apply_binding(binding, "%s" % resp, None, "", "SAMLResponse") hinfo = idp.apply_binding(binding, "%s" % resp, None, "", response=True)
# ----------- @SP ------------- # ----------- @SP -------------