# -*- coding: utf-8 -*- import django from django.http import HttpResponseRedirect from django.utils.http import urlencode from allauth.account.adapter import get_adapter from allauth.account.utils import get_next_redirect_url from allauth.socialaccount import providers from allauth.socialaccount.helpers import ( complete_social_login, render_authentication_error, ) import cas from . import CAS_PROVIDER_SESSION_KEY from .exceptions import CASAuthenticationError if django.VERSION >= (1, 10): from django.urls import reverse else: from django.core.urlresolvers import reverse class AuthAction(object): AUTHENTICATE = 'authenticate' REAUTHENTICATE = 'reauthenticate' DEAUTHENTICATE = 'deauthenticate' class CASAdapter(object): # CAS client parameters renew = False def __init__(self, request): self.request = request def get_provider(self): """ Returns a provider instance for the current request. """ return providers.registry.by_id(self.provider_id, self.request) def complete_login(self, request, response): """ Executed by the callback view after successful authentication on CAS server. Returns the SocialLogin object which represents the state of the current login-session. """ login = (self.get_provider() .sociallogin_from_response(request, response)) return login def get_service_url(self, request): """ Returns the service url to for a CAS client. From CAS specification, the service url is used in order to redirect user after a successful login on CAS server. Also, service_url sent when ticket is verified must be the one for which ticket was issued. To conform this, the service url is always the callback url. A redirect url is found from the current request and appended as parameter to the service url and is latter used by the callback view to redirect user. """ redirect_to = get_next_redirect_url(request) callback_kwargs = {'next': redirect_to} if redirect_to else {} callback_url = self.get_callback_url(request, **callback_kwargs) service_url = request.build_absolute_uri(callback_url) return service_url def get_callback_url(self, request, **kwargs): """ Returns the callback url of the provider. Keyword arguments are set as query string. """ url = reverse(self.provider_id + '_callback') if kwargs: url += '?' + urlencode(kwargs) return url class CASView(object): @classmethod def adapter_view(cls, adapter, **kwargs): """ Similar to the Django as_view() method. It also setups a few things: - given adapter argument will be used in views internals. - if the view execution raises a CASAuthenticationError, the view renders an authentication error page. To use this: - subclass CAS adapter as wanted: class MyAdapter(CASAdapter): url = 'https://my.cas.url' - define views: login = views.CASLoginView.adapter_view(MyAdapter) callback = views.CASCallbackView.adapter_view(MyAdapter) logout = views.CASLogoutView.adapter_view(MyAdapter) """ def view(request, *args, **kwargs): # Prepare the func-view. self = cls() self.request = request self.args = args self.kwargs = kwargs # Setup and store adapter as view attribute. self.adapter = adapter(request) try: return self.dispatch(request, *args, **kwargs) except CASAuthenticationError: return self.render_error() return view def get_client(self, request, action=AuthAction.AUTHENTICATE): """ Returns the CAS client to interact with the CAS server. """ provider = self.adapter.get_provider() auth_params = provider.get_auth_params(request, action) service_url = self.adapter.get_service_url(request) client = cas.CASClient( service_url=service_url, server_url=self.adapter.url, version=self.adapter.version, renew=self.adapter.renew, extra_login_params=auth_params, ) return client def render_error(self): """ Returns an HTTP response in case an authentication failure happens. """ return render_authentication_error( self.request, self.adapter.provider_id, ) class CASLoginView(CASView): def dispatch(self, request): """ Redirects to the CAS server login page. """ action = request.GET.get('action', AuthAction.AUTHENTICATE) client = self.get_client(request, action=action) return HttpResponseRedirect(client.get_login_url()) class CASCallbackView(CASView): def dispatch(self, request): """ The CAS server redirects the user to this view after a successful authentication. On redirect, CAS server should add a ticket whose validity is verified here. If ticket is valid, CAS server may also return extra attributes about user. """ provider = self.adapter.get_provider() client = self.get_client(request) # CAS server should let a ticket. try: ticket = request.GET['ticket'] except KeyError: raise CASAuthenticationError( "CAS server didn't respond with a ticket." ) # Check ticket validity. # Response format on: # - success: username, attributes, pgtiou # - error: None, {}, None response = client.verify_ticket(ticket) if not response[0]: raise CASAuthenticationError( "CAS server doesn't validate the ticket." ) # The CAS provider in use is stored to propose to the user to # disconnect from the latter when he logouts. request.session[CAS_PROVIDER_SESSION_KEY] = provider.id # Finish the login flow login = self.adapter.complete_login(request, response) return complete_social_login(request, login) class CASLogoutView(CASView): def dispatch(self, request, next_page=None): """ Redirects to the CAS server logout page. next_page is used to let the CAS server send back the user. If empty, the redirect url is built on request data. """ action = AuthAction.DEAUTHENTICATE redirect_url = next_page or self.get_redirect_url() redirect_to = request.build_absolute_uri(redirect_url) client = self.get_client(request, action=action) return HttpResponseRedirect(client.get_logout_url(redirect_to)) def get_redirect_url(self): """ Returns the url to redirect after logout from current request. """ request = self.request return ( get_next_redirect_url(request) or get_adapter(request).get_logout_redirect_url(request) )