1
|
|
2
|
|
3
|
|
4
|
|
5
|
|
6
|
|
7
|
|
8
|
|
9
|
|
10
|
|
11
|
|
12
|
|
13
|
|
14
|
|
15
|
|
16
|
|
17
|
import traceback
|
18
|
import urllib, os, socket, time
|
19
|
|
20
|
from twisted.web2 import http, responsecode
|
21
|
from twisted.web2.http_headers import Cookie as TwCookie, MimeType
|
22
|
from twisted.web2.resource import PostableResource
|
23
|
from twisted.internet.threads import callMultipleInThread
|
24
|
from twisted.python import failure
|
25
|
|
26
|
from pyeole import httprequest
|
27
|
from eolesso.util import (getCookies, gen_random_id, RedirectResponse,
|
28
|
format_err, get_service_from_url, is_true, http_headers)
|
29
|
|
30
|
import saml_message
|
31
|
from saml_crypto import sign_request
|
32
|
from saml_utils import (encode_request, gen_metadata, get_endpoint,
|
33
|
get_attributes, InternalError, Redirect,
|
34
|
extract_message, check_required_contexts,
|
35
|
process_auth_request, process_assertion,
|
36
|
encode_idp_cookie, split_form_arg)
|
37
|
from saml2 import samlp
|
38
|
from oidc_utils import openid_logout_url
|
39
|
from page import gen_page, trace, log
|
40
|
from config import (CERTFILE, AUTH_FORM_URL, IDP_IDENTITY, DEFAULT_CSS,
|
41
|
SEND_CAS_LOGOUT, DISPLAY_FEDERATION, DEBUG_LOG, encoding)
|
42
|
|
43
|
class SamlResource(PostableResource):
|
44
|
|
45
|
content_type = 'text/html; charset=utf-8'
|
46
|
|
47
|
headers = http_headers
|
48
|
|
49
|
def set_headers(self, resp, user_headers={}):
|
50
|
"""attache à la réponse HTTP les headers par défaut
|
51
|
+ les éventuels headers passés en paramètre
|
52
|
"""
|
53
|
if self.content_type:
|
54
|
resp.headers.setRawHeaders('Content-type', [self.content_type])
|
55
|
for headers in (self.headers, user_headers):
|
56
|
for h_name, h_values in headers.items():
|
57
|
resp.headers.setRawHeaders(h_name, h_values)
|
58
|
return resp
|
59
|
|
60
|
def render(self, request):
|
61
|
|
62
|
try:
|
63
|
return self._render(request)
|
64
|
except Redirect, exc:
|
65
|
headers = {'Content-type': MimeType.fromString(self.content_type)}
|
66
|
location = exc.location
|
67
|
headers['location'] = location
|
68
|
log.msg(_('Redirecting to '), location)
|
69
|
|
70
|
return http.Response(code = 303, headers = headers)
|
71
|
except Exception, exc:
|
72
|
traceback.print_exc()
|
73
|
return http.StatusResponse(code = responsecode.INTERNAL_SERVER_ERROR, description = str(exc))
|
74
|
|
75
|
def _render(self):
|
76
|
pass
|
77
|
|
78
|
def gen_page_err(err_msg, from_url, css):
|
79
|
"""construit une réponse d'erreur
|
80
|
"""
|
81
|
form = format_err(err_msg)
|
82
|
if from_url:
|
83
|
form += """<p><FORM action="%s">
|
84
|
<b>%s</b> : <br/><h2><font color="blue">%s</font></h2><br/></p>
|
85
|
<p class="formvalidation"><input class="btn" type="submit" value ="%s"/></p>
|
86
|
</form>""" % (from_url, _('Following page has been proposed'), from_url, _('Proceed to above url'))
|
87
|
else:
|
88
|
form += """<br/><form action="/login" method="post">
|
89
|
<p class="formvalidation">
|
90
|
<input class="btn" type="submit" value="%s"></p></form>""" % _('New session')
|
91
|
return http.Response(stream = gen_page(_('Logout'), form, css))
|
92
|
|
93
|
class SamlLogout(SamlResource):
|
94
|
"""Resource de gestion du logout (logout cas + single logout saml)
|
95
|
"""
|
96
|
addSlash = False
|
97
|
|
98
|
def __init__(self, manager):
|
99
|
self.manager = manager
|
100
|
super(SamlLogout, self).__init__()
|
101
|
|
102
|
@trace
|
103
|
def saml_logout_request(self, ticket, sso_session, from_url, css):
|
104
|
"""Envoi d'une requête de déconnexion à une entité partenaire et stocke les informations sur l'origine de la déconnexion"""
|
105
|
saml_identity = ticket.saml_ident
|
106
|
saml_role = ticket.saml_role
|
107
|
|
108
|
sp_meta = self.manager.get_metadata(saml_identity)
|
109
|
user_id = ticket.name_id
|
110
|
try:
|
111
|
binding, service_url, response_url = get_endpoint(sp_meta, 'SingleLogoutService', ticket.saml_role)
|
112
|
except InternalError:
|
113
|
log.msg(_('no usable endpoint for {0}').format(sp_meta['entityID']))
|
114
|
|
115
|
return None
|
116
|
|
117
|
response_id = self.manager.gen_saml_id({'type':'LogoutRequest'})
|
118
|
if hasattr(ticket, 'idp_session_index'):
|
119
|
session_index = ticket.idp_session_index
|
120
|
else:
|
121
|
session_index = ticket.ticket
|
122
|
|
123
|
if binding == samlp.BINDING_HTTP_POST:
|
124
|
sign_method, req = saml_message.gen_logout_request(response_id, session_index, \
|
125
|
user_id, IDP_IDENTITY, sp_meta['entityID'], service_url, CERTFILE)
|
126
|
|
127
|
req = encode_request(req)
|
128
|
form = """
|
129
|
<FORM action="%s" method="POST">
|
130
|
<input type="hidden" name="SAMLRequest" value="%s">
|
131
|
<input type="hidden" name="SigAlg" value="%s">
|
132
|
<input type="hidden" name="RelayState" value="%s">
|
133
|
<p class="formvalidation">
|
134
|
<input class="btn" type="submit" value="%s"></p>
|
135
|
""" % (service_url, split_form_arg(req), sign_method, from_url, _("Proceed to service"))
|
136
|
resp = http.Response(stream = gen_page(_('Logout request to service'), form, css))
|
137
|
resp = self.set_headers(resp)
|
138
|
else:
|
139
|
sign_method, req = saml_message.gen_logout_request(response_id, session_index, user_id, IDP_IDENTITY, \
|
140
|
sp_meta['entityID'], service_url, CERTFILE, sign = False)
|
141
|
|
142
|
req = encode_request(req, True)
|
143
|
req_args = sign_request(CERTFILE, sign_method, 'SAMLRequest', req, from_url)
|
144
|
if req_args != []:
|
145
|
redirect_url = '%s?%s' % (service_url, urllib.urlencode(req_args))
|
146
|
resp = RedirectResponse(redirect_url)
|
147
|
|
148
|
|
149
|
ticket.valid = False
|
150
|
self.manager.pending_logout[sso_session][3][response_id] = ticket.ticket
|
151
|
log.msg("%s -- %s" % (sso_session, _("Sending SAML logout request to {0} ({1}) : {2}").format(saml_identity, service_url, response_id)))
|
152
|
return resp
|
153
|
|
154
|
@trace
|
155
|
def openid_logout_request(self, ticket, sso_session, from_url, css):
|
156
|
"""Envoi d'une requête de déconnexion à une entité partenaire et stocke les informations sur l'origine de la déconnexion"""
|
157
|
|
158
|
state, redirect_url, needs_confirm = openid_logout_url(ticket.provider_data, self.manager, sso_session, from_url)
|
159
|
|
160
|
ticket.valid = False
|
161
|
self.manager.pending_logout[sso_session][3][state] = ticket.ticket
|
162
|
if needs_confirm:
|
163
|
return self.confirm_logout(redirect_url, ticket, css)
|
164
|
else:
|
165
|
return RedirectResponse(redirect_url)
|
166
|
|
167
|
@trace
|
168
|
def confirm_logout(self, redirect_url, ticket, css):
|
169
|
""" Asks if user wants to disconnect from Provider
|
170
|
"""
|
171
|
prov_label = self.manager.external_providers[ticket.provider_data['provider']][2]
|
172
|
content = ""
|
173
|
logout_msg = _('Do you want to disconnect from {0} ?').format(prov_label)
|
174
|
params = {'logout_url':redirect_url + "?state=%s" % ticket.provider_data['state'],
|
175
|
|
176
|
|
177
|
'fallback_url':AUTH_FORM_URL + '/logout?state=%s' % ticket.provider_data['state'],
|
178
|
'message':logout_msg
|
179
|
}
|
180
|
header_script = """<script type="text/javascript">\n"""
|
181
|
header_script += open('interface/scripts/confirm_logout.js').read() % params
|
182
|
header_script += """\n</script>"""
|
183
|
onload_script = """confirm_logout();"""
|
184
|
resp = http.Response(stream = gen_page(_('Logout request to service'), content,
|
185
|
javascript = onload_script,
|
186
|
header_script=header_script,
|
187
|
css=css))
|
188
|
resp = self.set_headers(resp)
|
189
|
return resp
|
190
|
|
191
|
@trace
|
192
|
def send_cas_logout_request(self, ticket, sso_session, use_SOAP = False):
|
193
|
"""
|
194
|
envoie une demande de déconnexion à un client CAS par un requête SAML
|
195
|
use_SOAP : utiliser une enveloppe SOAP pour transmettre le message si True
|
196
|
(versions futures du client CAS ?)
|
197
|
"""
|
198
|
logoutRequest = saml_message.gen_cas_logout_request(ticket.ticket, use_SOAP)
|
199
|
log.msg("%s -- %s" % (sso_session, _("Sending CAS logout request to {0}").format(ticket.service_url)))
|
200
|
|
201
|
|
202
|
req = httprequest.HTTPRequest()
|
203
|
self.manager.set_urllib_proxy(ticket)
|
204
|
try:
|
205
|
req.request(ticket.service_url, {'logoutRequest':logoutRequest})
|
206
|
except httprequest.RequestError, e:
|
207
|
log.msg("%s -- %s" % (ticket.session_id, _('Error sending logout request to {0} : ')\
|
208
|
.format(ticket.service_url)))
|
209
|
log.msg(str(e))
|
210
|
|
211
|
self.manager.set_urllib_proxy()
|
212
|
|
213
|
@trace
|
214
|
def saml_logout_response(self, request, sso_session):
|
215
|
"""traite une réponse de déconnexion et met à jour les tickets correspondants dans le gestionnaire de sessions
|
216
|
"""
|
217
|
signature_ok, saml_resp = extract_message(request, 'SAMLResponse')
|
218
|
if not signature_ok:
|
219
|
raise InternalError("%s : %s" % (_('signature error'), str(saml_resp)))
|
220
|
response = samlp.LogoutResponseFromString(saml_resp)
|
221
|
|
222
|
msg_id = response.id
|
223
|
if self.manager.replayed_saml_msg(msg_id):
|
224
|
|
225
|
raise InternalError("%s -- %s" % (sso_session, _('response message has been replayed ({0})').format(msg_id)))
|
226
|
resp_issuer = response.issuer.text.encode(encoding)
|
227
|
in_response_to = response.in_response_to
|
228
|
|
229
|
if in_response_to not in self.manager.pending_logout[sso_session][3]:
|
230
|
|
231
|
raise InternalError("%s : %s" % (_('corresponding request not found'), in_response_to))
|
232
|
orig_message_data = self.manager.get_saml_msg(in_response_to)
|
233
|
if orig_message_data.get('response_id', None) is not None:
|
234
|
raise InternalError((_('response already received for request {0}').format(in_response_to)))
|
235
|
|
236
|
status_code = response.status.status_code.value
|
237
|
|
238
|
|
239
|
ticket_id = self.manager.pending_logout[sso_session][3][in_response_to]
|
240
|
if status_code == samlp.STATUS_SUCCESS:
|
241
|
self.manager.pending_logout[sso_session][1].append(ticket_id)
|
242
|
|
243
|
self.manager.update_saml_msg(in_response_to, {'response_id':msg_id})
|
244
|
log.msg("%s -- %s" % (sso_session, _('SAMLResponse ({0}) received from {1} in response to {2}').format('LogoutResponse', resp_issuer, in_response_to)))
|
245
|
|
246
|
@trace
|
247
|
def openid_logout_response(self, request, sso_session):
|
248
|
"""traite une réponse de déconnexion et met à jour les tickets correspondants dans le gestionnaire de sessions
|
249
|
"""
|
250
|
in_response_to = request.args['state'][0]
|
251
|
|
252
|
if in_response_to not in self.manager.pending_logout[sso_session][3]:
|
253
|
|
254
|
raise InternalError("%s : %s" % (_('corresponding request not found'), in_response_to))
|
255
|
|
256
|
|
257
|
ticket_id = self.manager.pending_logout[sso_session][3][in_response_to]
|
258
|
self.manager.pending_logout[sso_session][1].append(ticket_id)
|
259
|
log.msg("%s -- %s" % (sso_session, _('OpenID logout performed in response to {0}').format(in_response_to)))
|
260
|
|
261
|
@trace
|
262
|
def build_logout_error(self, err_infos, sso_session, relay_state, css):
|
263
|
"""envoie une réponse de déconnexion négative en réponse à une requête
|
264
|
"""
|
265
|
|
266
|
req_id, req_issuer, err_msg = err_infos
|
267
|
issue_instant = time.time()
|
268
|
|
269
|
sp_meta = self.manager.get_metadata(req_issuer)
|
270
|
|
271
|
entity_role = 'SPSSODescriptor'
|
272
|
if 'SingleLogoutService' not in sp_meta[entity_role]:
|
273
|
entity_role = 'IDPSSODescriptor'
|
274
|
try:
|
275
|
binding, service_url, response_url = get_endpoint(sp_meta, 'SingleLogoutService', entity_role)
|
276
|
except InternalError:
|
277
|
traceback.print_exc()
|
278
|
|
279
|
err_msg = _('no usable endpoint for {0}').format(req_issuer)
|
280
|
return http.Response(stream = gen_page(_('Logout Error'), err_msg, css))
|
281
|
|
282
|
response_id = self.manager.gen_saml_id({'type':'LogoutResponse'})
|
283
|
sign_method, response = saml_message.gen_logout_response(req_id,
|
284
|
response_id,
|
285
|
IDP_IDENTITY,
|
286
|
response_url,
|
287
|
CERTFILE,
|
288
|
samlp.STATUS_UNKNOWN_PRINCIPAL,
|
289
|
status_msg = err_msg)
|
290
|
return self.send_logout_response(response, sign_method, sso_session, (binding, response_url, relay_state), css)
|
291
|
|
292
|
@trace
|
293
|
def process_logout_request(self, request, sso_session):
|
294
|
"""traite une demande de déconnexion et initie le processus de déconnexion
|
295
|
"""
|
296
|
signature_ok, saml_req = extract_message(request, 'SAMLRequest')
|
297
|
if not signature_ok:
|
298
|
|
299
|
raise InternalError("signature error : %s" % str(saml_req))
|
300
|
saml_req = samlp.LogoutRequestFromString(saml_req)
|
301
|
req_issuer = saml_req.issuer.text.encode(encoding)
|
302
|
req_id = saml_req.id
|
303
|
|
304
|
req_name_id = saml_req.name_id or saml_req.base_id or saml_req.encrypted_id
|
305
|
req_name_id = req_name_id.text.encode(encoding)
|
306
|
try:
|
307
|
if not sso_session:
|
308
|
|
309
|
raise InternalError(_('Invalid session'))
|
310
|
|
311
|
app_ticket = None
|
312
|
if saml_req.session_index:
|
313
|
app_ticket = saml_req.session_index.text.encode(encoding)
|
314
|
else:
|
315
|
|
316
|
for ticket in self.manager.user_app_tickets.get(sso_session, []):
|
317
|
if ticket.saml_ident == req_issuer and ticket.nameid == req_name_id:
|
318
|
|
319
|
app_ticket = ticket.ticket
|
320
|
break
|
321
|
if app_ticket in self.manager.saml_sessions:
|
322
|
|
323
|
app_ticket = self.manager.saml_sessions[app_ticket]
|
324
|
if self.manager._DBAppSessionFromTicket(app_ticket):
|
325
|
ticket = self.manager.app_sessions[app_ticket]
|
326
|
if saml_req.session_index:
|
327
|
if ticket.session_id == sso_session:
|
328
|
if req_issuer != ticket.saml_ident:
|
329
|
raise InternalError(_('logout requester ({0}) does not own session').format(req_issuer))
|
330
|
|
331
|
if (ticket.name_id != req_name_id):
|
332
|
raise InternalError(_('logout request for invalid session'))
|
333
|
|
334
|
self.manager.pending_logout[sso_session] = [samlp.STATUS_SUCCESS, [app_ticket], (app_ticket, req_id, req_issuer), {}, [], None]
|
335
|
|
336
|
ticket.valid = False
|
337
|
else:
|
338
|
raise InternalError(_('logout request for invalid session'))
|
339
|
else:
|
340
|
raise InternalError(_('Invalid session'))
|
341
|
log.msg("%s -- %s" % (sso_session, _('SAMLRequest ({0}) received from {1}').format('LogoutRequest', req_issuer)))
|
342
|
except InternalError, msg:
|
343
|
|
344
|
log.msg("%s -- %s" % (sso_session, _('Invalid SAMLRequest ({0}) received from {1}').format('LogoutRequest', req_issuer)))
|
345
|
return False, (req_id, req_issuer, str(msg))
|
346
|
return True , sso_session
|
347
|
|
348
|
@trace
|
349
|
def check_attached_sessions(self, sso_session, closed_services, failed_services, from_url, css):
|
350
|
"""Vérifie les tickets rattachés à la session en cours et envoie les demandes de déconnexion nécessaires
|
351
|
maintient la liste échecs/succés de chaque demande
|
352
|
"""
|
353
|
|
354
|
self.manager.pending_logout[sso_session][0] = samlp.STATUS_SUCCESS
|
355
|
services_done = self.manager.pending_logout[sso_session][4]
|
356
|
if from_url not in services_done:
|
357
|
services_done.append(from_url)
|
358
|
logout_call_list = []
|
359
|
|
360
|
if self.manager.pending_logout[sso_session][2]:
|
361
|
req_ticket, req_id, req_issuer = self.manager.pending_logout[sso_session][2]
|
362
|
else:
|
363
|
req_ticket = req_id = req_issuer = None
|
364
|
for ticket in self.manager.user_app_tickets[sso_session]:
|
365
|
if hasattr(ticket,'saml_ident'):
|
366
|
if ticket.ticket != req_ticket:
|
367
|
if ticket.valid:
|
368
|
if hasattr(ticket, 'provider_data'):
|
369
|
|
370
|
client = self.manager.external_providers[ticket.provider_data['provider']][0]
|
371
|
if not client.can_logout:
|
372
|
|
373
|
self.manager.pending_logout[sso_session][1].append(ticket.ticket)
|
374
|
ticket.valid = False
|
375
|
continue
|
376
|
else:
|
377
|
|
378
|
return self.openid_logout_request(ticket, sso_session, from_url, css)
|
379
|
else:
|
380
|
|
381
|
|
382
|
return self.saml_logout_request(ticket, sso_session, from_url, css)
|
383
|
else:
|
384
|
|
385
|
|
386
|
if ticket.saml_ident in self.manager.external_providers:
|
387
|
service_name = self.manager.external_providers[ticket.saml_ident][2]
|
388
|
else:
|
389
|
service_name = ticket.saml_ident
|
390
|
if ticket.ticket in self.manager.pending_logout[sso_session][1]:
|
391
|
closed_services.append(service_name)
|
392
|
else:
|
393
|
failed_services.append(service_name)
|
394
|
|
395
|
self.manager.pending_logout[sso_session][0] = samlp.STATUS_PARTIAL_LOGOUT
|
396
|
else:
|
397
|
if SEND_CAS_LOGOUT:
|
398
|
|
399
|
|
400
|
if ticket.service_url not in services_done:
|
401
|
services_done.append(ticket.service_url)
|
402
|
if not ticket.ticket.startswith('PT-'):
|
403
|
|
404
|
|
405
|
logout_call = (self.send_cas_logout_request,
|
406
|
[ticket, sso_session, False],
|
407
|
{})
|
408
|
logout_call_list.append(logout_call)
|
409
|
if logout_call_list:
|
410
|
|
411
|
callMultipleInThread(logout_call_list)
|
412
|
return None
|
413
|
|
414
|
|
415
|
@trace
|
416
|
def build_logout_response(self, sso_session, relay_state, css):
|
417
|
"""construit une réponse à destination de l'entité ayant demandé la déconnexion
|
418
|
"""
|
419
|
|
420
|
app_ticket, req_id, req_issuer = self.manager.pending_logout[sso_session][2]
|
421
|
|
422
|
saml_role = self.manager.app_sessions[app_ticket].saml_role
|
423
|
|
424
|
sp_meta = self.manager.get_metadata(req_issuer)
|
425
|
try:
|
426
|
binding, service_url, response_url = get_endpoint(sp_meta, 'SingleLogoutService', saml_role)
|
427
|
except InternalError:
|
428
|
traceback.print_exc()
|
429
|
err_msg = _('no usable endpoint for {0}').format(req_issuer)
|
430
|
return http.Response(stream = gen_page(_('Logout Error'), err_msg, css))
|
431
|
|
432
|
response_id = self.manager.gen_saml_id({'type':'LogoutResponse'})
|
433
|
sign_method, response = saml_message.gen_logout_response(req_id, response_id, IDP_IDENTITY, response_url, CERTFILE, \
|
434
|
self.manager.pending_logout[sso_session][0])
|
435
|
return self.send_logout_response(response, sign_method, sso_session, (binding, response_url, relay_state), css)
|
436
|
|
437
|
def send_logout_response(self, response, sign_method, sso_session, dest_info, css):
|
438
|
binding, response_url, relay_state = dest_info
|
439
|
if binding == samlp.BINDING_HTTP_REDIRECT:
|
440
|
|
441
|
response = encode_request(response, True)
|
442
|
|
443
|
req_args = sign_request(CERTFILE, sign_method, 'SAMLResponse', response, relay_state)
|
444
|
if req_args != []:
|
445
|
redirect_url = '%s?%s' % (response_url, urllib.urlencode(req_args))
|
446
|
else:
|
447
|
redirect_url = response_url
|
448
|
resp = RedirectResponse(redirect_url)
|
449
|
else:
|
450
|
|
451
|
response = encode_request(response)
|
452
|
relay_info = ""
|
453
|
if relay_state:
|
454
|
relay_info = """<input type="hidden" name="RelayState" value="%s">""" % relay_state
|
455
|
form = """
|
456
|
<FORM action="%s" method="POST">
|
457
|
<input type="hidden" name="SAMLResponse" value="%s">
|
458
|
%s
|
459
|
<input type="hidden" name="SigAlg" value="%s">
|
460
|
<p class="formvalidation">
|
461
|
<input class="btn" type="submit" value="%s"></p>
|
462
|
""" % (response_url, split_form_arg(response), relay_info, sign_method, _('Logout confirmation to service'))
|
463
|
resp = http.Response(stream = gen_page(_('Logout confirmation to service'), form, css))
|
464
|
resp = self.set_headers(resp)
|
465
|
log.msg("%s -- %s" % (sso_session, _("SAMLResponse ({0}) sent to {1}").format('LogoutResponse', response_url)))
|
466
|
return resp
|
467
|
|
468
|
@trace
|
469
|
def build_logout_page(self, sso_session, closed_services, failed_services, from_url, css):
|
470
|
"""construit la page à afficher en cas de déconnexion initiée localement
|
471
|
"""
|
472
|
logout_msg = ""
|
473
|
default_url, force_default = self.manager.get_default_logout_url(sso_session)
|
474
|
if not from_url or force_default:
|
475
|
from_url = default_url
|
476
|
if closed_services != []:
|
477
|
logout_msg += """<b><font color="green">%s</font></b>""" % _("Following service providers have been disconnected")
|
478
|
logout_msg += "<br/><table border=0><tr><td>%s</td></tr></table>" % "</td></tr><tr><td>".join(closed_services)
|
479
|
if failed_services != []:
|
480
|
logout_msg += """<b><font color="orange">%s</font></b>""" % _("Following service providers did not report successful logout")
|
481
|
logout_msg += "<br/><table border=0><tr><td>%s</td></tr></table>" % "</td></tr><tr><td>".join(failed_services)
|
482
|
if from_url:
|
483
|
logout_msg += """
|
484
|
<br/><b>%s</b> : <br/><h2><font color="blue">%s</font></h2><br/>
|
485
|
</p><p class="formvalidation">
|
486
|
<button type="button" class="btn" onclick="javascript:location.href='%s'">%s</button></p>
|
487
|
""" % (_('Following page has been proposed'), from_url, from_url, _('Proceed to above url'))
|
488
|
else:
|
489
|
logout_msg += """<br/><form action="/login" method="POST">
|
490
|
<p class="formvalidation">
|
491
|
<input class="btn" type="submit" value="%s"></p></form>""" % _('New session')
|
492
|
if self.manager.pending_logout[sso_session][0] == samlp.STATUS_PARTIAL_LOGOUT:
|
493
|
logout_status = _("Partial single logout success")
|
494
|
else:
|
495
|
logout_status = _("Single logout success")
|
496
|
resp = http.Response(stream = gen_page(logout_status, str(logout_msg), css))
|
497
|
return self.set_headers(resp)
|
498
|
|
499
|
def _render(self, request):
|
500
|
"""Gestion de la déconnexion des sessionss SSO
|
501
|
"""
|
502
|
|
503
|
cookie = None
|
504
|
authenticated = False
|
505
|
cookies = getCookies(request)
|
506
|
sso_session = None
|
507
|
for cookie in cookies:
|
508
|
if cookie.name == 'EoleSSOServer':
|
509
|
sso_session = cookie.value
|
510
|
authenticated = self.manager.validate_session(sso_session)
|
511
|
break
|
512
|
|
513
|
css = DEFAULT_CSS
|
514
|
|
515
|
from_url = None
|
516
|
relay_state = None
|
517
|
if 'RelayState' in request.args:
|
518
|
relay_state = request.args['RelayState'][0]
|
519
|
if request.args.has_key('url'):
|
520
|
from_url = request.args['url'][0]
|
521
|
elif request.args.has_key('service'):
|
522
|
from_url = request.args['service'][0]
|
523
|
elif relay_state:
|
524
|
|
525
|
from_url = relay_state
|
526
|
|
527
|
if from_url:
|
528
|
service_filter = self.manager._check_filter(from_url)[0]
|
529
|
if service_filter not in ['default', '']:
|
530
|
if os.path.exists(os.path.join('interface', '%s.css' % service_filter)):
|
531
|
css = service_filter
|
532
|
try:
|
533
|
css = request.args['css'][0]
|
534
|
except KeyError:
|
535
|
pass
|
536
|
closed_services = []
|
537
|
failed_services = []
|
538
|
resp = None
|
539
|
saml_resp = False
|
540
|
|
541
|
try:
|
542
|
|
543
|
|
544
|
if not authenticated:
|
545
|
|
546
|
sso_session = None
|
547
|
if not 'SAMLRequest' in request.args:
|
548
|
|
549
|
raise InternalError(_('Invalid session'))
|
550
|
|
551
|
if 'SAMLResponse' in request.args and sso_session in self.manager.pending_logout:
|
552
|
|
553
|
|
554
|
self.saml_logout_response(request, sso_session)
|
555
|
if 'state' in request.args and sso_session in self.manager.pending_logout:
|
556
|
self.openid_logout_response(request, sso_session)
|
557
|
if 'SAMLRequest' in request.args:
|
558
|
req_ok, err_infos = self.process_logout_request(request, sso_session)
|
559
|
if not req_ok:
|
560
|
|
561
|
return self.build_logout_error(err_infos, sso_session, relay_state, css)
|
562
|
elif sso_session not in self.manager.pending_logout:
|
563
|
|
564
|
|
565
|
self.manager.pending_logout[sso_session] = [samlp.STATUS_SUCCESS, [], None, {}, [], (from_url, relay_state)]
|
566
|
|
567
|
resp = self.check_attached_sessions(sso_session, closed_services, failed_services, from_url, css)
|
568
|
if resp is not None:
|
569
|
return resp
|
570
|
|
571
|
|
572
|
if self.manager.pending_logout[sso_session][5] is not None:
|
573
|
from_url, relay_state = self.manager.pending_logout[sso_session][5]
|
574
|
if self.manager.pending_logout[sso_session][2]:
|
575
|
|
576
|
saml_resp = True
|
577
|
resp = self.build_logout_response(sso_session, relay_state, css)
|
578
|
else:
|
579
|
|
580
|
resp = self.build_logout_page(sso_session, closed_services, failed_services, from_url, css)
|
581
|
|
582
|
|
583
|
|
584
|
|
585
|
default_url, force_default = self.manager.get_default_logout_url(sso_session)
|
586
|
|
587
|
|
588
|
if force_default:
|
589
|
from_url = default_url
|
590
|
|
591
|
self.manager.logout(sso_session)
|
592
|
except InternalError, err:
|
593
|
log.msg(err)
|
594
|
if not saml_resp:
|
595
|
|
596
|
|
597
|
if from_url and failed_services == []:
|
598
|
|
599
|
|
600
|
resp = RedirectResponse(from_url)
|
601
|
log.msg(_("Logged out : redirecting to {0}").format(from_url))
|
602
|
elif resp is None:
|
603
|
resp = RedirectResponse('/')
|
604
|
|
605
|
|
606
|
if sso_session and cookie:
|
607
|
cookie.expires = 1
|
608
|
resp.headers.setHeader('Set-Cookie', cookies)
|
609
|
return resp
|
610
|
|
611
|
class SamlMetadata(SamlResource):
|
612
|
|
613
|
def __init__(self, manager):
|
614
|
self.manager = manager
|
615
|
super(SamlMetadata, self).__init__()
|
616
|
|
617
|
def _render(self, request):
|
618
|
"""Affiche les métadonnées SAML du serveur local"""
|
619
|
data = gen_metadata(self.manager, AUTH_FORM_URL, CERTFILE)
|
620
|
resp = http.Response(stream = data.encode(encoding))
|
621
|
return self.set_headers(resp, {'Content-type': ('application/xml',)})
|
622
|
|
623
|
class SamlErrorPage(SamlResource):
|
624
|
"""
|
625
|
page d'erreur http
|
626
|
"""
|
627
|
addSlash = False
|
628
|
|
629
|
def __init__(self, code, description):
|
630
|
self.code = code
|
631
|
self.description = description
|
632
|
title = ('EoleSSO : Erreur')
|
633
|
super(SamlErrorPage, self).__init__()
|
634
|
|
635
|
def _render(self, request):
|
636
|
return http.StatusResponse(code = self.code, description = self.description)
|
637
|
|
638
|
class SamlConsumer(SamlResource):
|
639
|
"""Resource de traitement des demandes d'accès aux service tiers
|
640
|
(Fournisseur de service)
|
641
|
"""
|
642
|
addSlash = False
|
643
|
|
644
|
def __init__(self, manager):
|
645
|
self.manager = manager
|
646
|
super(SamlConsumer, self).__init__()
|
647
|
|
648
|
def _render(self, request):
|
649
|
"""Assertion Consuming processing
|
650
|
"""
|
651
|
|
652
|
authenticated = False
|
653
|
cookies = getCookies(request)
|
654
|
sso_session = None
|
655
|
for cookie in cookies:
|
656
|
if cookie.name == 'EoleSSOServer':
|
657
|
sso_session = cookie.value
|
658
|
|
659
|
authenticated = self.manager.validate_session(sso_session)
|
660
|
break
|
661
|
|
662
|
|
663
|
css = DEFAULT_CSS
|
664
|
|
665
|
return_url = None
|
666
|
relay_state = None
|
667
|
if 'RelayState' in request.args:
|
668
|
relay_state = request.args['RelayState'][0]
|
669
|
|
670
|
if relay_state:
|
671
|
|
672
|
return_url = self.manager.get_relay_data(relay_state)
|
673
|
if return_url is None:
|
674
|
|
675
|
return_url = relay_state
|
676
|
service_filter = self.manager._check_filter(return_url)[0]
|
677
|
if service_filter not in ['default', '']:
|
678
|
if os.path.exists(os.path.join('interface', '%s.css' % service_filter)):
|
679
|
css = service_filter
|
680
|
try:
|
681
|
css = request.args['css'][0]
|
682
|
except KeyError:
|
683
|
pass
|
684
|
try:
|
685
|
|
686
|
|
687
|
if authenticated:
|
688
|
|
689
|
pass
|
690
|
|
691
|
if 'SAMLResponse' not in request.args:
|
692
|
|
693
|
raise InternalError(_('SAMLResponse expected'))
|
694
|
else:
|
695
|
|
696
|
success, infos = process_assertion(self.manager, request)
|
697
|
|
698
|
except InternalError, e:
|
699
|
traceback.print_exc()
|
700
|
return self.set_headers(gen_page_err(str(e), return_url, css))
|
701
|
|
702
|
if not success:
|
703
|
content = _('Federation : No local user does match specified attributes')
|
704
|
return self.set_headers(gen_page_err(content, return_url, css))
|
705
|
else:
|
706
|
for assertion_id, data in infos.items():
|
707
|
|
708
|
session_index, assertion_issuer, resp_attrs, name_id, auth_instant, class_ref = data
|
709
|
|
710
|
|
711
|
if not return_url:
|
712
|
idp_opts = self.manager.get_federation_options(assertion_issuer)
|
713
|
if 'default_service' in idp_opts:
|
714
|
return_url = idp_opts['default_service']
|
715
|
content = u"<h2>%s</h2>" % (_("you have been authenticated by identity provider {0}").format(assertion_issuer))
|
716
|
|
717
|
if not authenticated:
|
718
|
sso_session = None
|
719
|
|
720
|
|
721
|
defer_federation = self.manager.authenticate_federated_user(resp_attrs, assertion_issuer, auth_instant, class_ref, sso_session)
|
722
|
return defer_federation.addBoth(self.callb_federation, return_url, assertion_id, data, content, css, request, cookies)
|
723
|
return self.set_headers(gen_page_err(_("NO USER FOUND"), return_url, css))
|
724
|
|
725
|
@trace
|
726
|
def callb_federation(self, result_fed, return_url, assertion_id, data, content, css, request, cookies):
|
727
|
session_id, user_data = result_fed
|
728
|
session_index, assertion_issuer, resp_attrs, name_id, auth_instant, class_ref = data
|
729
|
if session_id and not isinstance(session_id, failure.Failure):
|
730
|
|
731
|
cookies = []
|
732
|
for cookie in getCookies(request):
|
733
|
if cookie.name != 'EoleSSOServer':
|
734
|
cookies.append(cookie)
|
735
|
cookies.append(TwCookie("EoleSSOServer", session_id, path = "/", discard=True, secure=True))
|
736
|
|
737
|
self.manager.remove_old_ticket(session_id, assertion_issuer)
|
738
|
|
739
|
sp_meta = self.manager.get_metadata(assertion_issuer)
|
740
|
|
741
|
endpoint_index = None
|
742
|
if 'index' in request.args:
|
743
|
endpoint_index = request.args['index'][0]
|
744
|
try:
|
745
|
binding, service_url, response_url = get_endpoint(sp_meta, 'SingleSignOnService',
|
746
|
ent_type = 'IDPSSODescriptor',
|
747
|
index=endpoint_index)
|
748
|
except InternalError:
|
749
|
|
750
|
err_msg = _('no usable endpoint for {0}').format(assertion_issuer)
|
751
|
log.msg(err_msg)
|
752
|
return self.set_headers(http.Response(stream = gen_page(_('Assertion consuming error'), err_msg, css)))
|
753
|
app_ticket = self.manager.get_app_ticket(session_id, response_url, idp_ident=assertion_issuer)
|
754
|
if app_ticket:
|
755
|
|
756
|
if self.manager.app_sessions[app_ticket].filter == 'default':
|
757
|
self.manager.app_sessions[app_ticket].filter = 'saml'
|
758
|
|
759
|
service_filter = self.manager.app_sessions[app_ticket].filter
|
760
|
if service_filter not in ['default', '']:
|
761
|
if os.path.exists(os.path.join('interface', '%s.css' % service_filter)):
|
762
|
css = service_filter
|
763
|
|
764
|
self.manager.app_sessions[app_ticket].saml_ident = assertion_issuer
|
765
|
self.manager.app_sessions[app_ticket].saml_role = 'IDPSSODescriptor'
|
766
|
self.manager.app_sessions[app_ticket].response_id = assertion_id
|
767
|
self.manager.app_sessions[app_ticket].name_id = name_id
|
768
|
self.manager.app_sessions[app_ticket].idp_session_index = session_index
|
769
|
if 'uaj' in sp_meta:
|
770
|
|
771
|
self.manager.app_sessions[app_ticket].uaj = sp_meta.get('uaj')
|
772
|
if self.manager.app_sessions[app_ticket].timeout_callb.cancelled == 0:
|
773
|
|
774
|
self.manager.app_sessions[app_ticket].timeout_callb.cancel()
|
775
|
self.manager.saml_sessions[session_index] = app_ticket
|
776
|
|
777
|
if return_url is None:
|
778
|
if DEBUG_LOG:
|
779
|
content += """assertion_id = %s<br/><hr/>
|
780
|
idp session_index = %s<br/>
|
781
|
attributes = %s<br/>
|
782
|
authentication context = %s<br/><hr/>
|
783
|
local session created<br/>""" % (assertion_id, session_index, str(resp_attrs), class_ref)
|
784
|
else:
|
785
|
content += "<br>%s" % _("no destination service given")
|
786
|
|
787
|
else:
|
788
|
if isinstance(session_id, failure.Failure):
|
789
|
log.msg('Failure searching federated_user : %s' % session_id.getErrorMessage())
|
790
|
content = _('Federation : No local user does match specified attributes')
|
791
|
resp = gen_page_err(content, return_url, css)
|
792
|
return self.set_headers(gen_page_err)
|
793
|
return self.redirect_after_federation(return_url, content, css, cookies)
|
794
|
|
795
|
@trace
|
796
|
def redirect_after_federation(self, return_url, content, css, cookies):
|
797
|
if return_url:
|
798
|
resp = RedirectResponse(return_url)
|
799
|
else:
|
800
|
resp = http.Response(stream = gen_page('Request processed', content, css))
|
801
|
resp = self.set_headers(resp)
|
802
|
resp.headers.setHeader('Set-Cookie', cookies)
|
803
|
return resp
|
804
|
|
805
|
class SamlResponse(SamlResource):
|
806
|
"""ressource principale
|
807
|
traitement de l'envoi d'assertions SAML
|
808
|
(Fournisseur d'identité)
|
809
|
"""
|
810
|
|
811
|
def __init__(self, manager):
|
812
|
self.manager = manager
|
813
|
super(SamlResponse, self).__init__()
|
814
|
|
815
|
def _render(self, request):
|
816
|
"""Génère une réponse SAML2 correspondant au profil HTTP-POST
|
817
|
|
818
|
- verifie qu'une session SSO est déjà en cours
|
819
|
- sinon, essaie d'en établir une
|
820
|
- construit une Reponse SAML (contenant une assertion ou non) et l'envoie au fournisseur de service (entityID)
|
821
|
"""
|
822
|
app_ticket = None
|
823
|
relay_state = None
|
824
|
app_ticket = None
|
825
|
req_id = None
|
826
|
sp_ident = None
|
827
|
passive = False
|
828
|
force_auth = False
|
829
|
requested_contexts = []
|
830
|
comparison = ""
|
831
|
login_ticket = None
|
832
|
css = DEFAULT_CSS
|
833
|
compressed = True
|
834
|
if 'ticket' in request.args:
|
835
|
|
836
|
app_ticket = request.args['ticket'][0]
|
837
|
|
838
|
endpoint_index = None
|
839
|
if 'index' in request.args:
|
840
|
endpoint_index = request.args['index'][0]
|
841
|
if 'SAMLRequest' in request.args:
|
842
|
|
843
|
request_data = process_auth_request(self.manager, request, compressed=compressed)
|
844
|
|
845
|
sp_ident, response_url, binding, req_id, passive, force_auth, comparison, requested_contexts = request_data
|
846
|
login_ticket = self.manager.get_login_ticket(req_id)
|
847
|
|
848
|
|
849
|
req_ticket = self.manager.gen_relay_state(request_data)
|
850
|
log.msg("%s : %s" % (_('SAMLRequest ({0}) received from {1}').format('AuthnRequest', sp_ident), req_id))
|
851
|
sp_meta = self.manager.get_metadata(sp_ident)
|
852
|
|
853
|
if requested_contexts:
|
854
|
ctx_ok, required_ctx = check_required_contexts(comparison, requested_contexts)
|
855
|
if not ctx_ok:
|
856
|
|
857
|
resp = http.Response(stream = gen_page(_('Unreachable resource'), required_ctx, css))
|
858
|
return self.set_headers(resp)
|
859
|
elif 'ReqInfos' in request.args:
|
860
|
|
861
|
req_ticket = request.args['ReqInfos'][0]
|
862
|
try:
|
863
|
request_data = self.manager.get_relay_data(req_ticket)
|
864
|
sp_ident, response_url, binding, req_id, passive, force_auth, comparison, requested_contexts = request_data
|
865
|
sp_meta = self.manager.get_metadata(sp_ident)
|
866
|
except:
|
867
|
|
868
|
resp = http.Response(stream = gen_page(_('Unreachable resource'), 'ReqInfos=%s' % req_ticket, css))
|
869
|
return self.set_headers(resp)
|
870
|
else:
|
871
|
|
872
|
if 'sp_ident' in request.args:
|
873
|
sp_ident = request.args['sp_ident'][0]
|
874
|
|
875
|
sp_meta = self.manager.get_metadata(sp_ident)
|
876
|
sp_ident = sp_meta.get('entityID', sp_ident)
|
877
|
|
878
|
try:
|
879
|
binding, service_url, response_url = get_endpoint(sp_meta, 'AssertionConsumerService', index=endpoint_index)
|
880
|
except InternalError:
|
881
|
traceback.print_exc()
|
882
|
err_msg = _('no usable endpoint for {0}').format(sp_ident)
|
883
|
resp = http.Response(stream = gen_page(_('Unreachable resource'), err_msg, css))
|
884
|
return self.set_headers(resp)
|
885
|
else:
|
886
|
|
887
|
err_msg = _("Missing parameter")
|
888
|
resp = http.Response(stream = gen_page(_('Unreachable resource'), err_msg, css))
|
889
|
return self.set_headers(resp)
|
890
|
|
891
|
|
892
|
authenticated = False
|
893
|
session_id = None
|
894
|
user_id = ""
|
895
|
user_data = {}
|
896
|
cookies = getCookies(request)
|
897
|
from_credentials = False
|
898
|
for cookie in cookies:
|
899
|
if cookie.name == 'EoleSSOServer':
|
900
|
session_id = cookie.value
|
901
|
|
902
|
authenticated = self.manager.validate_session(session_id)
|
903
|
|
904
|
if not authenticated or (force_auth and not app_ticket):
|
905
|
if not passive:
|
906
|
redirect_args = []
|
907
|
for argname, argvalue in request.args.items():
|
908
|
if argname == "SAMLRequest":
|
909
|
|
910
|
|
911
|
argname = "ReqInfos"
|
912
|
argvalue = [req_ticket]
|
913
|
redirect_args.append((argname, argvalue[0]))
|
914
|
req_args = [('service', "%s/saml?%s" % (AUTH_FORM_URL, urllib.urlencode(redirect_args)))]
|
915
|
if force_auth:
|
916
|
req_args.append(('renew', 'true'))
|
917
|
if login_ticket:
|
918
|
req_args.append(('lt', login_ticket))
|
919
|
redirect_url = '%s?%s' % (AUTH_FORM_URL, urllib.urlencode(req_args))
|
920
|
resp = RedirectResponse(redirect_url)
|
921
|
return resp
|
922
|
elif req_id:
|
923
|
|
924
|
sign_method, response = saml_message.gen_status_response(CERTFILE, IDP_IDENTITY, response_url, samlp.STATUS_NO_PASSIVE, req_id, _("no previous authentication"))
|
925
|
|
926
|
|
927
|
try:
|
928
|
css = request.args['css'][0]
|
929
|
except KeyError:
|
930
|
css = DEFAULT_CSS
|
931
|
if 'RelayState' in request.args:
|
932
|
relay_state = request.args['RelayState'][0]
|
933
|
|
934
|
|
935
|
if app_ticket:
|
936
|
|
937
|
ticket = self.manager.app_sessions[app_ticket]
|
938
|
if ticket.session_id != session_id or \
|
939
|
ticket.from_credentials != True or \
|
940
|
ticket.service_url != "%s/saml" % (AUTH_FORM_URL):
|
941
|
app_ticket = None
|
942
|
if not app_ticket and authenticated:
|
943
|
|
944
|
self.manager.remove_old_ticket(session_id, sp_ident)
|
945
|
|
946
|
app_ticket = self.manager.get_app_ticket(session_id, response_url)
|
947
|
ticket = self.manager.app_sessions[app_ticket]
|
948
|
if app_ticket:
|
949
|
|
950
|
ticket.service_url = get_service_from_url(response_url)
|
951
|
if sp_ident in self.manager.apps['sp_ident']:
|
952
|
ticket.filter = self.manager.apps['sp_ident'][sp_ident][0]
|
953
|
else:
|
954
|
id_filter = self.manager._check_filter(ticket.service_url)[0]
|
955
|
ticket.filter = id_filter
|
956
|
|
957
|
if self.manager.app_sessions[app_ticket].filter == 'default':
|
958
|
self.manager.app_sessions[app_ticket].filter = 'saml'
|
959
|
|
960
|
service_filter = self.manager.app_sessions[app_ticket].filter
|
961
|
if service_filter not in ['default', ''] and css == DEFAULT_CSS:
|
962
|
if os.path.exists(os.path.join('interface', '%s.css' % service_filter)):
|
963
|
css = service_filter
|
964
|
|
965
|
|
966
|
|
967
|
|
968
|
user_id = gen_random_id('NAMEID_')
|
969
|
response_id = self.manager.gen_saml_id({'type':'SamlResponse'})
|
970
|
|
971
|
|
972
|
|
973
|
|
974
|
self.manager.app_sessions[app_ticket].saml_ident = sp_ident
|
975
|
self.manager.app_sessions[app_ticket].saml_role = 'SPSSODescriptor'
|
976
|
self.manager.app_sessions[app_ticket].response_id = response_id
|
977
|
self.manager.app_sessions[app_ticket].name_id = user_id
|
978
|
if 'uaj' in sp_meta:
|
979
|
|
980
|
self.manager.app_sessions[app_ticket].uaj = sp_meta.get('uaj')
|
981
|
|
982
|
if self.manager.app_sessions[app_ticket].timeout_callb.cancelled == 0:
|
983
|
self.manager.app_sessions[app_ticket].timeout_callb.cancel()
|
984
|
|
985
|
app_ok, user_data = self.manager.get_user_details(app_ticket, response_url, renew=False, sections=True, keep_valid=True)
|
986
|
data, filter_data = user_data
|
987
|
user_data = {}
|
988
|
|
989
|
for attrs in filter_data.values():
|
990
|
for name, label in attrs.items():
|
991
|
user_data[name] = data[label]
|
992
|
from_credentials = self.manager.app_sessions[app_ticket].from_credentials
|
993
|
|
994
|
attr_table = []
|
995
|
for attr, val in user_data.items():
|
996
|
if type(val) == list:
|
997
|
val = ', '.join(val)
|
998
|
if val == "":
|
999
|
val = " "
|
1000
|
attr_table.append('<tr><td>%s</td><td>%s</td></tr>' % (attr, val))
|
1001
|
attr_msg = _('Following attributes will be sent')
|
1002
|
|
1003
|
auth_instant = self.manager.get_auth_instant(app_ticket)
|
1004
|
|
1005
|
auth_class = self.manager.get_auth_class(app_ticket)
|
1006
|
|
1007
|
addr_client = request.chanRequest.getRemoteHost().host
|
1008
|
try:
|
1009
|
dns_client = socket.gethostbyaddr(addr_client)[0]
|
1010
|
except:
|
1011
|
|
1012
|
dns_client = None
|
1013
|
sign_method, response = saml_message.gen_response(response_id, authenticated, auth_instant, \
|
1014
|
app_ticket, from_credentials, user_id, user_data, req_id, response_url, \
|
1015
|
IDP_IDENTITY, sp_ident, CERTFILE, auth_class, addr_client, dns_client)
|
1016
|
|
1017
|
if request.args.has_key('show') and DEBUG_LOG:
|
1018
|
resp = http.Response(stream = response.encode(encoding))
|
1019
|
return self.set_headers(resp, {'Content-type': ('application/xml',)})
|
1020
|
if DISPLAY_FEDERATION == True and user_data:
|
1021
|
|
1022
|
msg = """<table border=1><tr><th colspan=2>%s</th></tr>%s</table>""" % (attr_msg, '\n'.join(attr_table))
|
1023
|
submit_input = """<p class="formvalidation"><input class="btn" type="Submit" value="%s"></p>""" % _('Proceed to service')
|
1024
|
else:
|
1025
|
msg = _('Please wait, accessing requested service...')
|
1026
|
submit_input = ""
|
1027
|
if binding == samlp.BINDING_HTTP_REDIRECT:
|
1028
|
|
1029
|
response = encode_request(response, True)
|
1030
|
req_args = sign_request(CERTFILE, sign_method, 'SAMLResponse', response, relay_state)
|
1031
|
if req_args != []:
|
1032
|
redirect_url = '%s?%s' % (response_url, urllib.urlencode(req_args))
|
1033
|
else:
|
1034
|
redirect_url = response_url
|
1035
|
resp = RedirectResponse(redirect_url)
|
1036
|
else:
|
1037
|
|
1038
|
response = encode_request(response)
|
1039
|
relay_info = ""
|
1040
|
if relay_state:
|
1041
|
relay_info = """<input type="hidden" name="RelayState" value="%s">""" % relay_state
|
1042
|
|
1043
|
form = """%s<br/><div class='ressource' id="wait_msg"></div><br/>
|
1044
|
<FORM name="federate_user" action="%s" method="POST" onSubmit="return aff_wait()">
|
1045
|
<input type="hidden" name="SAMLResponse" value="%s">
|
1046
|
%s
|
1047
|
<input type="hidden" xx name="SigAlg" value="%s">
|
1048
|
%s
|
1049
|
</FORM>""" % (msg, response_url, response, relay_info, sign_method, submit_input)
|
1050
|
|
1051
|
entity_name = sp_meta.get('entity_local_id', '')
|
1052
|
wait_msg = _("Awaiting response from ")
|
1053
|
header_script = """<script type="text/javascript">
|
1054
|
function aff_wait()
|
1055
|
{
|
1056
|
div_msg = document.getElementById('wait_msg');
|
1057
|
div_msg.innerHTML = "%s<span>%s</span>";
|
1058
|
return true;
|
1059
|
}
|
1060
|
</script>
|
1061
|
""" % (wait_msg, response_url)
|
1062
|
head = "%s : <br/>%s" % (_('You are being Authenticated to entity'), entity_name or sp_ident)
|
1063
|
if DISPLAY_FEDERATION == False or user_data == {}:
|
1064
|
head = _('Accessing to service')
|
1065
|
form += """<script type="text/javascript">aff_wait();document.federate_user.submit();</script>"""
|
1066
|
resp = http.Response(stream = gen_page(head, form, css, header_script=header_script))
|
1067
|
resp = self.set_headers(resp)
|
1068
|
if session_id:
|
1069
|
log_prefix = "%s -- " % session_id
|
1070
|
else:
|
1071
|
log_prefix = ""
|
1072
|
log.msg("%s%s" % (log_prefix, _('SAMLResponse ({0}) sent to {1}').format('AuthnStatement', sp_ident)))
|
1073
|
|
1074
|
return resp
|
1075
|
|
1076
|
def locateChild(self, request, segments):
|
1077
|
if segments and segments[0] == 'metadata':
|
1078
|
return SamlMetadata(self.manager), ()
|
1079
|
if segments and segments[0] == 'acs':
|
1080
|
return SamlConsumer(self.manager), ()
|
1081
|
if segments and segments[0] == 'logout':
|
1082
|
return SamlLogout(self.manager), ()
|
1083
|
if segments and segments[0] == 'discovery':
|
1084
|
return IDPDiscoveryService(self.manager), ()
|
1085
|
if segments == ():
|
1086
|
return self, ()
|
1087
|
return SamlErrorPage(responsecode.NOT_FOUND, "%s : %s" % (_('Unreachable resource'), '/'.join(segments))), ()
|
1088
|
|
1089
|
class IDPDiscoveryService(SamlResource):
|
1090
|
|
1091
|
isLeaf = True
|
1092
|
|
1093
|
def __init__(self, manager):
|
1094
|
self.manager = manager
|
1095
|
SamlResource.__init__(self)
|
1096
|
|
1097
|
@trace
|
1098
|
def render(self, request):
|
1099
|
try:
|
1100
|
css = request.args['css'][0]
|
1101
|
except KeyError:
|
1102
|
css = DEFAULT_CSS
|
1103
|
|
1104
|
try:
|
1105
|
idp_ident = request.args['idp_ident'][0]
|
1106
|
except:
|
1107
|
return SamlErrorPage(responsecode.INTERNAL_SERVER_ERROR, _("missing parameter: Identity Provider"))
|
1108
|
|
1109
|
|
1110
|
try:
|
1111
|
|
1112
|
return_url = request.args['return_url'][0]
|
1113
|
except:
|
1114
|
return_url = ""
|
1115
|
|
1116
|
endpoint_index = None
|
1117
|
if 'index' in request.args:
|
1118
|
endpoint_index = request.args['index'][0]
|
1119
|
|
1120
|
must_sign_request = False
|
1121
|
try:
|
1122
|
sp_meta = self.manager.get_metadata(idp_ident)
|
1123
|
idp_ident = sp_meta.get('entityID', idp_ident)
|
1124
|
|
1125
|
|
1126
|
binding, service_url, consumer_url = get_endpoint(sp_meta, 'SingleSignOnService',
|
1127
|
ent_type='IDPSSODescriptor', allowed_bindings=[samlp.BINDING_HTTP_REDIRECT, samlp.BINDING_HTTP_POST],
|
1128
|
index=endpoint_index)
|
1129
|
except:
|
1130
|
err_msg = _('no usable endpoint for {0}').format(idp_ident)
|
1131
|
return SamlErrorPage(responsecode.INTERNAL_SERVER_ERROR, err_msg)
|
1132
|
|
1133
|
|
1134
|
if not self.manager.check_federation_allowed(idp_ident):
|
1135
|
return SamlErrorPage(responsecode.UNAUTHORIZED, "%s : %s" % (idp_ident, _('no federation allowed for this entity')))
|
1136
|
|
1137
|
attr_service_index = self.manager.get_attribute_service_index(idp_ident)
|
1138
|
|
1139
|
request_id = self.manager.gen_saml_id({'type':'SamlRequest'})
|
1140
|
|
1141
|
idp_options = self.manager.get_federation_options(idp_ident)
|
1142
|
force_auth = is_true(idp_options.get('force_auth', 'false'))
|
1143
|
is_passive = is_true(idp_options.get('passive', 'false'))
|
1144
|
must_sign_request = idp_options.get('sign_request', None) or must_sign_request
|
1145
|
must_sign_request = False
|
1146
|
|
1147
|
comparison = idp_options.get('comparison', 'minimum')
|
1148
|
class_ref = idp_options.get('req_context', saml_message.URN_PROTECTED_PASSWORD)
|
1149
|
|
1150
|
sign_method, saml_request = saml_message.gen_request(self.manager, request_id, IDP_IDENTITY,
|
1151
|
idp_ident, consumer_url, attr_service_index, CERTFILE,
|
1152
|
sign_request=must_sign_request,
|
1153
|
is_passive=is_passive, force_auth=force_auth,
|
1154
|
comparison=comparison, class_ref=class_ref)
|
1155
|
|
1156
|
if request.args.has_key('show') and DEBUG_LOG:
|
1157
|
resp = http.Response(stream = saml_request.encode(encoding))
|
1158
|
return self.set_headers(resp, {'Content-type': ('application/xml',)})
|
1159
|
if return_url:
|
1160
|
|
1161
|
|
1162
|
relay_state = self.manager.gen_relay_state(return_url)
|
1163
|
|
1164
|
self.manager.update_saml_msg(request_id, {'relay_state':relay_state})
|
1165
|
else:
|
1166
|
relay_state = None
|
1167
|
if binding == samlp.BINDING_HTTP_REDIRECT:
|
1168
|
|
1169
|
saml_request = encode_request(saml_request, True)
|
1170
|
if must_sign_request:
|
1171
|
req_args = sign_request(CERTFILE, sign_method, 'SAMLRequest', saml_request, relay_state)
|
1172
|
if req_args != []:
|
1173
|
redirect_url = '%s?%s' % (consumer_url, urllib.urlencode(req_args))
|
1174
|
else:
|
1175
|
redirect_url = consumer_url
|
1176
|
else:
|
1177
|
req_args = []
|
1178
|
req_args.append(('SAMLRequest', saml_request))
|
1179
|
if relay_state:
|
1180
|
req_args.append(('RelayState', relay_state))
|
1181
|
redirect_url = '%s?%s' % (consumer_url, urllib.urlencode(req_args))
|
1182
|
resp = RedirectResponse(redirect_url)
|
1183
|
else:
|
1184
|
|
1185
|
saml_request = encode_request(saml_request)
|
1186
|
sign_method_info = ""
|
1187
|
relay_info = ""
|
1188
|
if relay_state:
|
1189
|
relay_info = """
|
1190
|
<input type="hidden" xx name="RelayState" value="%s">""" % relay_state
|
1191
|
if must_sign_request:
|
1192
|
sign_method_info = """
|
1193
|
<input type="hidden" xx name="SigAlg" value="%s">""" % sign_method
|
1194
|
form = """%s<br/>
|
1195
|
<FORM name="form_request" action="%s" method="POST">
|
1196
|
<input type="hidden" name="SAMLRequest" value="%s">%s%s
|
1197
|
<script type="text/javascript">document.form_request.submit();</script>
|
1198
|
</FORM>
|
1199
|
""" % (_('Contacting authentication server'), consumer_url, split_form_arg(saml_request), sign_method_info, relay_info)
|
1200
|
resp = self.set_headers(http.Response(stream = gen_page(_('IDPDiscovery'), form, css)))
|
1201
|
log.msg(_("Sending SAML authentication request to {0} ({1}) : {2}").format(idp_ident, consumer_url, request_id))
|
1202
|
return resp
|