Skip to content

Commit 7e34c16

Browse files
author
Roland Hedberg
committed
Brought example IdP using repoze.who back.
1 parent 9b8ef8a commit 7e34c16

File tree

8 files changed

+481
-0
lines changed

8 files changed

+481
-0
lines changed

example/idp/README

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
passwords:
2+
3+
roland - friend
4+

example/idp/idp.py

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
#!/usr/bin/env python
2+
3+
import re
4+
import logging
5+
6+
#from cgi import parse_qs
7+
from urlparse import parse_qs
8+
from saml2.httputil import Unauthorized, NotFound, BadRequest
9+
from saml2.httputil import ServiceError
10+
from saml2.httputil import Response
11+
from saml2.pack import http_form_post_message
12+
from saml2.saml import AUTHN_PASSWORD
13+
from saml2 import server
14+
from saml2 import BINDING_HTTP_REDIRECT, BINDING_HTTP_POST
15+
from saml2 import time_util
16+
from Cookie import SimpleCookie
17+
18+
logger = logging.getLogger("saml2.IDP")
19+
20+
AUTHN = (AUTHN_PASSWORD, "http://www.example.com/login")
21+
22+
def _expiration(timeout, format=None):
23+
if timeout == "now":
24+
return time_util.instant(format)
25+
else:
26+
# validity time should match lifetime of assertions
27+
return time_util.in_a_while(minutes=timeout, format=format)
28+
29+
# -----------------------------------------------------------------------------
30+
def dict_to_table(ava, lev=0, width=1):
31+
txt = ['<table border=%s bordercolor="black">\n' % width]
32+
for prop, valarr in ava.items():
33+
txt.append("<tr>\n")
34+
if isinstance(valarr, basestring):
35+
txt.append("<th>%s</th>\n" % str(prop))
36+
try:
37+
txt.append("<td>%s</td>\n" % valarr.encode("utf8"))
38+
except AttributeError:
39+
txt.append("<td>%s</td>\n" % valarr)
40+
elif isinstance(valarr, list):
41+
index = 0
42+
num = len(valarr)
43+
for val in valarr:
44+
if not index:
45+
txt.append("<th rowspan=%d>%s</td>\n" % (len(valarr), prop))
46+
else:
47+
txt.append("<tr>\n")
48+
if isinstance(val, dict):
49+
txt.append("<td>\n")
50+
txt.extend(dict_to_table(val, lev+1, width-1))
51+
txt.append("</td>\n")
52+
else:
53+
try:
54+
txt.append("<td>%s</td>\n" % val.encode("utf8"))
55+
except AttributeError:
56+
txt.append("<td>%s</td>\n" % val)
57+
if num > 1:
58+
txt.append("</tr>\n")
59+
num -= 1
60+
index += 1
61+
elif isinstance(valarr, dict):
62+
txt.append("<th>%s</th>\n" % prop)
63+
txt.append("<td>\n")
64+
txt.extend(dict_to_table(valarr, lev+1, width-1))
65+
txt.append("</td>\n")
66+
txt.append("</tr>\n")
67+
txt.append('</table>\n')
68+
return txt
69+
70+
REPOZE_ID_EQUIVALENT = "uid"
71+
FORM_SPEC = """<form name="myform" method="post" action="%s">
72+
<input type="hidden" name="SAMLResponse" value="%s" />
73+
<input type="hidden" name="RelayState" value="%s" />
74+
</form>"""
75+
76+
77+
def sso(environ, start_response, user):
78+
""" Supposed to return a self issuing Form POST """
79+
#edict = dict_to_table(environ)
80+
#if logger: logger.info("Environ keys: %s" % environ.keys())
81+
logger.info("--- In SSO ---")
82+
query = None
83+
if "QUERY_STRING" in environ:
84+
if logger:
85+
logger.info("Query string: %s" % environ["QUERY_STRING"])
86+
query = parse_qs(environ["QUERY_STRING"])
87+
elif "s2repoze.qinfo" in environ:
88+
query = environ["s2repoze.qinfo"]
89+
90+
if not query:
91+
resp = Unauthorized('Unknown user')
92+
return resp(environ, start_response)
93+
94+
# base 64 encoded request
95+
# Assume default binding, that is HTTP-redirect
96+
req = IDP.parse_authn_request(query["SAMLRequest"][0])
97+
98+
if req is None:
99+
resp = ServiceError("Failed to parse the SAML request")
100+
return resp(environ, start_response)
101+
102+
logger.info("parsed OK")
103+
logger.info("%s" % req)
104+
105+
identity = dict(environ["repoze.who.identity"]["user"])
106+
logger.info("Identity: %s" % (identity,))
107+
userid = environ["repoze.who.identity"]['repoze.who.userid']
108+
if REPOZE_ID_EQUIVALENT:
109+
identity[REPOZE_ID_EQUIVALENT] = userid
110+
111+
# What's the binding ? ProtocolBinding
112+
if req.message.protocol_binding == BINDING_HTTP_REDIRECT:
113+
_binding = BINDING_HTTP_POST
114+
else:
115+
_binding = req.message.protocol_binding
116+
117+
try:
118+
resp_args = IDP.response_args(req.message, [_binding])
119+
except Exception:
120+
raise
121+
122+
if req.message.assertion_consumer_service_url:
123+
if req.message.assertion_consumer_service_url != resp_args["destination"]:
124+
# serious error on someones behalf
125+
logger.error("%s != %s" % (req.message.assertion_consumer_service_url,
126+
resp_args["destination"]))
127+
resp = BadRequest("ConsumerURL and return destination mismatch")
128+
raise resp(environ, start_response)
129+
130+
try:
131+
authn_resp = IDP.create_authn_response(identity, userid=userid,
132+
authn=AUTHN, **resp_args)
133+
except Exception, excp:
134+
logger.error("Exception: %s" % (excp,))
135+
raise
136+
137+
logger.info("AuthNResponse: %s" % authn_resp)
138+
139+
http_args = http_form_post_message(authn_resp, resp_args["destination"],
140+
relay_state=query["RelayState"][0],
141+
typ="SAMLResponse")
142+
143+
resp = Response(http_args["data"], headers=http_args["headers"])
144+
return resp(environ, start_response)
145+
146+
def whoami(environ, start_response, user):
147+
identity = environ["repoze.who.identity"].copy()
148+
for prop in ["login", "password"]:
149+
try:
150+
del identity[prop]
151+
except KeyError:
152+
continue
153+
response = Response(dict_to_table(identity))
154+
return response(environ, start_response)
155+
156+
def not_found(environ, start_response):
157+
"""Called if no URL matches."""
158+
resp = NotFound('Not Found')
159+
return resp(environ, start_response)
160+
161+
def not_authn(environ, start_response):
162+
if "QUERY_STRING" in environ:
163+
query = parse_qs(environ["QUERY_STRING"])
164+
logger.info("query: %s" % query)
165+
resp = Unauthorized('Unknown user')
166+
return resp(environ, start_response)
167+
168+
def slo(environ, start_response, user):
169+
""" Expects a HTTP-redirect logout request """
170+
171+
query = None
172+
if "QUERY_STRING" in environ:
173+
logger.info("Query string: %s" % environ["QUERY_STRING"])
174+
query = parse_qs(environ["QUERY_STRING"])
175+
176+
if not query:
177+
resp = Unauthorized('Unknown user')
178+
return resp(environ, start_response)
179+
180+
try:
181+
req_info = IDP.parse_logout_request(query["SAMLRequest"][0],
182+
BINDING_HTTP_REDIRECT)
183+
logger.info("LOGOUT request parsed OK")
184+
logger.info("REQ_INFO: %s" % req_info.message)
185+
except KeyError, exc:
186+
logger.info("logout request error: %s" % (exc,))
187+
resp = BadRequest('Request parse error')
188+
return resp(environ, start_response)
189+
190+
# look for the subject
191+
subject = req_info.subject_id()
192+
subject = subject.text.strip()
193+
logger.info("Logout subject: %s" % (subject,))
194+
195+
status = None
196+
197+
# Either HTTP-Post or HTTP-redirect is possible, prefer HTTP-Post.
198+
# Order matters
199+
bindings = [BINDING_HTTP_POST, BINDING_HTTP_REDIRECT]
200+
try:
201+
response = IDP.create_logout_response(req_info.message,
202+
bindings)
203+
binding, destination = IDP.pick_binding("single_logout_service",
204+
bindings, "spsso", response)
205+
206+
http_args = IDP.apply_binding(binding, "%s" % response, destination,
207+
query["RelayState"], response=True)
208+
209+
except Exception, exc:
210+
resp = BadRequest('%s' % exc)
211+
return resp(environ, start_response)
212+
213+
delco = delete_cookie(environ, "pysaml2idp")
214+
if delco:
215+
http_args["headers"].append(delco)
216+
217+
if binding == BINDING_HTTP_POST:
218+
resp = Response(http_args["data"], headers=http_args["headers"])
219+
else:
220+
resp = NotFound(http_args["data"], headers=http_args["headers"])
221+
return resp(environ, start_response)
222+
223+
def delete_cookie(environ, name):
224+
kaka = environ.get("HTTP_COOKIE", '')
225+
if kaka:
226+
cookie_obj = SimpleCookie(kaka)
227+
morsel = cookie_obj.get(name, None)
228+
cookie = SimpleCookie()
229+
cookie[name] = morsel
230+
cookie[name]["expires"] = \
231+
_expiration("now", "%a, %d-%b-%Y %H:%M:%S CET")
232+
return tuple(cookie.output().split(": ", 1))
233+
return None
234+
235+
# ----------------------------------------------------------------------------
236+
237+
# map urls to functions
238+
URLS = [
239+
(r'whoami$', whoami),
240+
(r'whoami/(.*)$', whoami),
241+
(r'sso$', sso),
242+
(r'sso/(.*)$', sso),
243+
(r'logout$', slo),
244+
(r'logout/(.*)$', slo),
245+
]
246+
247+
# ----------------------------------------------------------------------------
248+
249+
def application(environ, start_response):
250+
"""
251+
The main WSGI application. Dispatch the current request to
252+
the functions from above and store the regular expression
253+
captures in the WSGI environment as `myapp.url_args` so that
254+
the functions from above can access the url placeholders.
255+
256+
If nothing matches call the `not_found` function.
257+
258+
:param environ: The HTTP application environment
259+
:param start_response: The application to run when the handling of the
260+
request is done
261+
:return: The response as a list of lines
262+
"""
263+
user = environ.get("REMOTE_USER", "")
264+
kaka = environ.get("HTTP_COOKIE", '')
265+
if not user:
266+
user = environ.get("repoze.who.identity", "")
267+
268+
path = environ.get('PATH_INFO', '').lstrip('/')
269+
logger.info("<application> PATH: %s" % path)
270+
logger.info("Cookie: %s" % (kaka,))
271+
for regex, callback in URLS:
272+
if user:
273+
match = re.search(regex, path)
274+
if match is not None:
275+
try:
276+
environ['myapp.url_args'] = match.groups()[0]
277+
except IndexError:
278+
environ['myapp.url_args'] = path
279+
logger.info("callback: %s" % (callback,))
280+
return callback(environ, start_response, user)
281+
else:
282+
logger.info("-- No USER --")
283+
return not_authn(environ, start_response)
284+
return not_found(environ, start_response)
285+
286+
# ----------------------------------------------------------------------------
287+
288+
from repoze.who.config import make_middleware_with_config
289+
290+
APP_WITH_AUTH = make_middleware_with_config(application, {"here":"."},
291+
'./who.ini', log_file="repoze_who.log")
292+
293+
# ----------------------------------------------------------------------------
294+
295+
if __name__ == '__main__':
296+
import sys
297+
from wsgiref.simple_server import make_server
298+
299+
PORT = 8088
300+
301+
IDP = server.Server(sys.argv[1])
302+
SRV = make_server('localhost', PORT, APP_WITH_AUTH)
303+
print "IdP listening on port: %s" % PORT
304+
SRV.serve_forever()

example/idp/idp_conf.py.example

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from saml2 import BINDING_HTTP_REDIRECT
2+
from saml2.saml import NAME_FORMAT_URI
3+
4+
BASE = "http://localhost:8088/"
5+
6+
CONFIG={
7+
"entityid" : "urn:mace:umu.se:saml:roland:idp",
8+
"description": "My IDP",
9+
"service": {
10+
"idp": {
11+
"name" : "Rolands IdP",
12+
"endpoints" : {
13+
"single_sign_on_service" : [BASE+"sso"],
14+
"single_logout_service" : [(BASE+"logout",
15+
BINDING_HTTP_REDIRECT)],
16+
},
17+
"policy": {
18+
"default": {
19+
"lifetime": {"minutes":15},
20+
"attribute_restrictions": None, # means all I have
21+
"name_form": NAME_FORMAT_URI
22+
},
23+
"urn:mace:umu.se:saml:roland:sp": {
24+
"lifetime": {"minutes": 5},
25+
}
26+
},
27+
"subject_data": "./idp.subject.db",
28+
}
29+
},
30+
"debug" : 1,
31+
"key_file" : "pki/mykey.pem",
32+
"cert_file" : "pki/mycert.pem",
33+
"metadata" : {
34+
"local": ["../sp/sp.xml"],
35+
},
36+
"organization": {
37+
"display_name": "Rolands Identiteter",
38+
"name": "Rolands Identiteter",
39+
"url": "http://www.example.com",
40+
},
41+
# This database holds the map between a subjects local identifier and
42+
# the identifier returned to a SP
43+
#"xmlsec_binary": "/usr/local/bin/xmlsec1",
44+
"attribute_map_dir" : "../attributemaps",
45+
"logger": {
46+
"rotating": {
47+
"filename": "idp.log",
48+
"maxBytes": 100000,
49+
"backupCount": 5,
50+
},
51+
"loglevel": "debug",
52+
}
53+
}

example/idp/idp_user.ini

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[roland]
2+
surname=Hedberg
3+
givenName=Roland
4+
eduPersonAffiliation=staff
5+
uid=rohe0002
6+
7+
[ozzie]
8+
surname=Guillen
9+
givenName=Ozzie
10+
eduPersonAffiliation=affiliate
11+
12+
[derek]
13+
surname=Jeter
14+
givenName=Derek
15+
eduPersonAffiliation=affiliate
16+
17+
[ichiro]
18+
surname=Suzuki
19+
givenName=Ischiro
20+
eduPersonAffiliation=affiliate
21+
22+
[ryan]
23+
surname=Howard
24+
givenName=Ryan
25+
eduPersonAffiliation=affiliate

example/idp/passwd

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
roland:Jek7qtYXouxmM
2+
ozzie:wT390u9XwBFaU
3+
derek:efNb53YcncbRI
4+
ryan:YlIhvZ6Rdt6fA
5+
ischiro:wgMhJvmkQgMGs

0 commit comments

Comments
 (0)