Updated IdP example2

This commit is contained in:
Roland Hedberg 2013-02-01 13:49:41 +01:00
parent d1523b6d5e
commit 7f777c2c38
8 changed files with 173 additions and 147 deletions

View File

@ -41,7 +41,7 @@ CONFIG={
# This database holds the map between a subjects local identifier and
# the identifier returned to a SP
#"xmlsec_binary": "/usr/local/bin/xmlsec1",
"attribute_map_dir" : "./attributemaps",
"attribute_map_dir" : "../attributemaps",
"logger": {
"rotating": {
"filename": "idp.log",

View File

@ -1,4 +1,5 @@
#!/usr/bin/env python
import base64
import re
import logging
@ -9,23 +10,24 @@ from hashlib import sha1
from urlparse import parse_qs
from Cookie import SimpleCookie
from saml2 import server, BINDING_HTTP_ARTIFACT
from saml2 import server
from saml2 import BINDING_HTTP_ARTIFACT
from saml2 import BINDING_URI
from saml2 import BINDING_PAOS
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
from saml2.httputil import Response, NotFound
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
from saml2.s_utils import rndstr, UnknownPrincipal, UnsupportedBinding
from saml2.s_utils import PolicyError
from saml2.saml import AUTHN_PASSWORD
from saml2.saml import NAMEID_FORMAT_PERSISTENT
from saml2.saml import NameID
logger = logging.getLogger("saml2.idp")
@ -113,14 +115,15 @@ def dict2list_of_tuples(d):
# -----------------------------------------------------------------------------
def _operation(environ, start_response, user, _dict, func, binding):
def _operation(environ, start_response, user, _dict, func, binding,
**kwargs):
logger.debug("_operation: %s" % _dict)
if not _dict:
resp = BadRequest('Error parsing request or no request')
return resp(environ, start_response)
else:
return func(environ, start_response, user, _dict["SAMLRequest"],
binding, _dict["RelayState"])
binding, _dict["RelayState"], **kwargs)
def _artifact_oper(environ, start_response, user, _dict, func):
if not _dict:
@ -133,6 +136,13 @@ def _artifact_oper(environ, start_response, user, _dict, func):
return func(environ, start_response, user, request,
BINDING_HTTP_ARTIFACT, _dict["RelayState"])
def _response(environ, start_response, binding, http_args):
if binding == BINDING_HTTP_ARTIFACT:
resp = Redirect()
else:
resp = Response(http_args["data"], headers=http_args["headers"])
return resp(environ, start_response)
# -----------------------------------------------------------------------------
AUTHN = (AUTHN_PASSWORD, "http://lingon.catalogix.se/login")
@ -146,7 +156,8 @@ FORM_SPEC = """<form name="myform" method="post" action="%s">
# === Single log in ====
# -----------------------------------------------------------------------------
def _sso(environ, start_response, user, query, binding, relay_state=""):
def _sso(environ, start_response, user, query, binding, relay_state="",
response_bindings=None):
logger.info("--- In SSO ---")
logger.debug("user: %s" % user)
@ -161,7 +172,16 @@ def _sso(environ, start_response, user, query, binding, relay_state=""):
logger.info("%s" % req_info)
_authn_req = req_info.message
resp_args = IDP.response_args(_authn_req)
try:
resp_args = IDP.response_args(_authn_req)
except UnknownPrincipal, excp:
#IDP.create_error_response()
resp = ServiceError("UnknownPrincipal: %s" % (excp,))
return resp(environ, start_response)
except UnsupportedBinding, excp:
#IDP.create_error_response()
resp = ServiceError("UnsupportedBinding: %s" % (excp,))
return resp(environ, start_response)
identity = USERS[user]
logger.info("Identity: %s" % (identity,))
@ -178,12 +198,13 @@ def _sso(environ, start_response, user, query, binding, relay_state=""):
logger.info("AuthNResponse: %s" % authn_resp)
binding, destination = IDP.pick_binding("assertion_consumer_service",
bindings=response_bindings,
entity_id=_authn_req.issuer.text)
logger.debug("Binding: %s, destination: %s" % (binding, destination))
http_args = IDP.apply_binding(binding, "%s" % authn_resp, destination,
relay_state, response=True)
resp = Response(http_args["data"], headers=http_args["headers"])
return resp(environ, start_response)
return _response(environ, start_response, binding, http_args)
def sso(environ, start_response, user):
""" This is the HTTP-redirect endpoint """
@ -222,6 +243,36 @@ def sso_art(environ, start_response, user):
del IDP.ticket[_dict["key"]]
return _artifact_oper(environ, start_response, user, _request, _sso)
def sso_ecp(environ, start_response, user):
# The ECP interface
logger.info("--- ECP SSO ---")
logger.debug("ENVIRON: %s" % environ)
resp = None
try:
authz_info = environ["HTTP_AUTHORIZATION"]
if authz_info.startswith("Basic "):
_info = base64.b64decode(authz_info[6:])
logger.debug("Authz_info: %s" % _info)
try:
(user,passwd) = _info.split(":")
if PASSWD[user] != passwd:
resp = Unauthorized()
except ValueError:
resp = Unauthorized()
else:
resp = Unauthorized()
except KeyError:
resp = Unauthorized()
if resp:
return resp(environ, start_response)
_dict = unpack_soap(environ)
# Basic auth ?!
return _operation(environ, start_response, user, _dict, _sso, BINDING_SOAP,
response_bindings=[BINDING_PAOS])
# -----------------------------------------------------------------------------
# === Authentication ====
# -----------------------------------------------------------------------------
@ -286,12 +337,10 @@ def do_authentication(environ, start_response, cookie=None):
def verify_username_and_password(dic):
global PASSWD
# verify username and password
for user, pwd in PASSWD:
if user == dic["login"][0]:
if pwd == dic["password"][0]:
return True, user
return False, ""
if PASSWD[dic["login"][0]] == dic["password"][0]:
return True, dic["login"][0]
else:
return False, ""
def do_verify(environ, start_response, _user):
@ -331,6 +380,7 @@ def _slo(environ, start_response, _, request, binding, relay_state=""):
try:
req_info = IDP.parse_logout_request(request, binding)
except Exception, exc:
logger.error("Bad request: %s" % exc)
resp = BadRequest("%s" % exc)
return resp(environ, start_response)
@ -342,6 +392,7 @@ def _slo(environ, start_response, _, request, binding, relay_state=""):
try:
IDP.remove_authn_statements(msg.name_id)
except KeyError,exc:
logger.error("ServiceError: %s" % exc)
resp = ServiceError("%s" % exc)
return resp(environ, start_response)
@ -350,6 +401,7 @@ def _slo(environ, start_response, _, request, binding, relay_state=""):
try:
hinfo = IDP.apply_binding(binding, "%s" % resp, "", relay_state)
except Exception, exc:
logger.error("ServiceError: %s" % exc)
resp = ServiceError("%s" % exc)
return resp(environ, start_response)
@ -410,9 +462,9 @@ def not_found(environ, start_response):
return ['Not Found']
PASSWD = [("roland", "dianakra"),
("babs", "howes"),
("upper", "crust")]
PASSWD = {"roland": "dianakra",
"babs": "howes",
"upper": "crust"}
# ----------------------------------------------------------------------------
@ -433,21 +485,24 @@ def kaka2user(kaka):
def _mni(environ, start_response, user, query, binding, relay_state=""):
logger.info("--- Manage Name ID Service ---")
req = IDP.parse_manage_name_id_response(query, binding)
req = IDP.parse_manage_name_id_request(query, binding)
request = req.message
# Do the necessary stuff
in_response_to = req.message.id
name_id = NameID(format=NAMEID_FORMAT_PERSISTENT, text="foobar")
name_id = IDP.ident.handle_manage_name_id_request(request.name_id,
request.new_id,
request.new_encrypted_id,
request.terminate)
info = IDP.response_args(req)
_resp = IDP.create_manage_name_id_response(name_id, **info)
logger.debug("New NameID: %s" % name_id)
_resp = IDP.create_manage_name_id_response(request)
# It's using SOAP binding
hinfo = IDP.apply_binding(binding, "%s" % _resp, "", relay_state,
"SAMLResponse")
response=True)
resp = Response(hinfo["data"],
headers=dict2list_of_tuples(hinfo["headers"]))
resp = Response(hinfo["data"], headers=hinfo["headers"])
return resp(environ, start_response)
def mni(environ, start_response, user):
@ -478,25 +533,29 @@ def mni_art(environ, start_response, user):
# === Assertion ID request ===
# ----------------------------------------------------------------------------
# Only SOAP binding
# Only URI binding
def assertion_id_request(environ, start_response, user=None):
logger.info("--- Assertion ID Service ---")
_dict = unpack_soap(environ)
_binding = BINDING_SOAP
_binding = BINDING_URI
if not _dict:
_dict = unpack_artifact(environ)
logger.debug("INPUT: %s" % _dict)
# Presently only HTTP GET is supported
if "ID" in _dict:
aid = _dict["ID"]
else:
resp = BadRequest("Missing or faulty request")
return resp(environ, start_response)
req_info = IDP.parse_assertion_id_request("%s" % _dict["SAMLRequest"],
_binding)
try:
assertion = IDP.create_assertion_id_request_response(aid)
except Unknown:
resp = NotFound(aid)
return resp(environ, start_response)
asids = [x.text for x in req_info.message.assertion_id_ref]
resp_args = IDP.response_args(req_info.message, _binding, "spsso")
response = IDP.create_assertion_id_request_response(asids, **resp_args)
hinfo = IDP.apply_binding(_binding, "%s" % response, "","",response=True)
hinfo = IDP.apply_binding(_binding, "%s" % assertion, response=True)
logger.debug("HINFO: %s" % hinfo)
resp = Response(hinfo["data"], headers=hinfo["headers"])
return resp(environ, start_response)
@ -559,6 +618,45 @@ def authn_query_service(environ, start_response, user=None):
return resp(environ, start_response)
# ----------------------------------------------------------------------------
# === Attribute query service ===
# ----------------------------------------------------------------------------
# Only SOAP binding
def attribute_query_service(environ, start_response, user=None):
"""
:param environ: Execution environment
:param start_response: Function to start the response with
"""
logger.info("--- Attribute Query Service ---")
_dict = unpack_soap(environ)
_binding = BINDING_SOAP
if not _dict:
resp = BadRequest("Missing or faulty request")
return resp(environ, start_response)
_req = IDP.parse_attribute_query("%s" % _dict["SAMLRequest"], _binding)
_query = _req.message
name_id = _query.subject.name_id
uid = IDP.ident.find_local_id(name_id)
logger.debug("Local uid: %s" % uid)
identity = EXTRA[uid]
# Comes in over SOAP so only need to construct the response
args = IDP.response_args(_query, [BINDING_SOAP])
msg = IDP.create_attribute_response(identity, destination="",
name_id=name_id, **args)
logger.debug("response: %s" % msg)
hinfo = IDP.apply_binding(_binding, "%s" % msg, "","",response=True)
resp = Response(hinfo["data"], headers=hinfo["headers"])
return resp(environ, start_response)
# ----------------------------------------------------------------------------
# Name ID Mapping service
# When an entity that shares an identifier for a principal with an identity
@ -569,11 +667,12 @@ def authn_query_service(environ, start_response, user=None):
def _nim(environ, start_response, user, query, binding, relay_state=""):
req = IDP.parse_manage_name_id_response(query, binding)
req = IDP.parse_name_id_mapping_request(query, binding)
request = req.message
# Do the necessary stuff
try:
name_id = IDP.ident.handle_name_id_mapping_request()
name_id = IDP.ident.handle_name_id_mapping_request(request.name_id,
request.name_id_policy)
except Unknown:
resp = BadRequest("Unknown entity")
return resp(environ, start_response)
@ -581,56 +680,19 @@ def _nim(environ, start_response, user, query, binding, relay_state=""):
resp = BadRequest("Unknown entity")
return resp(environ, start_response)
info = IDP.response_args(req)
_resp = IDP.create_manage_name_id_response(name_id, **info)
info = IDP.response_args(request)
_resp = IDP.create_name_id_mapping_response(name_id, **info)
# It's using SOAP binding
# Only SOAP
hinfo = IDP.apply_binding(binding, "%s" % _resp, "", "", response=True)
resp = Response(hinfo["data"],
headers=dict2list_of_tuples(hinfo["headers"]))
resp = Response(hinfo["data"], headers=hinfo["headers"])
return resp(environ, start_response)
def nim(environ, start_response, user):
""" Expects a HTTP-redirect logout request """
_dict = unpack_redirect(environ)
if not _dict:
resp = Unauthorized('Unknown user')
return resp(environ, start_response)
else:
return _mni(environ, start_response, user, _dict["SAMLRequest"],
BINDING_HTTP_REDIRECT, _dict["RelayState"])
def nim_post(environ, start_response, user):
""" Expects a HTTP-POST logout request """
_dict = unpack_post(environ)
if not _dict:
resp = Unauthorized('Unknown user')
return resp(environ, start_response)
else:
return _mni(environ, start_response, user, _dict["SAMLRequest"],
BINDING_HTTP_REDIRECT, _dict["RelayState"])
def nim_soap(environ, start_response, user):
_dict = unpack_soap(environ)
return _operation(environ, start_response, user, _dict, _nim, BINDING_SOAP)
_dict = unpack_post(environ)
if not _dict:
resp = Unauthorized('Unknown user')
return resp(environ, start_response)
else:
return _mni(environ, start_response, user, _dict["SAMLRequest"],
BINDING_HTTP_REDIRECT, _dict["RelayState"])
def nim_art(environ, start_response, user):
# Could be by HTTP_REDIRECT or HTTP_POST
_dict = unpack_redirect(environ)
if not _dict:
_dict = unpack_post(environ)
return _artifact_oper(environ, start_response, user, _dict, _mni)
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
@ -689,21 +751,17 @@ AUTHN_URLS = [
(r'mni/soap$', mni_soap),
(r'mni/soap/(.*)$', mni_soap),
# nim
(r'nim/post$', nim_post),
(r'nim/post/(.*)$', nim_post),
(r'nim/redirect$', nim),
(r'nim/redirect/(.*)$', nim),
(r'nim/art$', nim_art),
(r'nim/art/(.*)$', nim_art),
(r'nim/soap$', nim_soap),
(r'nim/soap/(.*)$', nim_soap),
(r'nim$', nim_soap),
(r'nim/(.*)$', nim_soap),
#
(r'aqs$', authn_query_service)
(r'aqs$', authn_query_service),
(r'attr$', attribute_query_service)
]
NON_AUTHN_URLS = [
(r'login?(.*)$', do_authentication),
(r'verify?(.*)$', do_verify),
(r'sso/ecp$', sso_ecp),
]
# ----------------------------------------------------------------------------
@ -776,6 +834,7 @@ LOOKUP = TemplateLookup(directories=[ROOT + 'templates', ROOT + 'htdocs'],
if __name__ == '__main__':
import sys
from idp_user import USERS
from idp_user import EXTRA
from wsgiref.simple_server import make_server
PORT = 8088

View File

@ -1,4 +1,6 @@
from saml2 import BINDING_HTTP_REDIRECT
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from saml2 import BINDING_HTTP_REDIRECT, BINDING_URI
from saml2 import BINDING_HTTP_ARTIFACT
from saml2 import BINDING_HTTP_POST
from saml2 import BINDING_SOAP
@ -27,8 +29,7 @@ CONFIG={
"aa": {
"endpoints" : {
"attribute_service": [
("%s/attr/post" % BASE, BINDING_HTTP_POST),
("%s/attr/soap" % BASE, BINDING_SOAP)
("%s/attr" % BASE, BINDING_SOAP)
]
},
"name_id_format": [NAMEID_FORMAT_TRANSIENT,
@ -47,7 +48,8 @@ CONFIG={
"single_sign_on_service" : [
("%s/sso/redirect" % BASE, BINDING_HTTP_REDIRECT),
("%s/sso/post" % BASE, BINDING_HTTP_POST),
("%s/sso/art" % BASE, BINDING_HTTP_ARTIFACT)
("%s/sso/art" % BASE, BINDING_HTTP_ARTIFACT),
("%s/sso/ecp" % BASE, BINDING_SOAP)
],
"single_logout_service": [
("%s/slo/soap" % BASE, BINDING_SOAP),
@ -58,7 +60,7 @@ CONFIG={
("%s/ars" % BASE, BINDING_SOAP)
],
"assertion_id_request_service": [
("%s/airs" % BASE, BINDING_SOAP)
("%s/airs" % BASE, BINDING_URI)
],
"manage_name_id_service":[
("%s/mni/soap" % BASE, BINDING_SOAP),
@ -67,10 +69,7 @@ CONFIG={
("%s/mni/art" % BASE, BINDING_HTTP_ARTIFACT)
],
"name_id_mapping_service":[
("%s/nim/soap" % BASE, BINDING_SOAP),
("%s/nim/post" % BASE, BINDING_HTTP_POST),
("%s/nim/redirect" % BASE, BINDING_HTTP_REDIRECT),
("%s/nim/art" % BASE, BINDING_HTTP_ARTIFACT)
("%s/nim" % BASE, BINDING_SOAP),
],
},
"policy": {

View File

@ -5,24 +5,22 @@ USERS = {
"eduPersonAffiliation": "staff",
"uid": "rohe0002"
},
"ozzie": {
"surname": "Guillen",
"babs": {
"surname": "Babs",
"givenName": "Ozzie",
"eduPersonAffiliation": "affiliate"
},
"derek": {
"upper": {
"surname": "Jeter",
"givenName": "Derek",
"eduPersonAffiliation": "affiliate"
},
"ichiro": {
"surname": "Suzuki",
"givenName": "Ischiro",
"eduPersonAffiliation": "affiliate"
},
"ryan": {
"surname": "Howard",
"givenName": "Ryan",
"eduPersonAffiliation": "affiliate"
}
}
EXTRA = {
"roland": {
"eduPersonEntitlement" : "urn:mace:swamid.se:foo:bar",
"schacGender": "male",
"schacUserPresenceID": "skype:pepe.perez"
}
}

View File

@ -1,20 +1,5 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2006 Google Inc.
# Copyright (C) 2009 Umeå University
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Contains base classes representing SAML elements.
@ -32,21 +17,6 @@
provides methods and functions to convert SAML classes to and from strings.
"""
# try:
# # lxml: best performance for XML processing
# import lxml.etree as ET
# except ImportError:
# try:
# # Python 2.5+: batteries included
# import xml.etree.cElementTree as ET
# except ImportError:
# try:
# # Python <2.5: standalone ElementTree install
# import elementtree.cElementTree as ET
# except ImportError:
# raise ImportError, "lxml or ElementTree are not installed, "\
# +"see http://codespeak.net/lxml "\
# +"or http://effbot.org/zone/element-index.htm"
import logging
from saml2.validate import valid_instance