1
|
import datetime
|
2
|
import os
|
3
|
import urllib2
|
4
|
|
5
|
import lasso
|
6
|
|
7
|
from urlparse import parse_qs
|
8
|
|
9
|
from mandaye import config, utils
|
10
|
from mandaye.saml import saml2utils
|
11
|
from mandaye.auth.authform import AuthForm
|
12
|
from mandaye.exceptions import MandayeSamlException, ImproperlyConfigured
|
13
|
from mandaye.response import _302, _401
|
14
|
from mandaye.log import logger
|
15
|
from mandaye.http import HTTPResponse, HTTPHeader
|
16
|
|
17
|
"""
|
18
|
Mandaye saml2 authentification support
|
19
|
|
20
|
To use it you must set the following options into your
|
21
|
virtual host :
|
22
|
* saml2_idp_metadata: a link to your idp metadata
|
23
|
* saml2_signature_public_key: a path to your public key
|
24
|
* saml2_signature_private_key: a path to your private key
|
25
|
|
26
|
Optional options :
|
27
|
* saml2_sp_logout_url: the url to logout the service provider
|
28
|
* saml2_authnresp_binding: only post is supported for now
|
29
|
* saml2_authnreq_http_method: only http_redirect at the moment
|
30
|
* saml2_name_identifier_format: only persistent at the moment
|
31
|
* saml2_metadata_url: saml end point of the metadata
|
32
|
* saml2_single_sign_on_post_url: saml end point of single sign on post
|
33
|
* saml2_single_logout_url: saml end point of logout
|
34
|
* saml2_single_logout_return_url: saml end point of the single logout return
|
35
|
"""
|
36
|
|
37
|
# XXX: remove this for the 1.0. Keep it only for compability reasons.
|
38
|
END_POINTS_PATH = {
|
39
|
'metadata': '/mandaye/metadata',
|
40
|
'single_sign_on_post': '/mandaye/singleSignOnPost',
|
41
|
'single_logout': '/mandaye/singleLogout',
|
42
|
'single_logout_return': '/mandaye/singleLogoutReturn',
|
43
|
}
|
44
|
|
45
|
class SAML2Auth(AuthForm):
|
46
|
""" SAML 2 authentification
|
47
|
"""
|
48
|
|
49
|
def __init__(self, env, mapper):
|
50
|
""" saml2_config: saml 2 config module
|
51
|
env: WSGI environment
|
52
|
mapper: mapper's module like mandaye.mappers.linuxfr
|
53
|
"""
|
54
|
self.env = env
|
55
|
self.END_POINTS_PATH = {
|
56
|
'metadata': self.env['mandaye.config'].
|
57
|
get('saml2_metadata_url', '/mandaye/metadata'),
|
58
|
'single_sign_on_post': self.env['mandaye.config'].\
|
59
|
get('saml2_single_sign_on_post_url', '/mandaye/singleSignOnPost'),
|
60
|
'single_logout': self.env['mandaye.config'].\
|
61
|
get('saml2_single_logout_url', '/mandaye/singleLogout'),
|
62
|
'single_logout_return': self.env['mandaye.config'].\
|
63
|
get('saml2_single_logout_return_url', '/mandaye/singleLogoutReturn'),
|
64
|
}
|
65
|
for param in ('saml2_idp_metadata',
|
66
|
'saml2_signature_public_key',
|
67
|
'saml2_signature_private_key'):
|
68
|
if not self.env['mandaye.config'].has_key(param):
|
69
|
err = 'you must set %s option in vhost : %s' % \
|
70
|
(param, self.env['mandaye.vhost'])
|
71
|
logger.error(err)
|
72
|
raise ImproperlyConfigured, err
|
73
|
public_key = self._get_file_content(
|
74
|
self.env['mandaye.config']['saml2_signature_public_key']
|
75
|
)
|
76
|
private_key = self._get_file_content(
|
77
|
self.env['mandaye.config']['saml2_signature_private_key']
|
78
|
)
|
79
|
self.config = {
|
80
|
'saml2_idp_metadata': self.env['mandaye.config']['saml2_idp_metadata'],
|
81
|
'saml2_signature_public_key': public_key,
|
82
|
'saml2_signature_private_key': private_key,
|
83
|
'saml2_sp_logout_url': self.env['mandaye.config'].get('saml2_sp_logout_url'),
|
84
|
'saml2_authnresp_binding': lasso.SAML2_METADATA_BINDING_POST,
|
85
|
'saml2_authnreq_http_method': lasso.HTTP_METHOD_REDIRECT,
|
86
|
'saml2_name_identifier_format': lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT
|
87
|
}
|
88
|
|
89
|
self.metadata_map = (
|
90
|
('AssertionConsumerService',
|
91
|
lasso.SAML2_METADATA_BINDING_POST ,
|
92
|
self.END_POINTS_PATH['single_sign_on_post']
|
93
|
),
|
94
|
('SingleLogoutService',
|
95
|
lasso.SAML2_METADATA_BINDING_REDIRECT,
|
96
|
self.END_POINTS_PATH['single_logout'],
|
97
|
self.END_POINTS_PATH['single_logout_return']),
|
98
|
)
|
99
|
self.metadata_options = { 'key': public_key }
|
100
|
super(SAML2Auth, self).__init__(env, mapper)
|
101
|
|
102
|
def _get_file_content(self, path):
|
103
|
if not os.path.isabs(path):
|
104
|
path = os.path.join(config.config_root,
|
105
|
path)
|
106
|
if not os.path.exists(path):
|
107
|
err = "%s: file %s doesn't exist" % \
|
108
|
(path, self.env['mandaye.vhost'])
|
109
|
logger.error(err)
|
110
|
raise ImproperlyConfigured, err
|
111
|
with open(path, 'r') as f:
|
112
|
content = f.read()
|
113
|
return content
|
114
|
|
115
|
def get_default_mapping(self):
|
116
|
default_mapping = super(SAML2Auth, self).get_default_mapping()
|
117
|
default_mapping.extend([
|
118
|
{
|
119
|
'path': r'%s$' % self.END_POINTS_PATH['metadata'],
|
120
|
'method': 'GET',
|
121
|
'response': {'filter': self.metadata,}
|
122
|
},
|
123
|
{
|
124
|
'path': r'%s$' % self.END_POINTS_PATH['single_sign_on_post'],
|
125
|
'method': 'POST',
|
126
|
'response': {'auth': 'single_sign_on_post'}
|
127
|
},
|
128
|
{
|
129
|
'path': r'%s$' % self.END_POINTS_PATH['single_logout'],
|
130
|
'method': 'GET',
|
131
|
'response': {'auth': 'single_logout',}
|
132
|
},
|
133
|
{
|
134
|
'path': r'%s$' % self.END_POINTS_PATH['single_logout_return'],
|
135
|
'method': 'GET',
|
136
|
'response': {'auth': 'single_logout_return',}
|
137
|
},
|
138
|
])
|
139
|
return default_mapping
|
140
|
|
141
|
def local_logout(self, env, values, request, response):
|
142
|
logger.info('SP logout initiated by Mandaye')
|
143
|
# Mandaye logout
|
144
|
self.logout(env, values, request, response)
|
145
|
|
146
|
next_url = None
|
147
|
qs = parse_qs(env['QUERY_STRING'])
|
148
|
if qs.has_key('RelayState'):
|
149
|
next_url = qs['RelayState'][0]
|
150
|
elif qs.has_key('next_url'):
|
151
|
next_url = qs['next_url'][0]
|
152
|
elif values.has_key('next_url'):
|
153
|
next_url = values['next_url']
|
154
|
|
155
|
req_cookies = request.cookies
|
156
|
if not self.config['saml2_sp_logout_url']:
|
157
|
logger.warning('saml2_sp_logout_url not set into vhost configuration only removing cookies')
|
158
|
for cookie in req_cookies.values():
|
159
|
cookie['expires'] = 'Thu, 01 Jan 1970 00:00:01 GMT'
|
160
|
cookie['path'] = '/'
|
161
|
if next_url:
|
162
|
return _302(next_url, req_cookies)
|
163
|
else:
|
164
|
return _302('/', req_cookies)
|
165
|
return _302(self.config['saml2_sp_logout_url'], req_cookies)
|
166
|
|
167
|
def _get_idp_metadata_file_path(self):
|
168
|
metadata_file_path = None
|
169
|
if self.config['saml2_idp_metadata']:
|
170
|
metadata_file_path = os.path.join(config.data_dir,
|
171
|
self.config['saml2_idp_metadata'].\
|
172
|
replace('://', '_').\
|
173
|
replace('/', '_')
|
174
|
)
|
175
|
if not os.path.isfile(metadata_file_path):
|
176
|
try:
|
177
|
response = urllib2.urlopen(self.config['saml2_idp_metadata'])
|
178
|
metadata = response.read()
|
179
|
response.close()
|
180
|
except Exception, e:
|
181
|
logger.error("Unable to fetch metadata %r: %r",
|
182
|
self.config['saml2_idp_metadata'], str(e))
|
183
|
raise MandayeSamlException("Unable to find metadata: %s" % str(e))
|
184
|
metadata_file = open(metadata_file_path, 'w')
|
185
|
metadata_file.write(metadata)
|
186
|
metadata_file.close()
|
187
|
return metadata_file_path
|
188
|
|
189
|
def _get_metadata(self, env):
|
190
|
url_prefix = env['mandaye.scheme'] + '://' + env['HTTP_HOST']
|
191
|
metadata_path = self.END_POINTS_PATH['metadata']
|
192
|
single_sign_on_post_path = \
|
193
|
self.END_POINTS_PATH['single_sign_on_post']
|
194
|
metagen = saml2utils.Saml2Metadata(url_prefix + metadata_path,
|
195
|
url_prefix = url_prefix)
|
196
|
metagen.add_sp_descriptor(self.metadata_map, self.metadata_options)
|
197
|
return str(metagen)
|
198
|
|
199
|
def sso(self, env, values, request, response):
|
200
|
req_cookies = request.cookies
|
201
|
if env['beaker.session'].has_key('unique_id'):
|
202
|
env['beaker.session']['unique_id'] = None
|
203
|
for cookie in req_cookies.values():
|
204
|
if cookie.key != 'beaker.session.id':
|
205
|
cookie['expires'] = 'Thu, 01 Jan 1970 00:00:01 GMT'
|
206
|
cookie['path'] = '/'
|
207
|
qs = parse_qs(env['QUERY_STRING'])
|
208
|
target_idp = self.config['saml2_idp_metadata']
|
209
|
metadata_file_path = self._get_idp_metadata_file_path()
|
210
|
if not metadata_file_path:
|
211
|
raise MandayeSamlException("sso: unable to load provider")
|
212
|
logger.debug('sso: target_idp is %r', target_idp)
|
213
|
logger.debug('sso: metadata url is %r', self.config['saml2_idp_metadata'])
|
214
|
logger.debug('sso: mandaye metadata are %r', self._get_metadata(env))
|
215
|
server = lasso.Server.newFromBuffers(self._get_metadata(env),
|
216
|
self.config['saml2_signature_private_key'])
|
217
|
if not server:
|
218
|
raise MandayeSamlException("sso: error creating server object.")
|
219
|
logger.debug('sso: mandaye server object created')
|
220
|
server.addProvider(lasso.PROVIDER_ROLE_IDP, metadata_file_path)
|
221
|
login = lasso.Login(server)
|
222
|
if not login:
|
223
|
raise MandayeSamlException("sso: error creating login object.")
|
224
|
http_method = self.config['saml2_authnreq_http_method']
|
225
|
try:
|
226
|
login.initAuthnRequest(target_idp, http_method)
|
227
|
except lasso.Error, error:
|
228
|
raise MandayeSamlException("sso: Error initiating request %s" % lasso.strError(error[0]))
|
229
|
login.request.nameIDPolicy.format = self.config['saml2_name_identifier_format']
|
230
|
login.request.nameIDPolicy.allowCreate = True
|
231
|
login.request.nameIDPolicy.spNameQualifier = None
|
232
|
login.request.protocolBinding = self.config['saml2_authnresp_binding']
|
233
|
if qs.has_key('next_url'):
|
234
|
login.msgRelayState = qs['next_url'][0]
|
235
|
try:
|
236
|
login.buildAuthnRequestMsg()
|
237
|
except lasso.Error, error:
|
238
|
raise MandayeSamlException("sso: Error initiating request %s" % lasso.strError(error[0]))
|
239
|
logger.debug('sso: set request id in session %s' % login.request.iD)
|
240
|
env['beaker.session']['request_id'] = login.request.iD
|
241
|
env['beaker.session'].save()
|
242
|
if not login.msgUrl:
|
243
|
raise MandayeSamlException("sso: Unable to perform sso by redirection")
|
244
|
return _302(login.msgUrl, req_cookies)
|
245
|
|
246
|
def single_sign_on_post(self, env, values, request, response):
|
247
|
if self.urls.get('connection_failed_url'):
|
248
|
failed_url = self.urls['connection_failed_url']
|
249
|
else:
|
250
|
failed_url = '/'
|
251
|
metadata_file_path = self._get_idp_metadata_file_path()
|
252
|
if not metadata_file_path:
|
253
|
raise MandayeSamlException("single_sign_on_post: Unable to load provider")
|
254
|
server = lasso.Server.newFromBuffers(self._get_metadata(env),
|
255
|
self.config['saml2_signature_private_key'])
|
256
|
if not server:
|
257
|
raise MandayeSamlException("singleSignOnPost: error creating server object")
|
258
|
server.addProvider(lasso.PROVIDER_ROLE_IDP, metadata_file_path)
|
259
|
login = lasso.Login(server)
|
260
|
if not login:
|
261
|
raise MandayeSamlException("singleSignOnPost: Error creating login object")
|
262
|
|
263
|
if env['REQUEST_METHOD'] != 'POST':
|
264
|
raise MandayeSamlException("singleSignOnPost: Not a POST request")
|
265
|
|
266
|
msg = env['wsgi.input']
|
267
|
params = parse_qs(msg.read())
|
268
|
if not params or not lasso.SAML2_FIELD_RESPONSE in params.keys():
|
269
|
raise MandayeSamlException("singleSignOnPost: Missing response")
|
270
|
message = params[lasso.SAML2_FIELD_RESPONSE][0]
|
271
|
logger.debug('singleSignOnPost message: %r', message)
|
272
|
if not env['beaker.session'].has_key('request_id'):
|
273
|
logger.warning("singleSignOnPost: Unable to find request_id in session")
|
274
|
return _302(failed_url)
|
275
|
saml_request_id = env['beaker.session']['request_id']
|
276
|
try:
|
277
|
login.processAuthnResponseMsg(message)
|
278
|
except lasso.ProfileRequestDeniedError, e:
|
279
|
logger.warning('singleSignOnPost: request denied for nameid %r', saml_request_id)
|
280
|
logger.warning('singleSignOnPost: request denied error: %r', e)
|
281
|
return _302(failed_url)
|
282
|
subject_confirmation = utils.get_absolute_uri(env)
|
283
|
check = saml2utils.authnresponse_checking(login, subject_confirmation,
|
284
|
logger, saml_request_id=saml_request_id)
|
285
|
if not check:
|
286
|
logger.warning("singleSignOnPost: error checking authn response for %r",
|
287
|
saml_request_id)
|
288
|
return _302(failed_url)
|
289
|
logger.debug('sso: response successfully checked')
|
290
|
|
291
|
try:
|
292
|
login.acceptSso()
|
293
|
except lasso.Error, error:
|
294
|
raise MandayeSamlException("singleSignOnPost: Error validating\
|
295
|
sso (%s)" % lasso.strError(error[0]))
|
296
|
logger.debug('sso: sso accepted, session validation')
|
297
|
|
298
|
env['beaker.session']['validated'] = True
|
299
|
attributes = saml2utils.get_attributes_from_assertion(login.assertion,
|
300
|
logger)
|
301
|
env['beaker.session']['attributes'] = attributes
|
302
|
env['beaker.session']['unique_id'] = login.nameIdentifier.content
|
303
|
env['beaker.session']['liberty_session'] = login.session.dump()
|
304
|
env['beaker.session'].save()
|
305
|
|
306
|
next_url = None
|
307
|
if params.has_key('RelayState'):
|
308
|
next_url = params['RelayState'][0]
|
309
|
elif values.has_key('next_url'):
|
310
|
next_url = values['next_url']
|
311
|
|
312
|
if next_url:
|
313
|
return _302("%s?next_url=%s" % (self.urls.get('login_url'), next_url))
|
314
|
else:
|
315
|
return _302(self.urls.get('login_url'))
|
316
|
|
317
|
def slo(self, env, values, request, response):
|
318
|
"""
|
319
|
Single Logout SP initiated by redirected
|
320
|
"""
|
321
|
logger.debug('slo: new slo request')
|
322
|
target_idp = self.config['saml2_idp_metadata']
|
323
|
metadata_file_path = self._get_idp_metadata_file_path()
|
324
|
if not metadata_file_path:
|
325
|
raise MandayeSamlException("slo: Unable to load provider.")
|
326
|
logger.debug('slo: target idp %s' % target_idp)
|
327
|
logger.debug('slo: metadata file path %s' % metadata_file_path)
|
328
|
|
329
|
server = lasso.Server.newFromBuffers(self._get_metadata(env),
|
330
|
self.config['saml2_signature_private_key'])
|
331
|
if not server:
|
332
|
raise MandayeSamlException("slo: Error creating server object")
|
333
|
logger.debug('slo: mandaye server object created')
|
334
|
server.addProvider(lasso.PROVIDER_ROLE_IDP, metadata_file_path)
|
335
|
logout = lasso.Logout(server)
|
336
|
|
337
|
# Load liberty session
|
338
|
if not env['beaker.session'].has_key('liberty_session'):
|
339
|
logger.warning('slo: no liberty session in the session')
|
340
|
return self.local_logout(env, values, request, response)
|
341
|
logout.setSessionFromDump(env['beaker.session']['liberty_session'])
|
342
|
if not logout:
|
343
|
logger.warning('slo: error creating logout object')
|
344
|
return self.local_logout(env, values, request, response)
|
345
|
try:
|
346
|
logout.initRequest(None, lasso.HTTP_METHOD_REDIRECT)
|
347
|
except Exception, e:
|
348
|
logger.warning('sp_slo: init request error')
|
349
|
logger.warning('sp_slo error : %s' % str(e))
|
350
|
return self.local_logout(env, values, request, response)
|
351
|
# Manage RelayState
|
352
|
qs = parse_qs(env['QUERY_STRING'])
|
353
|
if qs.has_key('next_url'):
|
354
|
logout.msgRelayState = qs['next_url'][0]
|
355
|
try:
|
356
|
logout.buildRequestMsg()
|
357
|
except Exception, e:
|
358
|
logger.warning('slo: build request error')
|
359
|
logger.warning('slo error : %s' % e)
|
360
|
return self.local_logout(env, values, request, response)
|
361
|
logger.info('slo: sp_slo by redirect')
|
362
|
return _302(logout.msgUrl)
|
363
|
|
364
|
def slo_return_response(self, logout, cookies=None):
|
365
|
try:
|
366
|
logout.buildResponseMsg()
|
367
|
except lasso.Error, error:
|
368
|
logger.warning('saml2_slo_return_response: %s' % lasso.strError(error[0]))
|
369
|
return _401('saml2_slo_return_response: %s' % lasso.strError(error[0]))
|
370
|
else:
|
371
|
logger.info('saml2_slo_return_response: redirect to %s' % logout.msgUrl)
|
372
|
return _302(logout.msgUrl, cookies)
|
373
|
|
374
|
def single_logout(self, env, values, request, response):
|
375
|
"""
|
376
|
Single Logout IdP initiated by Redirect
|
377
|
"""
|
378
|
qs = env['QUERY_STRING']
|
379
|
if not env['QUERY_STRING']:
|
380
|
raise MandayeSamlException("single_logout: Single Logout by \
|
381
|
Redirect without query string")
|
382
|
|
383
|
server = lasso.Server.newFromBuffers(self._get_metadata(env),
|
384
|
self.config['saml2_signature_private_key'])
|
385
|
if not server:
|
386
|
logger.warning('single_logout: Service provider not configured')
|
387
|
return _401('single_logout: Service provider not configured')
|
388
|
metadata_file_path = self._get_idp_metadata_file_path()
|
389
|
server.addProvider(lasso.PROVIDER_ROLE_IDP, metadata_file_path)
|
390
|
logout = lasso.Logout(server)
|
391
|
if not logout:
|
392
|
logger.error('single_logout: Unable to create Logout object')
|
393
|
raise MandayeSamlException("single_logout: Unable to create Logout object")
|
394
|
try:
|
395
|
logout.processRequestMsg(qs)
|
396
|
except lasso.Error, error:
|
397
|
logger.error('saml2_slo: %s' % lasso.strError(error[0]))
|
398
|
return self.slo_return_response(logout)
|
399
|
|
400
|
logger.info('single_logout: slo from %s' % logout.remoteProviderId)
|
401
|
# Load liberty session
|
402
|
if not env['beaker.session'].has_key('liberty_session'):
|
403
|
logger.error('single_logout: no liberty session in the session')
|
404
|
raise MandayeSamlException("single_logout: no liberty session in the session")
|
405
|
logout.setSessionFromDump(env['beaker.session']['liberty_session'])
|
406
|
|
407
|
try:
|
408
|
logout.validateRequest()
|
409
|
except lasso.Error, error:
|
410
|
logger.error('single_logout: %s' % lasso.strError(error[0]))
|
411
|
return self.slo_return_response(logout)
|
412
|
|
413
|
# SP logout
|
414
|
response = self.local_logout(env, values, request, response)
|
415
|
return self.slo_return_response(logout, response.cookies)
|
416
|
|
417
|
def single_logout_return(self, env, values, request, response):
|
418
|
"""
|
419
|
Return point of the slo
|
420
|
"""
|
421
|
metadata_file_path = self._get_idp_metadata_file_path()
|
422
|
server = lasso.Server.newFromBuffers(self._get_metadata(env),
|
423
|
self.config['saml2_signature_private_key'])
|
424
|
server.addProvider(lasso.PROVIDER_ROLE_IDP, metadata_file_path)
|
425
|
if not server:
|
426
|
logger.error('single_logout_return: Error creating server object.')
|
427
|
raise MandayeSamlException("single_logout_return: Error creating server object")
|
428
|
qs = env['QUERY_STRING']
|
429
|
if not env['QUERY_STRING']:
|
430
|
logger.error('single_logout_return: Single Logout \
|
431
|
by Redirect without query string')
|
432
|
raise MandayeSamlException("single_logout_return: Single Logout \
|
433
|
by Redirect without query string")
|
434
|
logout = lasso.Logout(server)
|
435
|
if not logout:
|
436
|
logger.error('single_logout_return: Unable to create Logout object')
|
437
|
raise MandayeSamlException("single_logout_return: Unable to create Logout object")
|
438
|
# Load liberty session
|
439
|
if not env['beaker.session'].has_key('liberty_session'):
|
440
|
logger.warning('single_logout_return: no liberty session found.')
|
441
|
try:
|
442
|
logout.processResponseMsg(qs)
|
443
|
except lasso.Error, e:
|
444
|
logger.warning("single_logout_return: %s" % lasso.strError(e[0]))
|
445
|
# Local logout
|
446
|
return self.local_logout(env, values, request, response)
|
447
|
|
448
|
|
449
|
def metadata(self, env, values, request, response):
|
450
|
headers = HTTPHeader({'Content-Type': ['text/xml']})
|
451
|
return HTTPResponse(200, 'Found', headers,
|
452
|
self._get_metadata(env))
|
453
|
|