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:
@@ -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>
|
||||||
|
@@ -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)
|
||||||
|
Reference in New Issue
Block a user