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