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