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:
Roland Hedberg
2013-04-28 09:54:35 +02:00
parent 3bc4cd1d3d
commit 7e19adb496
2 changed files with 138 additions and 76 deletions

View File

@@ -7,7 +7,8 @@
<form action="${action}" method="post"> <form action="${action}" method="post">
<input type="hidden" name="key" value="${key}"/> <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"> <div class="label">
<label for="login">Username</label> <label for="login">Username</label>

View File

@@ -17,16 +17,24 @@ from saml2 import BINDING_SOAP
from saml2 import BINDING_HTTP_REDIRECT from saml2 import BINDING_HTTP_REDIRECT
from saml2 import BINDING_HTTP_POST from saml2 import BINDING_HTTP_POST
from saml2 import time_util 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 get_post
from saml2.httputil import Redirect from saml2.httputil import Redirect
from saml2.httputil import Unauthorized from saml2.httputil import Unauthorized
from saml2.httputil import BadRequest from saml2.httputil import BadRequest
from saml2.httputil import ServiceError from saml2.httputil import ServiceError
from saml2.ident import Unknown 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.s_utils import PolicyError
from saml2.saml import AUTHN_PASSWORD
from saml2.sigver import verify_redirect_signature from saml2.sigver import verify_redirect_signature
logger = logging.getLogger("saml2.idp") logger = logging.getLogger("saml2.idp")
@@ -176,12 +184,14 @@ class Service(object):
# #
# return resp(self.environ, self.start_response) # return resp(self.environ, self.start_response)
def not_authn(self, key): def not_authn(self, key, requested_authn_context):
pass 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" REPOZE_ID_EQUIVALENT = "uid"
FORM_SPEC = """<form name="myform" method="post" action="%s"> 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): class SSO(Service):
def __init__(self, environ, start_response, user=None): def __init__(self, environ, start_response, user=None):
Service.__init__(self, environ, start_response, user) Service.__init__(self, environ, start_response, user)
@@ -204,7 +220,7 @@ class SSO(Service):
self.destination = None self.destination = None
self.req_info = None self.req_info = None
def verify(self, query, binding): def verify_request(self, query, binding):
""" """
:param query: The SAML query, transport encoded :param query: The SAML query, transport encoded
:param binding: Which binding the query came in over :param binding: Which binding the query came in over
@@ -244,7 +260,7 @@ class SSO(Service):
def do(self, query, binding_in, relay_state=""): def do(self, query, binding_in, relay_state=""):
try: try:
resp_args, _resp = self.verify(query, binding_in) resp_args, _resp = self.verify_request(query, binding_in)
except UnknownPrincipal, excp: except UnknownPrincipal, excp:
logger.error("UnknownPrincipal: %s" % (excp,)) logger.error("UnknownPrincipal: %s" % (excp,))
resp = ServiceError("UnknownPrincipal: %s" % (excp,)) resp = ServiceError("UnknownPrincipal: %s" % (excp,))
@@ -261,8 +277,10 @@ class SSO(Service):
if REPOZE_ID_EQUIVALENT: if REPOZE_ID_EQUIVALENT:
identity[REPOZE_ID_EQUIVALENT] = self.user identity[REPOZE_ID_EQUIVALENT] = self.user
try: try:
_resp = IDP.create_authn_response(identity, userid=self.user, _resp = IDP.create_authn_response(
authn=AUTHN, **resp_args) identity, userid=self.user,
authn=AUTHN_BROKER[self.environ["idp.authn_ref"]],
**resp_args)
except Exception, excp: except Exception, excp:
logger.error("Exception: %s" % (excp,)) logger.error("Exception: %s" % (excp,))
resp = ServiceError("Exception: %s" % (excp,)) resp = ServiceError("Exception: %s" % (excp,))
@@ -275,34 +293,29 @@ class SSO(Service):
logger.debug("HTTPargs: %s" % http_args) logger.debug("HTTPargs: %s" % http_args)
return self.response(self.binding_out, http_args) return self.response(self.binding_out, http_args)
def _authn(self, _dict): def _store_request(self, _dict):
logger.debug("_auth: %s" % _dict) logger.debug("_store_request: %s" % _dict)
if not self.user: key = sha1(_dict["SAMLRequest"]).hexdigest()
key = sha1("%s" % _dict).hexdigest() # store the AuthnRequest
IDP.ticket[key] = _dict IDP.ticket[key] = _dict
_resp = key return 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 redirect(self): def redirect(self):
""" This is the HTTP-redirect endpoint """ """ This is the HTTP-redirect endpoint """
logger.info("--- In SSO Redirect ---") logger.info("--- In SSO Redirect ---")
_info = self._authn(self.unpack_redirect()) _info = self.unpack_redirect()
if isinstance(_info, basestring):
return self.not_authn(_info)
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"], self.req_info = IDP.parse_authn_request(_info["SAMLRequest"],
BINDING_HTTP_REDIRECT) BINDING_HTTP_REDIRECT)
issuer = self.req_info.message.issuer.text _req = self.req_info.message
if "SigAlg" in _info and "Signature" in _info: # Signed request
issuer = _req.issuer.text
_certs = IDP.metadata.certs(issuer, "any", "signing") _certs = IDP.metadata.certs(issuer, "any", "signing")
verified_ok = False verified_ok = False
for cert in _certs: for cert in _certs:
@@ -313,6 +326,18 @@ class SSO(Service):
resp = BadRequest("Message signature verification failure") resp = BadRequest("Message signature verification failure")
return resp(self.environ, self.start_response) 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) return self.operation(_info, BINDING_HTTP_REDIRECT)
def post(self): def post(self):
@@ -320,19 +345,28 @@ class SSO(Service):
The HTTP-Post endpoint The HTTP-Post endpoint
""" """
logger.info("--- In SSO POST ---") logger.info("--- In SSO POST ---")
_dict = self.unpack_either() _info = self.unpack_either()
_resp = self._authn(_dict) self.req_info = IDP.parse_authn_request(
logger.debug("_req: %s" % _resp) _info["SAMLRequest"], BINDING_HTTP_POST)
if isinstance(_resp, basestring): _req = self.req_info.message
return self.not_authn(_resp) if self.user:
return self.operation(_resp, BINDING_HTTP_POST) 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): # def artifact(self):
# Can be either by HTTP_Redirect or HTTP_POST # # Can be either by HTTP_Redirect or HTTP_POST
_req = self._authn(self.unpack_either()) # _req = self._store_request(self.unpack_either())
if isinstance(_req, basestring): # if isinstance(_req, basestring):
return self.not_authn(_req) # return self.not_authn(_req)
return self.artifact_operation(_req) # return self.artifact_operation(_req)
def ecp(self): def ecp(self):
# The ECP interface # The ECP interface
@@ -368,21 +402,37 @@ class SSO(Service):
# === Authentication ==== # === 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", PASSWD = {"roland": "dianakra",
"babs": "howes", "babs": "howes",
"upper": "crust"} "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 Display the login form
""" """
query = parse_qs(environ["QUERY_STRING"])
logger.info("The login page") logger.info("The login page")
if cookie:
headers = [cookie]
else:
headers = [] headers = []
resp = Response(mako_template="login.mako", template_lookup=LOOKUP, resp = Response(mako_template="login.mako", template_lookup=LOOKUP,
@@ -390,10 +440,11 @@ def do_authentication(environ, start_response, cookie=None):
argv = { argv = {
"action": "/verify", "action": "/verify",
"came_from": query["came_from"][0],
"login": "", "login": "",
"password": "", "password": "",
"key": query["key"][0] "key": key,
"authn_reference": reference,
"redirect_uri": redirect_uri
} }
logger.info("do_authentication argv: %s" % argv) logger.info("do_authentication argv: %s" % argv)
return resp(environ, start_response, **argv) return resp(environ, start_response, **argv)
@@ -426,9 +477,10 @@ def do_verify(environ, start_response, _):
IDP.cache.uid2user[uid] = user IDP.cache.uid2user[uid] = user
IDP.cache.user2uid[user] = uid IDP.cache.user2uid[user] = uid
logger.debug("Register %s under '%s'" % (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"], kaka = set_cookie("idpauthn", "/", uid, query["authn_reference"][0])
query["came_from"][0], uid,
lox = "%s?id=%s&key=%s" % (query["redirect_uri"][0], uid,
query["key"][0]) query["key"][0])
logger.debug("Redirect => %s" % lox) logger.debug("Redirect => %s" % lox)
resp = Redirect(lox, headers=[kaka], content="text/html") resp = Redirect(lox, headers=[kaka], content="text/html")
@@ -663,19 +715,20 @@ class NIM(Service):
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
# Cookie handling # Cookie handling
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
def kaka2user(kaka): def info_from_cookie(kaka):
logger.debug("KAKA: %s" % kaka) logger.debug("KAKA: %s" % kaka)
if kaka: if kaka:
cookie_obj = SimpleCookie(kaka) cookie_obj = SimpleCookie(kaka)
morsel = cookie_obj.get("idpauthn", None) morsel = cookie_obj.get("idpauthn", None)
if morsel: if morsel:
try: try:
return IDP.cache.uid2user[morsel.value] key, ref = base64.b64decode(morsel.value).split(":")
return IDP.cache.uid2user[key], ref
except KeyError: except KeyError:
return None return None, None
else: else:
logger.debug("No idpauthn cookie") logger.debug("No idpauthn cookie")
return None return None, None
def delete_cookie(environ, name): def delete_cookie(environ, name):
@@ -693,9 +746,9 @@ def delete_cookie(environ, name):
return None return None
def set_cookie(name, _, value): def set_cookie(name, _, *args):
cookie = SimpleCookie() cookie = SimpleCookie()
cookie[name] = value cookie[name] = base64.b64encode(":".join(args))
cookie[name]['path'] = "/" cookie[name]['path'] = "/"
cookie[name]["expires"] = _expiration(5) # 5 minutes from now cookie[name]["expires"] = _expiration(5) # 5 minutes from now
logger.debug("Cookie expires: %s" % cookie[name]["expires"]) logger.debug("Cookie expires: %s" % cookie[name]["expires"])
@@ -769,7 +822,8 @@ def application(environ, start_response):
if kaka: if kaka:
logger.info("= KAKA =") logger.info("= KAKA =")
user = kaka2user(kaka) user, authn_ref = info_from_cookie(kaka)
environ["idp.authn_ref"] = authn_ref
else: else:
try: try:
query = parse_qs(environ["QUERY_STRING"]) query = parse_qs(environ["QUERY_STRING"])
@@ -827,14 +881,21 @@ LOOKUP = TemplateLookup(directories=[ROOT + 'templates', ROOT + 'htdocs'],
if __name__ == '__main__': if __name__ == '__main__':
import sys import sys
import socket
from idp_user import USERS from idp_user import USERS
from idp_user import EXTRA from idp_user import EXTRA
from wsgiref.simple_server import make_server from wsgiref.simple_server import make_server
PORT = 8088 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 = {} IDP.ticket = {}
SRV = make_server('', PORT, application) SRV = make_server('', PORT, application)