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 ()
0 commit comments