Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2# This program is distributed in the hope that it will be useful, but WITHOUT
3# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
4# FOR A PARTICULAR PURPOSE. See the GNU General Public License version 3 for
5# more details.
6#
7# You should have received a copy of the GNU General Public License version 3
8# along with this program; if not, write to the Free Software Foundation, Inc., 51
9# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
10#
11# (c) 2015-2016 Valentin Samir
12"""views for the app"""
13from .default_settings import settings, SessionStore
15from django.shortcuts import render, redirect
16from django.http import HttpResponse, HttpResponseRedirect
17from django.contrib import messages
18from django.utils.decorators import method_decorator
19from django.utils.translation import ugettext as _
20from django.utils import timezone
21from django.views.decorators.csrf import csrf_exempt
22from django.middleware.csrf import CsrfViewMiddleware
23from django.views.generic import View
24from django.utils.encoding import python_2_unicode_compatible
25from django.utils.safestring import mark_safe
26try:
27 from django.urls import reverse
28except ImportError:
29 from django.core.urlresolvers import reverse
31import re
32import logging
33import pprint
34import requests
35from lxml import etree
36from datetime import timedelta
38import cas_server.utils as utils
39import cas_server.forms as forms
40import cas_server.models as models
42from .utils import json_response
43from .models import Ticket, ServiceTicket, ProxyTicket, ProxyGrantingTicket
44from .models import ServicePattern, FederatedIendityProvider, FederatedUser
45from .federate import CASFederateValidateUser
47logger = logging.getLogger(__name__)
50class LogoutMixin(object):
51 """destroy CAS session utils"""
53 def logout(self, all_session=False):
54 """
55 effectively destroy a CAS session
57 :param boolean all_session: If ``True`` destroy all the user sessions, otherwise
58 destroy the current user session.
59 :return: The number of destroyed sessions
60 :rtype: int
61 """
62 # initialize the counter of the number of destroyed sesisons
63 session_nb = 0
64 # save the current user username before flushing the session
65 username = self.request.session.get("username")
66 if username:
67 if all_session:
68 logger.info("Logging out user %s from all sessions." % username)
69 else:
70 logger.info("Logging out user %s." % username)
71 users = []
72 # try to get the user from the current session
73 try:
74 users.append(
75 models.User.objects.get(
76 username=username,
77 session_key=self.request.session.session_key
78 )
79 )
80 except models.User.DoesNotExist:
81 # if user not found in database, flush the session anyway
82 self.request.session.flush()
84 # If all_session is set, search all of the user sessions
85 if all_session:
86 users.extend(
87 models.User.objects.filter(
88 username=username
89 ).exclude(
90 session_key=self.request.session.session_key
91 )
92 )
94 # Iterate over all user sessions that have to be logged out
95 for user in users:
96 # get the user session
97 session = SessionStore(session_key=user.session_key)
98 # flush the session
99 session.flush()
100 # send SLO requests
101 user.logout(self.request)
102 # delete the user
103 user.delete()
104 # increment the destroyed session counter
105 session_nb += 1
106 if username:
107 logger.info("User %s logged out" % username)
108 return session_nb
111class CsrfExemptView(View):
112 """base class for csrf exempt class views"""
114 @method_decorator(csrf_exempt) # csrf is disabled for allowing SLO requests reception
115 def dispatch(self, request, *args, **kwargs):
116 """
117 dispatch different http request to the methods of the same name
119 :param django.http.HttpRequest request: The current request object
120 """
121 return super(CsrfExemptView, self).dispatch(request, *args, **kwargs)
124class LogoutView(View, LogoutMixin):
125 """destroy CAS session (logout) view"""
127 #: current :class:`django.http.HttpRequest` object
128 request = None
129 #: service GET parameter
130 service = None
131 #: url GET paramet
132 url = None
133 #: ``True`` if the HTTP_X_AJAX http header is sent and ``settings.CAS_ENABLE_AJAX_AUTH``
134 #: is ``True``, ``False`` otherwise.
135 ajax = None
137 def init_get(self, request):
138 """
139 Initialize the :class:`LogoutView` attributes on GET request
141 :param django.http.HttpRequest request: The current request object
142 """
143 self.request = request
144 self.service = request.GET.get('service')
145 self.url = request.GET.get('url')
146 self.ajax = settings.CAS_ENABLE_AJAX_AUTH and 'HTTP_X_AJAX' in request.META
148 def get(self, request, *args, **kwargs):
149 """
150 method called on GET request on this view
152 :param django.http.HttpRequest request: The current request object
153 """
154 logger.info("logout requested")
155 # initialize the class attributes
156 self.init_get(request)
157 # if CAS federation mode is enable, bakup the provider before flushing the sessions
158 if settings.CAS_FEDERATE:
159 try:
160 user = FederatedUser.get_from_federated_username(
161 self.request.session.get("username")
162 )
163 auth = CASFederateValidateUser(user.provider, service_url="")
164 except FederatedUser.DoesNotExist:
165 auth = None
166 session_nb = self.logout(self.request.GET.get("all"))
167 # if CAS federation mode is enable, redirect to user CAS logout page, appending the
168 # current querystring
169 if settings.CAS_FEDERATE:
170 if auth is not None:
171 params = utils.copy_params(request.GET, ignore={"forget_provider"})
172 url = auth.get_logout_url()
173 response = HttpResponseRedirect(utils.update_url(url, params))
174 if request.GET.get("forget_provider"):
175 response.delete_cookie("remember_provider")
176 return response
177 # if service is set, redirect to service after logout
178 if self.service:
179 list(messages.get_messages(request)) # clean messages before leaving the django app
180 return HttpResponseRedirect(self.service)
181 # if service is not set but url is set, redirect to url after logout
182 elif self.url:
183 list(messages.get_messages(request)) # clean messages before leaving the django app
184 return HttpResponseRedirect(self.url)
185 else:
186 # build logout message depending of the number of sessions the user logs out
187 if session_nb == 1:
188 logout_msg = mark_safe(_(
189 "<h3>Logout successful</h3>"
190 "You have successfully logged out from the Central Authentication Service. "
191 "For security reasons, close your web browser."
192 ))
193 elif session_nb > 1:
194 logout_msg = mark_safe(_(
195 "<h3>Logout successful</h3>"
196 "You have successfully logged out from %d sessions of the Central "
197 "Authentication Service. "
198 "For security reasons, close your web browser."
199 ) % session_nb)
200 else:
201 logout_msg = mark_safe(_(
202 "<h3>Logout successful</h3>"
203 "You were already logged out from the Central Authentication Service. "
204 "For security reasons, close your web browser."
205 ))
207 # depending of settings, redirect to the login page with a logout message or display
208 # the logout page. The default is to display tge logout page.
209 if settings.CAS_REDIRECT_TO_LOGIN_AFTER_LOGOUT:
210 messages.add_message(request, messages.SUCCESS, logout_msg)
211 if self.ajax:
212 url = reverse("cas_server:login")
213 data = {
214 'status': 'success',
215 'detail': 'logout',
216 'url': url,
217 'session_nb': session_nb
218 }
219 return json_response(request, data)
220 else:
221 return redirect("cas_server:login")
222 else:
223 if self.ajax:
224 data = {'status': 'success', 'detail': 'logout', 'session_nb': session_nb}
225 return json_response(request, data)
226 else:
227 return render(
228 request,
229 settings.CAS_LOGOUT_TEMPLATE,
230 utils.context({'logout_msg': logout_msg})
231 )
234class FederateAuth(CsrfExemptView):
235 """
236 view to authenticated user against a backend CAS then CAS_FEDERATE is True
238 csrf is disabled for allowing SLO requests reception.
239 """
241 #: current URL used as service URL by the CAS client
242 service_url = None
244 def get_cas_client(self, request, provider, renew=False):
245 """
246 return a CAS client object matching provider
248 :param django.http.HttpRequest request: The current request object
249 :param cas_server.models.FederatedIendityProvider provider: the user identity provider
250 :return: The user CAS client object
251 :rtype: :class:`federate.CASFederateValidateUser
252 <cas_server.federate.CASFederateValidateUser>`
253 """
254 # compute the current url, ignoring ticket dans provider GET parameters
255 service_url = utils.get_current_url(request, {"ticket", "provider"})
256 self.service_url = service_url
257 return CASFederateValidateUser(provider, service_url, renew=renew)
259 def post(self, request, provider=None):
260 """
261 method called on POST request
263 :param django.http.HttpRequest request: The current request object
264 :param unicode provider: Optional parameter. The user provider suffix.
265 """
266 # if settings.CAS_FEDERATE is not True redirect to the login page
267 if not settings.CAS_FEDERATE:
268 logger.warning("CAS_FEDERATE is False, set it to True to use federation")
269 return redirect("cas_server:login")
270 # POST with a provider suffix, this is probably an SLO request. csrf is disabled for
271 # allowing SLO requests reception
272 try:
273 provider = FederatedIendityProvider.objects.get(suffix=provider)
274 auth = self.get_cas_client(request, provider)
275 try:
276 auth.clean_sessions(request.POST['logoutRequest'])
277 except (KeyError, AttributeError):
278 pass
279 return HttpResponse("ok")
280 # else, a User is trying to log in using an identity provider
281 except FederatedIendityProvider.DoesNotExist:
282 # Manually checking for csrf to protect the code below
283 reason = CsrfViewMiddleware().process_view(request, None, (), {})
284 if reason is not None: # pragma: no cover (csrf checks are disabled during tests)
285 return reason # Failed the test, stop here.
286 form = forms.FederateSelect(request.POST)
287 if form.is_valid():
288 params = utils.copy_params(
289 request.POST,
290 ignore={"provider", "csrfmiddlewaretoken", "ticket", "lt"}
291 )
292 if params.get("renew") == "False":
293 del params["renew"]
294 url = utils.reverse_params(
295 "cas_server:federateAuth",
296 kwargs=dict(provider=form.cleaned_data["provider"].suffix),
297 params=params
298 )
299 return HttpResponseRedirect(url)
300 else:
301 return redirect("cas_server:login")
303 def get(self, request, provider=None):
304 """
305 method called on GET request
307 :param django.http.HttpRequestself. request: The current request object
308 :param unicode provider: Optional parameter. The user provider suffix.
309 """
310 # if settings.CAS_FEDERATE is not True redirect to the login page
311 if not settings.CAS_FEDERATE:
312 logger.warning("CAS_FEDERATE is False, set it to True to use federation")
313 return redirect("cas_server:login")
314 renew = bool(request.GET.get('renew') and request.GET['renew'] != "False")
315 # Is the user is already authenticated, no need to request authentication to the user
316 # identity provider.
317 if self.request.session.get("authenticated") and not renew:
318 logger.warning("User already authenticated, dropping federated authentication request")
319 return redirect("cas_server:login")
320 try:
321 # get the identity provider from its suffix
322 provider = FederatedIendityProvider.objects.get(suffix=provider)
323 # get a CAS client for the user identity provider
324 auth = self.get_cas_client(request, provider, renew)
325 # if no ticket submited, redirect to the identity provider CAS login page
326 if 'ticket' not in request.GET:
327 logger.info("Trying to authenticate %s again" % auth.provider.server_url)
328 return HttpResponseRedirect(auth.get_login_url())
329 else:
330 ticket = request.GET['ticket']
331 try:
332 # if the ticket validation succeed
333 if auth.verify_ticket(ticket):
334 logger.info(
335 "Got a valid ticket for %s from %s" % (
336 auth.username,
337 auth.provider.server_url
338 )
339 )
340 params = utils.copy_params(request.GET, ignore={"ticket", "remember"})
341 request.session["federate_username"] = auth.federated_username
342 request.session["federate_ticket"] = ticket
343 auth.register_slo(
344 auth.federated_username,
345 request.session.session_key,
346 ticket
347 )
348 # redirect to the the login page for the user to become authenticated
349 # thanks to the `federate_username` and `federate_ticket` session parameters
350 url = utils.reverse_params("cas_server:login", params)
351 response = HttpResponseRedirect(url)
352 # If the user has checked "remember my identity provider" store it in a
353 # cookie
354 if request.GET.get("remember"):
355 max_age = settings.CAS_FEDERATE_REMEMBER_TIMEOUT
356 utils.set_cookie(
357 response,
358 "remember_provider",
359 provider.suffix,
360 max_age
361 )
362 return response
363 # else redirect to the identity provider CAS login page
364 else:
365 logger.info(
366 (
367 "Got an invalid ticket %s from %s for service %s. "
368 "Retrying authentication"
369 ) % (
370 ticket,
371 auth.provider.server_url,
372 self.service_url
373 )
374 )
375 return HttpResponseRedirect(auth.get_login_url())
376 # both xml.etree.ElementTree and lxml.etree exceptions inherit from SyntaxError
377 except SyntaxError as error:
378 messages.add_message(
379 request,
380 messages.ERROR,
381 _(
382 u"Invalid response from your identity provider CAS upon "
383 u"ticket %(ticket)s validation: %(error)r"
384 ) % {'ticket': ticket, 'error': error}
385 )
386 response = redirect("cas_server:login")
387 response.delete_cookie("remember_provider")
388 return response
389 except FederatedIendityProvider.DoesNotExist:
390 logger.warning("Identity provider suffix %s not found" % provider)
391 # if the identity provider is not found, redirect to the login page
392 return redirect("cas_server:login")
395class LoginView(View, LogoutMixin):
396 """credential requestor / acceptor"""
398 # pylint: disable=too-many-instance-attributes
399 # Nine is reasonable in this case.
401 #: The current :class:`models.User<cas_server.models.User>` object
402 user = None
403 #: The form to display to the user
404 form = None
406 #: current :class:`django.http.HttpRequest` object
407 request = None
408 #: service GET/POST parameter
409 service = None
410 #: ``True`` if renew GET/POST parameter is present and not "False"
411 renew = None
412 #: the warn GET/POST parameter
413 warn = None
414 #: the gateway GET/POST parameter
415 gateway = None
416 #: the method GET/POST parameter
417 method = None
419 #: ``True`` if the HTTP_X_AJAX http header is sent and ``settings.CAS_ENABLE_AJAX_AUTH``
420 #: is ``True``, ``False`` otherwise.
421 ajax = None
423 #: ``True`` if the user has just authenticated
424 renewed = False
425 #: ``True`` if renew GET/POST parameter is present and not "False"
426 warned = False
428 #: The :class:`FederateAuth` transmited username (only used if ``settings.CAS_FEDERATE``
429 #: is ``True``)
430 username = None
431 #: The :class:`FederateAuth` transmited ticket (only used if ``settings.CAS_FEDERATE`` is
432 #: ``True``)
433 ticket = None
435 INVALID_LOGIN_TICKET = 1
436 USER_LOGIN_OK = 2
437 USER_LOGIN_FAILURE = 3
438 USER_ALREADY_LOGGED = 4
439 USER_AUTHENTICATED = 5
440 USER_NOT_AUTHENTICATED = 6
442 def init_post(self, request):
443 """
444 Initialize POST received parameters
446 :param django.http.HttpRequest request: The current request object
447 """
448 self.request = request
449 self.service = request.POST.get('service')
450 self.renew = bool(request.POST.get('renew') and request.POST['renew'] != "False")
451 self.gateway = request.POST.get('gateway')
452 self.method = request.POST.get('method')
453 self.ajax = settings.CAS_ENABLE_AJAX_AUTH and 'HTTP_X_AJAX' in request.META
454 if request.POST.get('warned') and request.POST['warned'] != "False":
455 self.warned = True
456 self.warn = request.POST.get('warn')
457 if settings.CAS_FEDERATE:
458 self.username = request.POST.get('username')
459 # in federated mode, the valdated indentity provider CAS ticket is used as password
460 self.ticket = request.POST.get('password')
462 def gen_lt(self):
463 """Generate a new LoginTicket and add it to the list of valid LT for the user"""
464 self.request.session['lt'] = self.request.session.get('lt', []) + [utils.gen_lt()]
465 if len(self.request.session['lt']) > 100:
466 self.request.session['lt'] = self.request.session['lt'][-100:]
468 def check_lt(self):
469 """
470 Check is the POSTed LoginTicket is valid, if yes invalide it
472 :return: ``True`` if the LoginTicket is valid, ``False`` otherwise
473 :rtype: bool
474 """
475 # save LT for later check
476 lt_valid = self.request.session.get('lt', [])
477 lt_send = self.request.POST.get('lt')
478 # generate a new LT (by posting the LT has been consumed)
479 self.gen_lt()
480 # check if send LT is valid
481 if lt_send not in lt_valid:
482 return False
483 else:
484 self.request.session['lt'].remove(lt_send)
485 # we need to redo the affectation for django to detect that the list has changed
486 # and for its new value to be store in the session
487 self.request.session['lt'] = self.request.session['lt']
488 return True
490 def post(self, request, *args, **kwargs):
491 """
492 method called on POST request on this view
494 :param django.http.HttpRequest request: The current request object
495 """
496 # initialize class parameters
497 self.init_post(request)
498 # process the POST request
499 ret = self.process_post()
500 if ret == self.INVALID_LOGIN_TICKET:
501 messages.add_message(
502 self.request,
503 messages.ERROR,
504 _(u"Invalid login ticket, please try to log in again")
505 )
506 elif ret == self.USER_LOGIN_OK:
507 # On successful login, update the :class:`models.User<cas_server.models.User>` ``date``
508 # attribute by saving it. (``auto_now=True``)
509 self.user = models.User.objects.get_or_create(
510 username=self.request.session['username'],
511 session_key=self.request.session.session_key
512 )[0]
513 self.user.last_login = timezone.now()
514 self.user.save()
515 elif ret == self.USER_LOGIN_FAILURE: # bad user login
516 if settings.CAS_FEDERATE:
517 self.ticket = None
518 self.username = None
519 self.init_form()
520 # preserve valid LoginTickets from session flush
521 lt = self.request.session.get('lt', [])
522 # On login failure, flush the session
523 self.logout()
524 # restore valid LoginTickets
525 self.request.session['lt'] = lt
526 elif ret == self.USER_ALREADY_LOGGED:
527 pass
528 else: # pragma: no cover (should no happen)
529 raise EnvironmentError("invalid output for LoginView.process_post")
530 # call the GET/POST common part
531 response = self.common()
532 if self.warn:
533 utils.set_cookie(
534 response,
535 "warn",
536 "on",
537 10 * 365 * 24 * 3600
538 )
539 else:
540 response.delete_cookie("warn")
541 return response
543 def process_post(self):
544 """
545 Analyse the POST request:
547 * check that the LoginTicket is valid
548 * check that the user sumited credentials are valid
550 :return:
551 * :attr:`INVALID_LOGIN_TICKET` if the POSTed LoginTicket is not valid
552 * :attr:`USER_ALREADY_LOGGED` if the user is already logged and do no request
553 reauthentication.
554 * :attr:`USER_LOGIN_FAILURE` if the user is not logged or request for
555 reauthentication and his credentials are not valid
556 * :attr:`USER_LOGIN_OK` if the user is not logged or request for
557 reauthentication and his credentials are valid
558 :rtype: int
559 """
560 if not self.check_lt():
561 self.init_form(self.request.POST)
562 logger.warning("Received an invalid login ticket")
563 return self.INVALID_LOGIN_TICKET
564 elif not self.request.session.get("authenticated") or self.renew:
565 # authentication request receive, initialize the form to use
566 self.init_form(self.request.POST)
567 if self.form.is_valid():
568 self.request.session.set_expiry(0)
569 self.request.session["username"] = self.form.cleaned_data['username']
570 self.request.session["warn"] = True if self.form.cleaned_data.get("warn") else False
571 self.request.session["authenticated"] = True
572 self.renewed = True
573 self.warned = True
574 logger.info("User %s successfully authenticated" % self.request.session["username"])
575 return self.USER_LOGIN_OK
576 else:
577 logger.warning("A login attempt failed")
578 return self.USER_LOGIN_FAILURE
579 else:
580 logger.warning("Received a login attempt for an already-active user")
581 return self.USER_ALREADY_LOGGED
583 def init_get(self, request):
584 """
585 Initialize GET received parameters
587 :param django.http.HttpRequest request: The current request object
588 """
589 self.request = request
590 self.service = request.GET.get('service')
591 self.renew = bool(request.GET.get('renew') and request.GET['renew'] != "False")
592 self.gateway = request.GET.get('gateway')
593 self.method = request.GET.get('method')
594 self.ajax = settings.CAS_ENABLE_AJAX_AUTH and 'HTTP_X_AJAX' in request.META
595 self.warn = request.GET.get('warn')
596 if settings.CAS_FEDERATE:
597 # here username and ticket are fetch from the session after a redirection from
598 # FederateAuth.get
599 self.username = request.session.get("federate_username")
600 self.ticket = request.session.get("federate_ticket")
601 if self.username:
602 del request.session["federate_username"]
603 if self.ticket:
604 del request.session["federate_ticket"]
606 def get(self, request, *args, **kwargs):
607 """
608 method called on GET request on this view
610 :param django.http.HttpRequest request: The current request object
611 """
612 # initialize class parameters
613 self.init_get(request)
614 # process the GET request
615 self.process_get()
616 # call the GET/POST common part
617 return self.common()
619 def process_get(self):
620 """
621 Analyse the GET request
623 :return:
624 * :attr:`USER_NOT_AUTHENTICATED` if the user is not authenticated or is requesting
625 for authentication renewal
626 * :attr:`USER_AUTHENTICATED` if the user is authenticated and is not requesting
627 for authentication renewal
628 :rtype: int
629 """
630 # generate a new LT
631 self.gen_lt()
632 if not self.request.session.get("authenticated") or self.renew:
633 # authentication will be needed, initialize the form to use
634 self.init_form()
635 return self.USER_NOT_AUTHENTICATED
636 return self.USER_AUTHENTICATED
638 def init_form(self, values=None):
639 """
640 Initialization of the good form depending of POST and GET parameters
642 :param django.http.QueryDict values: A POST or GET QueryDict
643 """
644 if values:
645 values = values.copy()
646 values['lt'] = self.request.session['lt'][-1]
647 form_initial = {
648 'service': self.service,
649 'method': self.method,
650 'warn': (
651 self.warn or self.request.session.get("warn") or self.request.COOKIES.get('warn')
652 ),
653 'lt': self.request.session['lt'][-1],
654 'renew': self.renew
655 }
656 if settings.CAS_FEDERATE:
657 if self.username and self.ticket:
658 form_initial['username'] = self.username
659 form_initial['password'] = self.ticket
660 form_initial['ticket'] = self.ticket
661 self.form = forms.FederateUserCredential(
662 values,
663 initial=form_initial
664 )
665 else:
666 self.form = forms.FederateSelect(values, initial=form_initial)
667 else:
668 self.form = forms.UserCredential(
669 values,
670 initial=form_initial
671 )
673 def service_login(self):
674 """
675 Perform login against a service
677 :return:
678 * The rendering of the ``settings.CAS_WARN_TEMPLATE`` if the user asked to be
679 warned before ticket emission and has not yep been warned.
680 * The redirection to the service URL with a ticket GET parameter
681 * The redirection to the service URL without a ticket if ticket generation failed
682 and the :attr:`gateway` attribute is set
683 * The rendering of the ``settings.CAS_LOGGED_TEMPLATE`` template with some error
684 messages if the ticket generation failed (e.g: user not allowed).
685 :rtype: django.http.HttpResponse
686 """
687 try:
688 # is the service allowed
689 service_pattern = ServicePattern.validate(self.service)
690 # is the current user allowed on this service
691 service_pattern.check_user(self.user)
692 # if the user has asked to be warned before any login to a service
693 if self.request.session.get("warn", True) and not self.warned:
694 messages.add_message(
695 self.request,
696 messages.WARNING,
697 _(u"Authentication has been required by service %(name)s (%(url)s)") %
698 {'name': service_pattern.name, 'url': self.service}
699 )
700 if self.ajax:
701 data = {"status": "error", "detail": "confirmation needed"}
702 return json_response(self.request, data)
703 else:
704 warn_form = forms.WarnForm(initial={
705 'service': self.service,
706 'renew': self.renew,
707 'gateway': self.gateway,
708 'method': self.method,
709 'warned': True,
710 'lt': self.request.session['lt'][-1]
711 })
712 return render(
713 self.request,
714 settings.CAS_WARN_TEMPLATE,
715 utils.context({'form': warn_form})
716 )
717 else:
718 # redirect, using method ?
719 list(messages.get_messages(self.request)) # clean messages before leaving django
720 redirect_url = self.user.get_service_url(
721 self.service,
722 service_pattern,
723 renew=self.renewed
724 )
725 if not self.ajax:
726 return HttpResponseRedirect(redirect_url)
727 else:
728 data = {"status": "success", "detail": "auth", "url": redirect_url}
729 return json_response(self.request, data)
730 except ServicePattern.DoesNotExist:
731 error = 1
732 messages.add_message(
733 self.request,
734 messages.ERROR,
735 _(u'Service %(url)s not allowed.') % {'url': self.service}
736 )
737 except models.BadUsername:
738 error = 2
739 messages.add_message(
740 self.request,
741 messages.ERROR,
742 _(u"Username not allowed")
743 )
744 except models.BadFilter:
745 error = 3
746 messages.add_message(
747 self.request,
748 messages.ERROR,
749 _(u"User characteristics not allowed")
750 )
751 except models.UserFieldNotDefined:
752 error = 4
753 messages.add_message(
754 self.request,
755 messages.ERROR,
756 _(u"The attribute %(field)s is needed to use"
757 u" that service") % {'field': service_pattern.user_field}
758 )
760 # if gateway is set and auth failed redirect to the service without authentication
761 if self.gateway and not self.ajax:
762 list(messages.get_messages(self.request)) # clean messages before leaving django
763 return HttpResponseRedirect(self.service)
765 if not self.ajax:
766 return render(
767 self.request,
768 settings.CAS_LOGGED_TEMPLATE,
769 utils.context({'session': self.request.session})
770 )
771 else:
772 data = {"status": "error", "detail": "auth", "code": error}
773 return json_response(self.request, data)
775 def authenticated(self):
776 """
777 Processing authenticated users
779 :return:
780 * The returned value of :meth:`service_login` if :attr:`service` is defined
781 * The rendering of ``settings.CAS_LOGGED_TEMPLATE`` otherwise
782 :rtype: django.http.HttpResponse
783 """
784 # Try to get the current :class:`models.User<cas_server.models.User>` object for the current
785 # session
786 try:
787 self.user = models.User.objects.get(
788 username=self.request.session.get("username"),
789 session_key=self.request.session.session_key
790 )
791 # if not found, flush the session and redirect to the login page
792 except models.User.DoesNotExist:
793 logger.warning(
794 "User %s seems authenticated but is not found in the database." % (
795 self.request.session.get("username"),
796 )
797 )
798 self.logout()
799 if self.ajax:
800 data = {
801 "status": "error",
802 "detail": "login required",
803 "url": utils.reverse_params("cas_server:login", params=self.request.GET)
804 }
805 return json_response(self.request, data)
806 else:
807 return utils.redirect_params("cas_server:login", params=self.request.GET)
809 # if login against a service
810 if self.service:
811 return self.service_login()
812 # else display the logged template
813 else:
814 if self.ajax:
815 data = {"status": "success", "detail": "logged"}
816 return json_response(self.request, data)
817 else:
818 return render(
819 self.request,
820 settings.CAS_LOGGED_TEMPLATE,
821 utils.context({'session': self.request.session})
822 )
824 def not_authenticated(self):
825 """
826 Processing non authenticated users
828 :return:
829 * The rendering of ``settings.CAS_LOGIN_TEMPLATE`` with various messages
830 depending of GET/POST parameters
831 * The redirection to :class:`FederateAuth` if ``settings.CAS_FEDERATE`` is ``True``
832 and the "remember my identity provider" cookie is found
833 :rtype: django.http.HttpResponse
834 """
835 if self.service:
836 try:
837 service_pattern = ServicePattern.validate(self.service)
838 if self.gateway and not self.ajax:
839 # clean messages before leaving django
840 list(messages.get_messages(self.request))
841 return HttpResponseRedirect(self.service)
843 if settings.CAS_SHOW_SERVICE_MESSAGES:
844 if self.request.session.get("authenticated") and self.renew:
845 messages.add_message(
846 self.request,
847 messages.WARNING,
848 _(u"Authentication renewal required by service %(name)s (%(url)s).") %
849 {'name': service_pattern.name, 'url': self.service}
850 )
851 else:
852 messages.add_message(
853 self.request,
854 messages.WARNING,
855 _(u"Authentication required by service %(name)s (%(url)s).") %
856 {'name': service_pattern.name, 'url': self.service}
857 )
858 except ServicePattern.DoesNotExist:
859 if settings.CAS_SHOW_SERVICE_MESSAGES:
860 messages.add_message(
861 self.request,
862 messages.ERROR,
863 _(u'Service %s not allowed') % self.service
864 )
865 if self.ajax:
866 data = {
867 "status": "error",
868 "detail": "login required",
869 "url": utils.reverse_params("cas_server:login", params=self.request.GET)
870 }
871 return json_response(self.request, data)
872 else:
873 if settings.CAS_FEDERATE:
874 if self.username and self.ticket:
875 return render(
876 self.request,
877 settings.CAS_LOGIN_TEMPLATE,
878 utils.context({
879 'form': self.form,
880 'auto_submit': True,
881 'post_url': reverse("cas_server:login")
882 })
883 )
884 else:
885 if (
886 self.request.COOKIES.get('remember_provider') and
887 FederatedIendityProvider.objects.filter(
888 suffix=self.request.COOKIES['remember_provider']
889 )
890 ):
891 params = utils.copy_params(self.request.GET)
892 url = utils.reverse_params(
893 "cas_server:federateAuth",
894 params=params,
895 kwargs=dict(provider=self.request.COOKIES['remember_provider'])
896 )
897 return HttpResponseRedirect(url)
898 else:
899 # if user is authenticated and auth renewal is requested, redirect directly
900 # to the user identity provider
901 if self.renew and self.request.session.get("authenticated"):
902 try:
903 user = FederatedUser.get_from_federated_username(
904 self.request.session.get("username")
905 )
906 params = utils.copy_params(self.request.GET)
907 url = utils.reverse_params(
908 "cas_server:federateAuth",
909 params=params,
910 kwargs=dict(provider=user.provider.suffix)
911 )
912 return HttpResponseRedirect(url)
913 # Should normally not happen: if the user is logged, it exists in the
914 # database.
915 except FederatedUser.DoesNotExist: # pragma: no cover
916 pass
917 return render(
918 self.request,
919 settings.CAS_LOGIN_TEMPLATE,
920 utils.context({
921 'form': self.form,
922 'post_url': reverse("cas_server:federateAuth")
923 })
924 )
925 else:
926 return render(
927 self.request,
928 settings.CAS_LOGIN_TEMPLATE,
929 utils.context({'form': self.form})
930 )
932 def common(self):
933 """
934 Common part execute uppon GET and POST request
936 :return:
937 * The returned value of :meth:`authenticated` if the user is authenticated and
938 not requesting for authentication or if the authentication has just been renewed
939 * The returned value of :meth:`not_authenticated` otherwise
940 :rtype: django.http.HttpResponse
941 """
942 # if authenticated and successfully renewed authentication if needed
943 if self.request.session.get("authenticated") and (not self.renew or self.renewed):
944 return self.authenticated()
945 else:
946 return self.not_authenticated()
949class Auth(CsrfExemptView):
950 """
951 A simple view to validate username/password/service tuple
953 csrf is disable as it is intended to be used by programs. Security is assured by a shared
954 secret between the programs dans django-cas-server.
955 """
957 @staticmethod
958 def post(request):
959 """
960 method called on POST request on this view
962 :param django.http.HttpRequest request: The current request object
963 :return: ``HttpResponse(u"yes\\n")`` if the POSTed tuple (username, password, service)
964 if valid (i.e. (username, password) is valid dans username is allowed on service).
965 ``HttpResponse(u"no\\n…")`` otherwise, with possibly an error message on the second
966 line.
967 :rtype: django.http.HttpResponse
968 """
969 username = request.POST.get('username')
970 password = request.POST.get('password')
971 service = request.POST.get('service')
972 secret = request.POST.get('secret')
974 if not settings.CAS_AUTH_SHARED_SECRET:
975 return HttpResponse(
976 "no\nplease set CAS_AUTH_SHARED_SECRET",
977 content_type="text/plain; charset=utf-8"
978 )
979 if secret != settings.CAS_AUTH_SHARED_SECRET:
980 return HttpResponse(u"no\n", content_type="text/plain; charset=utf-8")
981 if not username or not password or not service:
982 return HttpResponse(u"no\n", content_type="text/plain; charset=utf-8")
983 form = forms.UserCredential(
984 request.POST,
985 initial={
986 'service': service,
987 'method': 'POST',
988 'warn': False
989 }
990 )
991 if form.is_valid():
992 try:
993 user = models.User.objects.get_or_create(
994 username=form.cleaned_data['username'],
995 session_key=request.session.session_key
996 )[0]
997 user.save()
998 # is the service allowed
999 service_pattern = ServicePattern.validate(service)
1000 # is the current user allowed on this service
1001 service_pattern.check_user(user)
1002 if not request.session.get("authenticated"):
1003 user.delete()
1004 return HttpResponse(u"yes\n", content_type="text/plain; charset=utf-8")
1005 except (ServicePattern.DoesNotExist, models.ServicePatternException):
1006 return HttpResponse(u"no\n", content_type="text/plain; charset=utf-8")
1007 else:
1008 return HttpResponse(u"no\n", content_type="text/plain; charset=utf-8")
1011class Validate(View):
1012 """service ticket validation"""
1013 @staticmethod
1014 def get(request):
1015 """
1016 method called on GET request on this view
1018 :param django.http.HttpRequest request: The current request object
1019 :return:
1020 * ``HttpResponse("yes\\nusername")`` if submited (service, ticket) is valid
1021 * else ``HttpResponse("no\\n")``
1022 :rtype: django.http.HttpResponse
1023 """
1024 # store wanted GET parameters
1025 service = request.GET.get('service')
1026 ticket = request.GET.get('ticket')
1027 renew = True if request.GET.get('renew') else False
1028 # service and ticket parameters are mandatory
1029 if service and ticket:
1030 try:
1031 # search for the ticket, associated at service that is not yet validated but is
1032 # still valid
1033 ticket = ServiceTicket.get(ticket, renew, service)
1034 logger.info(
1035 "Validate: Service ticket %s validated, user %s authenticated on service %s" % (
1036 ticket.value,
1037 ticket.user.username,
1038 ticket.service
1039 )
1040 )
1041 return HttpResponse(
1042 u"yes\n%s\n" % ticket.username(),
1043 content_type="text/plain; charset=utf-8"
1044 )
1045 except ServiceTicket.DoesNotExist:
1046 logger.warning(
1047 (
1048 "Validate: Service ticket %s not found or "
1049 "already validated, auth to %s failed"
1050 ) % (
1051 ticket,
1052 service
1053 )
1054 )
1055 return HttpResponse(u"no\n", content_type="text/plain; charset=utf-8")
1056 else:
1057 logger.warning("Validate: service or ticket missing")
1058 return HttpResponse(u"no\n", content_type="text/plain; charset=utf-8")
1061@python_2_unicode_compatible
1062class ValidationBaseError(Exception):
1063 """Base class for both saml and cas validation error"""
1065 #: The error code
1066 code = None
1067 #: The error message
1068 msg = None
1070 def __init__(self, code, msg=""):
1071 self.code = code
1072 self.msg = msg
1073 super(ValidationBaseError, self).__init__(code)
1075 def __str__(self):
1076 return u"%s" % self.msg
1078 def render(self, request):
1079 """
1080 render the error template for the exception
1082 :param django.http.HttpRequest request: The current request object:
1083 :return: the rendered ``cas_server/serviceValidateError.xml`` template
1084 :rtype: django.http.HttpResponse
1085 """
1086 return render(
1087 request,
1088 self.template,
1089 self.context(), content_type="text/xml; charset=utf-8"
1090 )
1093class ValidateError(ValidationBaseError):
1094 """handle service validation error"""
1096 #: template to be render for the error
1097 template = "cas_server/serviceValidateError.xml"
1099 def context(self):
1100 """
1101 content to use to render :attr:`template`
1103 :return: A dictionary to contextualize :attr:`template`
1104 :rtype: dict
1105 """
1106 return {'code': self.code, 'msg': self.msg}
1109class ValidateService(View):
1110 """service ticket validation [CAS 2.0] and [CAS 3.0]"""
1111 #: Current :class:`django.http.HttpRequest` object
1112 request = None
1113 #: The service GET parameter
1114 service = None
1115 #: the ticket GET parameter
1116 ticket = None
1117 #: the pgtUrl GET parameter
1118 pgt_url = None
1119 #: the renew GET parameter
1120 renew = None
1121 #: specify if ProxyTicket are allowed by the view. Hence we user the same view for
1122 #: ``/serviceValidate`` and ``/proxyValidate`` juste changing the parameter.
1123 allow_proxy_ticket = False
1125 def get(self, request):
1126 """
1127 method called on GET request on this view
1129 :param django.http.HttpRequest request: The current request object:
1130 :return: The rendering of ``cas_server/serviceValidate.xml`` if no errors is raised,
1131 the rendering or ``cas_server/serviceValidateError.xml`` otherwise.
1132 :rtype: django.http.HttpResponse
1133 """
1134 # define the class parameters
1135 self.request = request
1136 self.service = request.GET.get('service')
1137 self.ticket = request.GET.get('ticket')
1138 self.pgt_url = request.GET.get('pgtUrl')
1139 self.renew = True if request.GET.get('renew') else False
1141 # service and ticket parameter are mandatory
1142 if not self.service or not self.ticket:
1143 logger.warning("ValidateService: missing ticket or service")
1144 return ValidateError(
1145 u'INVALID_REQUEST',
1146 u"you must specify a service and a ticket"
1147 ).render(request)
1148 else:
1149 try:
1150 # search the ticket in the database
1151 self.ticket, proxies = self.process_ticket()
1152 # prepare template rendering context
1153 params = {
1154 'username': self.ticket.username(),
1155 'attributes': self.ticket.attributs_flat(),
1156 'proxies': proxies,
1157 'auth_date': self.ticket.user.last_login.replace(microsecond=0).isoformat(),
1158 'is_new_login': 'true' if self.ticket.renew else 'false'
1159 }
1160 # if pgtUrl is set, require https or localhost
1161 if self.pgt_url and (
1162 self.pgt_url.startswith("https://") or
1163 re.match(r"^http://(127\.0\.0\.1|localhost)(:[0-9]+)?(/.*)?$", self.pgt_url)
1164 ):
1165 return self.process_pgturl(params)
1166 else:
1167 logger.info(
1168 "ValidateService: ticket %s validated for user %s on service %s." % (
1169 self.ticket.value,
1170 self.ticket.user.username,
1171 self.ticket.service
1172 )
1173 )
1174 logger.debug(
1175 "ValidateService: User attributs are:\n%s" % (
1176 pprint.pformat(self.ticket.attributs),
1177 )
1178 )
1179 return render(
1180 request,
1181 "cas_server/serviceValidate.xml",
1182 params,
1183 content_type="text/xml; charset=utf-8"
1184 )
1185 except ValidateError as error:
1186 logger.warning(
1187 "ValidateService: validation error: %s %s" % (error.code, error.msg)
1188 )
1189 return error.render(request)
1191 def process_ticket(self):
1192 """
1193 fetch the ticket against the database and check its validity
1195 :raises ValidateError: if the ticket is not found or not valid, potentially for that
1196 service
1197 :returns: A couple (ticket, proxies list)
1198 :rtype: :obj:`tuple`
1199 """
1200 try:
1201 proxies = []
1202 if self.allow_proxy_ticket:
1203 ticket = models.Ticket.get(self.ticket, self.renew)
1204 else:
1205 ticket = models.ServiceTicket.get(self.ticket, self.renew)
1206 try:
1207 for prox in ticket.proxies.all():
1208 proxies.append(prox.url)
1209 except AttributeError:
1210 pass
1211 if ticket.service != self.service:
1212 raise ValidateError(u'INVALID_SERVICE', self.service)
1213 return ticket, proxies
1214 except Ticket.DoesNotExist:
1215 raise ValidateError(u'INVALID_TICKET', self.ticket)
1216 except (ServiceTicket.DoesNotExist, ProxyTicket.DoesNotExist):
1217 raise ValidateError(u'INVALID_TICKET', 'ticket not found')
1219 def process_pgturl(self, params):
1220 """
1221 Handle PGT request
1223 :param dict params: A template context dict
1224 :raises ValidateError: if pgtUrl is invalid or if TLS validation of the pgtUrl fails
1225 :return: The rendering of ``cas_server/serviceValidate.xml``, using ``params``
1226 :rtype: django.http.HttpResponse
1227 """
1228 try:
1229 pattern = ServicePattern.validate(self.pgt_url)
1230 if pattern.proxy_callback:
1231 proxyid = utils.gen_pgtiou()
1232 pticket = ProxyGrantingTicket.objects.create(
1233 user=self.ticket.user,
1234 service=self.pgt_url,
1235 service_pattern=pattern,
1236 single_log_out=pattern.single_log_out
1237 )
1238 url = utils.update_url(self.pgt_url, {'pgtIou': proxyid, 'pgtId': pticket.value})
1239 try:
1240 ret = requests.get(url, verify=settings.CAS_PROXY_CA_CERTIFICATE_PATH)
1241 if ret.status_code == 200:
1242 params['proxyGrantingTicket'] = proxyid
1243 else:
1244 pticket.delete()
1245 logger.info(
1246 (
1247 "ValidateService: ticket %s validated for user %s on service %s. "
1248 "Proxy Granting Ticket transmited to %s."
1249 ) % (
1250 self.ticket.value,
1251 self.ticket.user.username,
1252 self.ticket.service,
1253 self.pgt_url
1254 )
1255 )
1256 logger.debug(
1257 "ValidateService: User attributs are:\n%s" % (
1258 pprint.pformat(self.ticket.attributs),
1259 )
1260 )
1261 return render(
1262 self.request,
1263 "cas_server/serviceValidate.xml",
1264 params,
1265 content_type="text/xml; charset=utf-8"
1266 )
1267 except requests.exceptions.RequestException as error:
1268 error = utils.unpack_nested_exception(error)
1269 raise ValidateError(
1270 u'INVALID_PROXY_CALLBACK',
1271 u"%s: %s" % (type(error), str(error))
1272 )
1273 else:
1274 raise ValidateError(
1275 u'INVALID_PROXY_CALLBACK',
1276 u"callback url not allowed by configuration"
1277 )
1278 except ServicePattern.DoesNotExist:
1279 raise ValidateError(
1280 u'INVALID_PROXY_CALLBACK',
1281 u'callback url not allowed by configuration'
1282 )
1285class Proxy(View):
1286 """proxy ticket service"""
1288 #: Current :class:`django.http.HttpRequest` object
1289 request = None
1290 #: A ProxyGrantingTicket from the pgt GET parameter
1291 pgt = None
1292 #: the targetService GET parameter
1293 target_service = None
1295 def get(self, request):
1296 """
1297 method called on GET request on this view
1299 :param django.http.HttpRequest request: The current request object:
1300 :return: The returned value of :meth:`process_proxy` if no error is raised,
1301 else the rendering of ``cas_server/serviceValidateError.xml``.
1302 :rtype: django.http.HttpResponse
1303 """
1304 self.request = request
1305 self.pgt = request.GET.get('pgt')
1306 self.target_service = request.GET.get('targetService')
1307 try:
1308 # pgt and targetService parameters are mandatory
1309 if self.pgt and self.target_service:
1310 return self.process_proxy()
1311 else:
1312 raise ValidateError(
1313 u'INVALID_REQUEST',
1314 u"you must specify and pgt and targetService"
1315 )
1316 except ValidateError as error:
1317 logger.warning("Proxy: validation error: %s %s" % (error.code, error.msg))
1318 return error.render(request)
1320 def process_proxy(self):
1321 """
1322 handle PT request
1324 :raises ValidateError: if the PGT is not found, or the target service not allowed or
1325 the user not allowed on the tardet service.
1326 :return: The rendering of ``cas_server/proxy.xml``
1327 :rtype: django.http.HttpResponse
1328 """
1329 try:
1330 # is the target service allowed
1331 pattern = ServicePattern.validate(self.target_service)
1332 # to get a proxy ticket require that the service allow it
1333 if not pattern.proxy:
1334 raise ValidateError(
1335 u'UNAUTHORIZED_SERVICE',
1336 u'the service %s does not allow proxy tickets' % self.target_service
1337 )
1338 # is the proxy granting ticket valid
1339 ticket = ProxyGrantingTicket.get(self.pgt)
1340 # is the pgt user allowed on the target service
1341 pattern.check_user(ticket.user)
1342 pticket = ticket.user.get_ticket(
1343 ProxyTicket,
1344 self.target_service,
1345 pattern,
1346 renew=False
1347 )
1348 models.Proxy.objects.create(proxy_ticket=pticket, url=ticket.service)
1349 logger.info(
1350 "Proxy ticket created for user %s on service %s." % (
1351 ticket.user.username,
1352 self.target_service
1353 )
1354 )
1355 return render(
1356 self.request,
1357 "cas_server/proxy.xml",
1358 {'ticket': pticket.value},
1359 content_type="text/xml; charset=utf-8"
1360 )
1361 except (Ticket.DoesNotExist, ProxyGrantingTicket.DoesNotExist):
1362 raise ValidateError(u'INVALID_TICKET', u'PGT %s not found' % self.pgt)
1363 except ServicePattern.DoesNotExist:
1364 raise ValidateError(u'UNAUTHORIZED_SERVICE', self.target_service)
1365 except (models.BadUsername, models.BadFilter, models.UserFieldNotDefined):
1366 raise ValidateError(
1367 u'UNAUTHORIZED_USER',
1368 u'User %s not allowed on %s' % (ticket.user.username, self.target_service)
1369 )
1372class SamlValidateError(ValidationBaseError):
1373 """handle saml validation error"""
1375 #: template to be render for the error
1376 template = "cas_server/samlValidateError.xml"
1378 def context(self):
1379 """
1380 :return: A dictionary to contextualize :attr:`template`
1381 :rtype: dict
1382 """
1383 return {
1384 'code': self.code,
1385 'msg': self.msg,
1386 'IssueInstant': timezone.now().isoformat(),
1387 'ResponseID': utils.gen_saml_id()
1388 }
1391class SamlValidate(CsrfExemptView):
1392 """SAML ticket validation"""
1393 request = None
1394 target = None
1395 ticket = None
1396 root = None
1398 def post(self, request):
1399 """
1400 method called on POST request on this view
1402 :param django.http.HttpRequest request: The current request object
1403 :return: the rendering of ``cas_server/samlValidate.xml`` if no error is raised,
1404 else the rendering of ``cas_server/samlValidateError.xml``.
1405 :rtype: django.http.HttpResponse
1406 """
1407 self.request = request
1408 self.target = request.GET.get('TARGET')
1409 self.root = etree.fromstring(request.body)
1410 try:
1411 self.ticket = self.process_ticket()
1412 expire_instant = (self.ticket.creation +
1413 timedelta(seconds=self.ticket.VALIDITY)).isoformat()
1414 params = {
1415 'IssueInstant': timezone.now().isoformat(),
1416 'expireInstant': expire_instant,
1417 'Recipient': self.target,
1418 'ResponseID': utils.gen_saml_id(),
1419 'username': self.ticket.username(),
1420 'attributes': self.ticket.attributs_flat(),
1421 'auth_date': self.ticket.user.last_login.replace(microsecond=0).isoformat(),
1422 'is_new_login': 'true' if self.ticket.renew else 'false'
1424 }
1425 logger.info(
1426 "SamlValidate: ticket %s validated for user %s on service %s." % (
1427 self.ticket.value,
1428 self.ticket.user.username,
1429 self.ticket.service
1430 )
1431 )
1432 logger.debug(
1433 "SamlValidate: User attributes are:\n%s" % pprint.pformat(self.ticket.attributs)
1434 )
1436 return render(
1437 request,
1438 "cas_server/samlValidate.xml",
1439 params,
1440 content_type="text/xml; charset=utf-8"
1441 )
1442 except SamlValidateError as error:
1443 logger.warning("SamlValidate: validation error: %s %s" % (error.code, error.msg))
1444 return error.render(request)
1446 def process_ticket(self):
1447 """
1448 validate ticket from SAML XML body
1450 :raises: SamlValidateError: if the ticket is not found or not valid, or if we fail
1451 to parse the posted XML.
1452 :return: a ticket object
1453 :rtype: :class:`models.Ticket<cas_server.models.Ticket>`
1454 """
1455 try:
1456 auth_req = self.root.getchildren()[1].getchildren()[0]
1457 ticket = auth_req.getchildren()[0].text
1458 ticket = models.Ticket.get(ticket)
1459 if ticket.service != self.target:
1460 raise SamlValidateError(
1461 u'AuthnFailed',
1462 u'TARGET %s does not match ticket service' % self.target
1463 )
1464 return ticket
1465 except (IndexError, KeyError):
1466 raise SamlValidateError(u'VersionMismatch')
1467 except Ticket.DoesNotExist:
1468 raise SamlValidateError(
1469 u'AuthnFailed',
1470 u'ticket %s should begin with PT- or ST-' % ticket
1471 )
1472 except (ServiceTicket.DoesNotExist, ProxyTicket.DoesNotExist):
1473 raise SamlValidateError(u'AuthnFailed', u'ticket %s not found' % ticket)