This repository has been archived on 2023-02-21. You can view files and clone it, but cannot push or open issues or pull requests.
pyoidc-ozwillo/src/oic/utils/authn/saml.py

373 lines
15 KiB
Python

import importlib
import json
from tempfile import NamedTemporaryFile
from urllib import urlencode
import logging
import base64
from urlparse import parse_qs
from oic.oauth2 import VerificationError
from oic.utils.authn.user import UserAuthnMethod
from oic.utils.authn.user import create_return_url
from oic.utils.http_util import Redirect
from oic.utils.http_util import SeeOther
from oic.utils.http_util import Response
from oic.utils.http_util import Unauthorized
logger = logging.getLogger(__name__)
class ServiceErrorException(Exception):
pass
try:
import saml2
except ImportError:
saml2 = None
class SAMLAuthnMethod(UserAuthnMethod):
pass
else:
from saml2 import BINDING_HTTP_REDIRECT
from saml2 import BINDING_HTTP_ARTIFACT
from saml2 import BINDING_HTTP_POST
from saml2.client import Saml2Client
from saml2.s_utils import sid
from saml2.s_utils import rndstr
from saml2.s_utils import UnknownPrincipal
from saml2.s_utils import UnsupportedBinding
# This class handles user authentication with CAS.
class SAMLAuthnMethod(UserAuthnMethod):
CONST_QUERY = "query"
CONST_SAML_COOKIE = "samlauthc"
CONST_HASIDP = "hasidp"
def __init__(self, srv, lookup, userdb, spconf, url, return_to,
cache=None,
bindings=None, userinfo=None, samlcache=None):
"""
Constructor for the class.
:param srv: Usually none, but otherwise the oic server.
:param return_to: The URL to return to after a successful
authentication.
"""
self.userdb = userdb
self.userinfo = userinfo
if cache is None:
self.cache_outstanding_queries = {}
else:
self.cache_outstanding_queries = cache
UserAuthnMethod.__init__(self, srv)
self.return_to = return_to
self.idp_query_param = "IdpQuery"
if bindings:
self.bindings = bindings
else:
self.bindings = [BINDING_HTTP_REDIRECT, BINDING_HTTP_POST,
BINDING_HTTP_ARTIFACT]
# TODO Why does this exist?
self.verification_endpoint = ""
#Configurations for the SP handler. (pyOpSamlProxy.client.sp.conf)
self.sp_conf = importlib.import_module(spconf)
#self.sp_conf.BASE = self.sp_conf.BASE % url
ntf = NamedTemporaryFile(suffix="pyoidc.py", delete=True)
ntf.write("CONFIG = " + str(self.sp_conf.CONFIG).replace("%s", url))
ntf.seek(0)
self.sp = Saml2Client(config_file="%s" % ntf.name)
mte = lookup.get_template("unauthorized.mako")
argv = {
"message": "You are not authorized!",
}
self.not_authorized = mte.render(**argv)
self.samlcache = self.sp_conf.SAML_CACHE
def __call__(self, query="", end_point_index=None, *args, **kwargs):
(done, response) = self._pick_idp(query, end_point_index)
if done == 0:
entity_id = response
# Do the AuthnRequest
resp = self._redirect_to_auth(self.sp, entity_id, query, end_point_index)
return resp
return response
def verify(self, request, cookie, path, requrl, end_point_index=None, **kwargs):
"""
Verifies if the authentication was successful.
:rtype : Response
:param request: Contains the request parameters.
:param cookie: Cookies sent with the request.
:param kwargs: Any other parameters.
:return: If the authentication was successful: a redirect to the
return_to url. Otherwise a unauthorized response.
:raise: ValueError
"""
if isinstance(request, basestring):
request = parse_qs(request)
elif isinstance(request, dict):
pass
else:
raise ValueError("Wrong type of input")
acs = self.sp.config.getattr("endpoints", "sp")["assertion_consumer_service"]
acs_endpoints = [(ep[0].rsplit("/", 1)[1], ep[1]) for ep in acs]
binding = None
path = path[1:]
for endp in acs_endpoints:
if path == endp[0]:
binding = endp[1]
break
saml_cookie, _ts, _typ = self.getCookieValue(cookie,
self.CONST_SAML_COOKIE)
data = json.loads(saml_cookie)
rp_query_cookie = self.get_multi_auth_cookie(cookie)
query = rp_query_cookie
if not query:
query = base64.b64decode(data[self.CONST_QUERY])
if data[self.CONST_HASIDP] == 'False':
(done, response) = self._pick_idp(request, end_point_index)
if done == 0:
entity_id = response
# Do the AuthnRequest
resp = self._redirect_to_auth(self.sp, entity_id, query, end_point_index)
return resp, False
return response, False
if not request:
logger.info("Missing Response")
return Unauthorized("You are not authorized!"), False
try:
response = self.sp.parse_authn_request_response(
request["SAMLResponse"][0], binding,
self.cache_outstanding_queries)
except UnknownPrincipal, excp:
logger.error("UnknownPrincipal: %s" % (excp,))
return Unauthorized(self.not_authorized), False
except UnsupportedBinding, excp:
logger.error("UnsupportedBinding: %s" % (excp,))
return Unauthorized(self.not_authorized), False
except VerificationError, err:
logger.error("Verification error: %s" % (err,))
return Unauthorized(self.not_authorized), False
except Exception, err:
logger.error("Other error: %s" % (err,))
return Unauthorized(self.not_authorized), False
if self.sp_conf.VALID_ATTRIBUTE_RESPONSE is not None:
for k, v in self.sp_conf.VALID_ATTRIBUTE_RESPONSE.iteritems():
if k not in response.ava:
return Unauthorized(self.not_authorized), False
else:
allowed = False
for allowed_attr_value in v:
if isinstance(response.ava[k], list):
for resp_value in response.ava[k]:
if allowed_attr_value in resp_value:
allowed = True
break
elif allowed_attr_value in response.ava[k]:
allowed = True
break
if not allowed:
return Unauthorized(self.not_authorized), False
# logger.info("parsed OK")'
uid = response.assertion.subject.name_id.text
if self.userinfo == "AA":
if response.entity_id is not None and self.samlcache is not None:
self.samlcache["AA_ENTITYID"] = response.entity_id
self.setup_userdb(uid, response.ava)
return_to = create_return_url(self.return_to, uid,
**{self.query_param: "true"})
if '?' in return_to:
return_to += "&"
else:
return_to += "?"
return_to += query
auth_cookie = self.create_cookie(uid, "samlm")
resp = Redirect(return_to, headers=[auth_cookie])
return resp, True
def setup_userdb(self, uid, samldata):
attributes = {}
if self.sp_conf.ATTRIBUTE_WHITELIST is not None:
for attr, allowed in self.sp_conf.ATTRIBUTE_WHITELIST.iteritems():
if attr in samldata:
if allowed is not None:
tmp_attr_list = []
for tmp_value in samldata[attr]:
for allowed_str in allowed:
if allowed_str in tmp_value:
tmp_attr_list.append(tmp_value)
if len(tmp_attr_list) > 0:
attributes[attr] = tmp_attr_list
else:
attributes[attr] = samldata[attr]
else:
attributes = samldata
userdb = {}
if self.sp_conf.OPENID2SAMLMAP is None:
userdb = attributes.copy()
else:
for oic, saml in self.sp_conf.OPENID2SAMLMAP.iteritems():
if saml in attributes:
userdb[oic] = attributes[saml]
self.userdb[uid] = userdb
def _pick_idp(self, query, end_point_index):
"""
If more than one idp and if none is selected, I have to do wayf or
disco
"""
query_dict = {}
if isinstance(query, basestring):
query_dict = dict(parse_qs(query))
else:
for key, value in query.iteritems():
if isinstance(value, list):
query_dict[key] = value[0]
else:
query_dict[key] = value
query = urlencode(query_dict)
_cli = self.sp
# Find all IdPs
idps = self.sp.metadata.with_descriptor("idpsso")
idp_entity_id = None
if len(idps) == 1:
# idps is a dictionary
idp_entity_id = idps.keys()[0]
if not idp_entity_id and query:
try:
_idp_entity_id = query_dict[self.idp_query_param][0]
if _idp_entity_id in idps:
idp_entity_id = _idp_entity_id
except KeyError:
logger.debug("No IdP entity ID in query: %s" % query)
pass
if not idp_entity_id:
cookie = self.create_cookie(
'{"' + self.CONST_QUERY + '": "' + base64.b64encode(query) +
'" , "' + self.CONST_HASIDP + '": "False" }',
self.CONST_SAML_COOKIE, self.CONST_SAML_COOKIE)
if self.sp_conf.WAYF:
if query:
try:
wayf_selected = query_dict["wayf_selected"][0]
except KeyError:
return self._wayf_redirect(cookie)
idp_entity_id = wayf_selected
else:
return self._wayf_redirect(cookie)
elif self.sp_conf.DISCOSRV:
if query:
idp_entity_id = _cli.parse_discovery_service_response(query=query)
if not idp_entity_id:
sid_ = sid()
self.cache_outstanding_queries[sid_] = self.verification_endpoint
eid = _cli.config.entityid
disco_end_point_index = end_point_index["disco_end_point_index"]
ret = _cli.config.getattr("endpoints", "sp")[
"discovery_response"][disco_end_point_index][0]
ret += "?sid=%s" % sid_
loc = _cli.create_discovery_service_request(
self.sp_conf.DISCOSRV, eid, **{"return": ret})
return -1, SeeOther(loc, headers=[cookie])
elif not len(idps):
raise ServiceErrorException(
'Misconfiguration for the SAML Service Provider!')
else:
return -1, NotImplemented("No WAYF or DS present!")
return 0, idp_entity_id
def _wayf_redirect(self, cookie):
sid_ = sid()
self.cache_outstanding_queries[sid_] = self.verification_endpoint
return -1, SeeOther(headers=[
('Location', "%s?%s" % (self.sp_conf.WAYF, sid_)), cookie])
def _redirect_to_auth(self, _cli, entity_id, query, end_point_index, vorg_name=""):
try:
binding, destination = _cli.pick_binding(
"single_sign_on_service", self.bindings, "idpsso",
entity_id=entity_id)
logger.debug("binding: %s, destination: %s" % (binding,
destination))
extensions = None
kwargs = {}
if end_point_index:
kwargs["assertion_consumer_service_index"] = end_point_index[binding]
if _cli.authn_requests_signed:
_sid = saml2.s_utils.sid(_cli.seed)
req_id, msg_str = _cli.create_authn_request(
destination, vorg=vorg_name,
sign=_cli.authn_requests_signed, message_id=_sid,
extensions=extensions, **kwargs)
_sid = req_id
else:
req_id, req = _cli.create_authn_request(destination,
vorg=vorg_name,
sign=False, **kwargs)
msg_str = "%s" % req
_sid = req_id
_rstate = rndstr()
# self.cache.relay_state[_rstate] = came_from
ht_args = _cli.apply_binding(binding, msg_str, destination,
relay_state=_rstate)
logger.debug("ht_args: %s" % ht_args)
except Exception, exc:
logger.exception(exc)
raise ServiceErrorException(
"Failed to construct the AuthnRequest: %s" % exc)
# remember the request
self.cache_outstanding_queries[_sid] = self.return_to
return self.response(binding, ht_args, query)
def response(self, binding, http_args, query):
cookie = self.create_cookie(
'{"' + self.CONST_QUERY + '": "' + base64.b64encode(query) +
'" , "' + self.CONST_HASIDP + '": "True" }',
self.CONST_SAML_COOKIE, self.CONST_SAML_COOKIE)
if binding == BINDING_HTTP_ARTIFACT:
resp = Redirect()
elif binding == BINDING_HTTP_REDIRECT:
for param, value in http_args["headers"]:
if param == "Location":
resp = SeeOther(str(value), headers=[cookie])
break
else:
raise ServiceErrorException("Parameter error")
else:
http_args["headers"].append(cookie)
resp = Response(http_args["data"],
headers=http_args["headers"])
return resp