django-allauth-cas/allauth_cas/views.py

255 lines
7.6 KiB
Python
Raw Normal View History

2017-07-25 18:31:42 +02:00
# -*- coding: utf-8 -*-
from django.http import HttpResponseRedirect
2017-12-29 18:19:01 +01:00
from django.utils.functional import cached_property
2017-07-25 18:31:42 +02:00
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,
)
from allauth.socialaccount.models import SocialLogin
2017-07-25 18:31:42 +02:00
import cas
from . import CAS_PROVIDER_SESSION_KEY
from .exceptions import CASAuthenticationError
class AuthAction(object):
AUTHENTICATE = 'authenticate'
REAUTHENTICATE = 'reauthenticate'
DEAUTHENTICATE = 'deauthenticate'
class CASAdapter(object):
2017-12-29 18:19:01 +01:00
#: CAS server url.
url = None
#: CAS server version.
#: Choices: ``1`` or ``'1'``, ``2`` or ``'2'``, ``3`` or ``'3'``,
#: ``'CAS_2_SAML_1_0'``
version = None
2017-07-25 18:31:42 +02:00
def __init__(self, request):
self.request = request
2017-12-29 18:19:01 +01:00
@cached_property
def renew(self):
2017-12-29 18:19:01 +01:00
"""Controls presence of ``renew`` in requests to the CAS server.
If ``True``, opt out single sign-on (SSO) functionality of the CAS
server. So that, user is always prompted for his username and password.
If ``False``, the CAS server does not prompt users for their
credentials if a SSO exists.
The default allows user to connect via an already used CAS server
with other credentials.
Returns:
``True`` if logged in user has already connected to Django using
**any** CAS provider in the current session, ``False`` otherwise.
"""
return CAS_PROVIDER_SESSION_KEY in self.request.session
2017-12-29 18:19:01 +01:00
@cached_property
def provider(self):
2017-07-25 18:31:42 +02:00
"""
Returns a provider instance for the current request.
"""
return providers.registry.by_id(self.provider_id, self.request)
def complete_login(self, request, response):
"""
2017-12-29 18:19:01 +01:00
Executed by the callback view after successful authentication on the
CAS server.
Args:
request
response (`dict`): Data returned by the CAS server.
``response[username]`` contains the user identifier for the
server, and may contain extra user-attributes.
Returns:
`SocialLogin()` object: State of the login-session.
2017-07-25 18:31:42 +02:00
"""
2017-12-29 18:19:01 +01:00
login = self.provider.sociallogin_from_response(request, response)
2017-07-25 18:31:42 +02:00
return login
def get_service_url(self, request):
2017-12-29 18:19:01 +01:00
"""The service url, used by the CAS client.
2017-07-25 18:31:42 +02:00
2017-12-29 18:19:01 +01:00
According to the CAS spec, the service url is passed by the CAS client
at several times. It must be the same for all interactions with the CAS
server.
2017-07-25 18:31:42 +02:00
2017-12-29 18:19:01 +01:00
It is used as redirection from the CAS server after a succssful
authentication. So, the callback url is used as service url.
2017-07-25 18:31:42 +02:00
2017-12-29 18:19:01 +01:00
If present, the GET param ``next`` is added to the service url.
2017-07-25 18:31:42 +02:00
"""
redirect_to = get_next_redirect_url(request)
callback_kwargs = {'next': redirect_to} if redirect_to else {}
2017-12-29 18:19:01 +01:00
callback_url = (
self.provider.get_callback_url(request, **callback_kwargs))
2017-07-25 18:31:42 +02:00
service_url = request.build_absolute_uri(callback_url)
return service_url
class CASView(object):
2017-12-29 18:19:01 +01:00
"""
Base class for CAS views.
"""
2017-07-25 18:31:42 +02:00
@classmethod
2017-12-29 18:19:01 +01:00
def adapter_view(cls, adapter):
"""Transform the view class into a view function.
2017-07-25 18:31:42 +02:00
2017-12-29 18:19:01 +01:00
Similar to the Django ``as_view()`` method.
2017-07-25 18:31:42 +02:00
2017-12-29 18:19:01 +01:00
Notes:
An (human) error page is rendered if any ``CASAuthenticationError``
is catched.
2017-07-25 18:31:42 +02:00
2017-12-29 18:19:01 +01:00
Args:
adapter (:class:`CASAdapter`): Provide specifics of a CAS server.
2017-07-25 18:31:42 +02:00
2017-12-29 18:19:01 +01:00
Returns:
A view function. The given adapter and related provider are
accessible as attributes from the view class.
2017-07-25 18:31:42 +02:00
"""
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)
2017-12-29 18:19:01 +01:00
self.provider = self.adapter.provider
2017-07-25 18:31:42 +02:00
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.
"""
auth_params = self.provider.get_auth_params(request, action)
2017-07-25 18:31:42 +02:00
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.provider.id)
2017-07-25 18:31:42 +02:00
class CASLoginView(CASView):
def dispatch(self, request):
"""
Redirects to the CAS server login page.
"""
action = request.GET.get('action', AuthAction.AUTHENTICATE)
SocialLogin.stash_state(request)
2017-07-25 18:31:42 +02:00
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.
"""
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)
2017-12-29 18:19:01 +01:00
uid, extra, _ = response
if not uid:
2017-07-25 18:31:42 +02:00
raise CASAuthenticationError(
"CAS server doesn't validate the ticket."
)
2017-12-29 18:19:01 +01:00
# Keep tracks of the last used CAS provider.
request.session[CAS_PROVIDER_SESSION_KEY] = self.provider.id
2017-07-25 18:31:42 +02:00
2018-10-21 15:20:23 +02:00
data = (uid, extra or {})
2017-12-29 18:19:01 +01:00
# Finish the login flow.
login = self.adapter.complete_login(request, data)
login.state = SocialLogin.unstash_state(request)
2017-07-25 18:31:42 +02:00
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):
"""
2017-12-29 18:19:01 +01:00
Returns the url to redirect after logout.
2017-07-25 18:31:42 +02:00
"""
request = self.request
return (
get_next_redirect_url(request) or
get_adapter(request).get_logout_redirect_url(request)
)