Switched from one authentication method to use the AuthnBroker to chose a set of methods based on the requested authentication context.
This commit is contained in:
parent
3bc4cd1d3d
commit
7e19adb496
@ -7,7 +7,8 @@
|
||||
|
||||
<form action="${action}" method="post">
|
||||
<input type="hidden" name="key" value="${key}"/>
|
||||
<input type="hidden" name="came_from" value="${came_from}"/>
|
||||
<input type="hidden" name="authn_reference" value="${authn_reference}"/>
|
||||
<input type="hidden" name="redirect_uri" value="${redirect_uri}"/>
|
||||
|
||||
<div class="label">
|
||||
<label for="login">Username</label>
|
||||
|
@ -17,16 +17,24 @@ from saml2 import BINDING_SOAP
|
||||
from saml2 import BINDING_HTTP_REDIRECT
|
||||
from saml2 import BINDING_HTTP_POST
|
||||
from saml2 import time_util
|
||||
from saml2.httputil import Response, NotFound
|
||||
|
||||
from saml2.authn_context import AuthnBroker
|
||||
from saml2.authn_context import PASSWORD
|
||||
from saml2.authn_context import UNSPECIFIED
|
||||
from saml2.authn_context import authn_context_class_ref
|
||||
from saml2.httputil import Response
|
||||
from saml2.httputil import NotFound
|
||||
from saml2.httputil import geturl
|
||||
from saml2.httputil import get_post
|
||||
from saml2.httputil import Redirect
|
||||
from saml2.httputil import Unauthorized
|
||||
from saml2.httputil import BadRequest
|
||||
from saml2.httputil import ServiceError
|
||||
from saml2.ident import Unknown
|
||||
from saml2.s_utils import rndstr, UnknownPrincipal, UnsupportedBinding
|
||||
from saml2.s_utils import rndstr
|
||||
from saml2.s_utils import UnknownPrincipal
|
||||
from saml2.s_utils import UnsupportedBinding
|
||||
from saml2.s_utils import PolicyError
|
||||
from saml2.saml import AUTHN_PASSWORD
|
||||
from saml2.sigver import verify_redirect_signature
|
||||
|
||||
logger = logging.getLogger("saml2.idp")
|
||||
@ -176,12 +184,14 @@ class Service(object):
|
||||
#
|
||||
# return resp(self.environ, self.start_response)
|
||||
|
||||
def not_authn(self, key):
|
||||
pass
|
||||
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)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
AUTHN = (AUTHN_PASSWORD, "http://lingon.catalogix.se/login")
|
||||
|
||||
REPOZE_ID_EQUIVALENT = "uid"
|
||||
FORM_SPEC = """<form name="myform" method="post" action="%s">
|
||||
@ -194,6 +204,12 @@ FORM_SPEC = """<form name="myform" method="post" action="%s">
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class AuthenticationNeeded(Exception):
|
||||
def __init__(self, authn_context=None, *args, **kwargs):
|
||||
Exception.__init__(*args, **kwargs)
|
||||
self.authn_context = authn_context
|
||||
|
||||
|
||||
class SSO(Service):
|
||||
def __init__(self, environ, start_response, user=None):
|
||||
Service.__init__(self, environ, start_response, user)
|
||||
@ -204,7 +220,7 @@ class SSO(Service):
|
||||
self.destination = None
|
||||
self.req_info = None
|
||||
|
||||
def verify(self, query, binding):
|
||||
def verify_request(self, query, binding):
|
||||
"""
|
||||
:param query: The SAML query, transport encoded
|
||||
:param binding: Which binding the query came in over
|
||||
@ -244,7 +260,7 @@ class SSO(Service):
|
||||
|
||||
def do(self, query, binding_in, relay_state=""):
|
||||
try:
|
||||
resp_args, _resp = self.verify(query, binding_in)
|
||||
resp_args, _resp = self.verify_request(query, binding_in)
|
||||
except UnknownPrincipal, excp:
|
||||
logger.error("UnknownPrincipal: %s" % (excp,))
|
||||
resp = ServiceError("UnknownPrincipal: %s" % (excp,))
|
||||
@ -261,8 +277,10 @@ class SSO(Service):
|
||||
if REPOZE_ID_EQUIVALENT:
|
||||
identity[REPOZE_ID_EQUIVALENT] = self.user
|
||||
try:
|
||||
_resp = IDP.create_authn_response(identity, userid=self.user,
|
||||
authn=AUTHN, **resp_args)
|
||||
_resp = IDP.create_authn_response(
|
||||
identity, userid=self.user,
|
||||
authn=AUTHN_BROKER[self.environ["idp.authn_ref"]],
|
||||
**resp_args)
|
||||
except Exception, excp:
|
||||
logger.error("Exception: %s" % (excp,))
|
||||
resp = ServiceError("Exception: %s" % (excp,))
|
||||
@ -275,64 +293,80 @@ class SSO(Service):
|
||||
logger.debug("HTTPargs: %s" % http_args)
|
||||
return self.response(self.binding_out, http_args)
|
||||
|
||||
def _authn(self, _dict):
|
||||
logger.debug("_auth: %s" % _dict)
|
||||
if not self.user:
|
||||
key = sha1("%s" % _dict).hexdigest()
|
||||
IDP.ticket[key] = _dict
|
||||
_resp = key
|
||||
else:
|
||||
try:
|
||||
_resp = IDP.ticket[_dict["key"]]
|
||||
del IDP.ticket[_dict["key"]]
|
||||
except KeyError:
|
||||
key = sha1("%s" % _dict).hexdigest()
|
||||
IDP.ticket[key] = _dict
|
||||
_resp = key
|
||||
|
||||
return _resp
|
||||
def _store_request(self, _dict):
|
||||
logger.debug("_store_request: %s" % _dict)
|
||||
key = sha1(_dict["SAMLRequest"]).hexdigest()
|
||||
# store the AuthnRequest
|
||||
IDP.ticket[key] = _dict
|
||||
return key
|
||||
|
||||
def redirect(self):
|
||||
""" This is the HTTP-redirect endpoint """
|
||||
logger.info("--- In SSO Redirect ---")
|
||||
_info = self._authn(self.unpack_redirect())
|
||||
if isinstance(_info, basestring):
|
||||
return self.not_authn(_info)
|
||||
_info = self.unpack_redirect()
|
||||
|
||||
if "SigAlg" in _info and "Signature" in _info: # Signed request
|
||||
try:
|
||||
_info = IDP.ticket[_info["key"]]
|
||||
self.req_info = _info["req_info"]
|
||||
del IDP.ticket[_info["key"]]
|
||||
except KeyError:
|
||||
self.req_info = IDP.parse_authn_request(_info["SAMLRequest"],
|
||||
BINDING_HTTP_REDIRECT)
|
||||
issuer = self.req_info.message.issuer.text
|
||||
_certs = IDP.metadata.certs(issuer, "any", "signing")
|
||||
verified_ok = False
|
||||
for cert in _certs:
|
||||
if verify_redirect_signature(_info, cert):
|
||||
verified_ok = True
|
||||
break
|
||||
if not verified_ok:
|
||||
resp = BadRequest("Message signature verification failure")
|
||||
return resp(self.environ, self.start_response)
|
||||
_req = self.req_info.message
|
||||
|
||||
return self.operation(_info, BINDING_HTTP_REDIRECT)
|
||||
if "SigAlg" in _info and "Signature" in _info: # Signed request
|
||||
issuer = _req.issuer.text
|
||||
_certs = IDP.metadata.certs(issuer, "any", "signing")
|
||||
verified_ok = False
|
||||
for cert in _certs:
|
||||
if verify_redirect_signature(_info, cert):
|
||||
verified_ok = True
|
||||
break
|
||||
if not verified_ok:
|
||||
resp = BadRequest("Message signature verification failure")
|
||||
return resp(self.environ, self.start_response)
|
||||
|
||||
if self.user:
|
||||
if _req.force_authn:
|
||||
_info["req_info"] = self.req_info
|
||||
key = self._store_request(_info)
|
||||
return self.not_authn(key, _req.requested_authn_context)
|
||||
else:
|
||||
return self.operation(_info, BINDING_HTTP_REDIRECT)
|
||||
else:
|
||||
_info["req_info"] = self.req_info
|
||||
key = self._store_request(_info)
|
||||
return self.not_authn(key, _req.requested_authn_context)
|
||||
else:
|
||||
return self.operation(_info, BINDING_HTTP_REDIRECT)
|
||||
|
||||
def post(self):
|
||||
"""
|
||||
The HTTP-Post endpoint
|
||||
"""
|
||||
logger.info("--- In SSO POST ---")
|
||||
_dict = self.unpack_either()
|
||||
_resp = self._authn(_dict)
|
||||
logger.debug("_req: %s" % _resp)
|
||||
if isinstance(_resp, basestring):
|
||||
return self.not_authn(_resp)
|
||||
return self.operation(_resp, BINDING_HTTP_POST)
|
||||
_info = self.unpack_either()
|
||||
self.req_info = IDP.parse_authn_request(
|
||||
_info["SAMLRequest"], BINDING_HTTP_POST)
|
||||
_req = self.req_info.message
|
||||
if self.user:
|
||||
if _req.force_authn:
|
||||
_info["req_info"] = self.req_info
|
||||
key = self._store_request(_info)
|
||||
return self.not_authn(key, _req.requested_authn_context)
|
||||
else:
|
||||
return self.operation(_info, BINDING_HTTP_POST)
|
||||
else:
|
||||
_info["req_info"] = self.req_info
|
||||
key = self._store_request(_info)
|
||||
return self.not_authn(key, _req.requested_authn_context)
|
||||
|
||||
def artifact(self):
|
||||
# Can be either by HTTP_Redirect or HTTP_POST
|
||||
_req = self._authn(self.unpack_either())
|
||||
if isinstance(_req, basestring):
|
||||
return self.not_authn(_req)
|
||||
return self.artifact_operation(_req)
|
||||
# def artifact(self):
|
||||
# # 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)
|
||||
# return self.artifact_operation(_req)
|
||||
|
||||
def ecp(self):
|
||||
# The ECP interface
|
||||
@ -368,32 +402,49 @@ class SSO(Service):
|
||||
# === Authentication ====
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def do_authentication(environ, start_response, authn_context, key,
|
||||
redirect_uri):
|
||||
"""
|
||||
Display the login form
|
||||
"""
|
||||
logger.debug("Do authentication")
|
||||
auth_info = AUTHN_BROKER.pick(authn_context)
|
||||
|
||||
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)
|
||||
else:
|
||||
resp = Unauthorized("No usable authentication method")
|
||||
return resp(environ, start_response)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
PASSWD = {"roland": "dianakra",
|
||||
"babs": "howes",
|
||||
"upper": "crust"}
|
||||
|
||||
|
||||
def do_authentication(environ, start_response, cookie=None):
|
||||
def username_password_authn(environ, start_response, reference, key,
|
||||
redirect_uri):
|
||||
"""
|
||||
Display the login form
|
||||
"""
|
||||
query = parse_qs(environ["QUERY_STRING"])
|
||||
|
||||
logger.info("The login page")
|
||||
if cookie:
|
||||
headers = [cookie]
|
||||
else:
|
||||
headers = []
|
||||
headers = []
|
||||
|
||||
resp = Response(mako_template="login.mako", template_lookup=LOOKUP,
|
||||
headers=headers)
|
||||
|
||||
argv = {
|
||||
"action": "/verify",
|
||||
"came_from": query["came_from"][0],
|
||||
"login": "",
|
||||
"password": "",
|
||||
"key": query["key"][0]
|
||||
"key": key,
|
||||
"authn_reference": reference,
|
||||
"redirect_uri": redirect_uri
|
||||
}
|
||||
logger.info("do_authentication argv: %s" % argv)
|
||||
return resp(environ, start_response, **argv)
|
||||
@ -426,10 +477,11 @@ def do_verify(environ, start_response, _):
|
||||
IDP.cache.uid2user[uid] = user
|
||||
IDP.cache.user2uid[user] = uid
|
||||
logger.debug("Register %s under '%s'" % (user, uid))
|
||||
kaka = set_cookie("idpauthn", "/", uid)
|
||||
lox = "http://%s%s?id=%s&key=%s" % (environ["HTTP_HOST"],
|
||||
query["came_from"][0], uid,
|
||||
query["key"][0])
|
||||
|
||||
kaka = set_cookie("idpauthn", "/", uid, query["authn_reference"][0])
|
||||
|
||||
lox = "%s?id=%s&key=%s" % (query["redirect_uri"][0], uid,
|
||||
query["key"][0])
|
||||
logger.debug("Redirect => %s" % lox)
|
||||
resp = Redirect(lox, headers=[kaka], content="text/html")
|
||||
|
||||
@ -663,19 +715,20 @@ class NIM(Service):
|
||||
# ----------------------------------------------------------------------------
|
||||
# Cookie handling
|
||||
# ----------------------------------------------------------------------------
|
||||
def kaka2user(kaka):
|
||||
def info_from_cookie(kaka):
|
||||
logger.debug("KAKA: %s" % kaka)
|
||||
if kaka:
|
||||
cookie_obj = SimpleCookie(kaka)
|
||||
morsel = cookie_obj.get("idpauthn", None)
|
||||
if morsel:
|
||||
try:
|
||||
return IDP.cache.uid2user[morsel.value]
|
||||
key, ref = base64.b64decode(morsel.value).split(":")
|
||||
return IDP.cache.uid2user[key], ref
|
||||
except KeyError:
|
||||
return None
|
||||
return None, None
|
||||
else:
|
||||
logger.debug("No idpauthn cookie")
|
||||
return None
|
||||
return None, None
|
||||
|
||||
|
||||
def delete_cookie(environ, name):
|
||||
@ -693,9 +746,9 @@ def delete_cookie(environ, name):
|
||||
return None
|
||||
|
||||
|
||||
def set_cookie(name, _, value):
|
||||
def set_cookie(name, _, *args):
|
||||
cookie = SimpleCookie()
|
||||
cookie[name] = value
|
||||
cookie[name] = base64.b64encode(":".join(args))
|
||||
cookie[name]['path'] = "/"
|
||||
cookie[name]["expires"] = _expiration(5) # 5 minutes from now
|
||||
logger.debug("Cookie expires: %s" % cookie[name]["expires"])
|
||||
@ -769,7 +822,8 @@ def application(environ, start_response):
|
||||
|
||||
if kaka:
|
||||
logger.info("= KAKA =")
|
||||
user = kaka2user(kaka)
|
||||
user, authn_ref = info_from_cookie(kaka)
|
||||
environ["idp.authn_ref"] = authn_ref
|
||||
else:
|
||||
try:
|
||||
query = parse_qs(environ["QUERY_STRING"])
|
||||
@ -827,14 +881,21 @@ LOOKUP = TemplateLookup(directories=[ROOT + 'templates', ROOT + 'htdocs'],
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
import socket
|
||||
from idp_user import USERS
|
||||
from idp_user import EXTRA
|
||||
from wsgiref.simple_server import make_server
|
||||
|
||||
PORT = 8088
|
||||
|
||||
IDP = server.Server(sys.argv[1], cache=Cache()
|
||||
)
|
||||
AUTHN_BROKER = AuthnBroker()
|
||||
AUTHN_BROKER.add(authn_context_class_ref(PASSWORD),
|
||||
username_password_authn, 10,
|
||||
"http://%s" % socket.gethostname())
|
||||
AUTHN_BROKER.add(authn_context_class_ref(UNSPECIFIED),
|
||||
"", 0, "http://%s" % socket.gethostname())
|
||||
|
||||
IDP = server.Server(sys.argv[1], cache=Cache())
|
||||
IDP.ticket = {}
|
||||
|
||||
SRV = make_server('', PORT, application)
|
||||
|
Loading…
Reference in New Issue
Block a user