From aebbe6eb42091744b3ffa0b4ecd7b2dd96ddc6d0 Mon Sep 17 00:00:00 2001 From: Benjamin Renard Date: Thu, 4 May 2017 11:54:11 +0200 Subject: [PATCH] Add API method to generate auto-login link --- src/authentic2/api_urls.py | 4 ++ src/authentic2/api_views.py | 139 ++++++++++++++++++++++++++++++++++++++++- src/authentic2/app_settings.py | 3 + 3 files changed, 145 insertions(+), 1 deletion(-) diff --git a/src/authentic2/api_urls.py b/src/authentic2/api_urls.py index aa061f5..d4b0c44 100644 --- a/src/authentic2/api_urls.py +++ b/src/authentic2/api_urls.py @@ -11,5 +11,9 @@ urlpatterns = patterns('', name='a2-api-user'), url(r'^roles/(?P[\w+]*)/members/(?P[\w+]*)/$', api_views.roles, name='a2-api-role-member'), + url(r'^user/autologin$', api_views.userautologin, + name='a2-api-user-autologin'), + url(r'^user/autologin/(?P[\w: -]+)$', api_views.userautologinview, + name='autologin'), ) urlpatterns += api_views.router.urls diff --git a/src/authentic2/api_views.py b/src/authentic2/api_views.py index 971f61f..bc824d8 100644 --- a/src/authentic2/api_views.py +++ b/src/authentic2/api_views.py @@ -1,14 +1,23 @@ '''Views for Authentic2 API''' import logging import smtplib +import datetime +from hashlib import sha1 +from django import http from django.db import models -from django.contrib.auth import get_user_model +from django.contrib.auth import REDIRECT_FIELD_NAME, get_user_model +from django.core import signing +from django.core.cache import cache from django.core.exceptions import MultipleObjectsReturned from django.utils.translation import ugettext as _ +from django.utils.timezone import now +from django.utils.dateparse import parse_datetime from django.views.decorators.vary import vary_on_headers from django.views.decorators.cache import cache_control from django.shortcuts import get_object_or_404 +from django.views.generic.base import RedirectView +from django.core.urlresolvers import reverse from django_rbac.utils import get_ou_model, get_role_model @@ -29,6 +38,9 @@ from . import utils, decorators, attribute_kinds from .models import Attribute, PasswordReset from .a2_rbac.utils import get_default_ou +from utils import login +from . import app_settings + class HasUserAddPermission(permissions.BasePermission): def has_permission(self, request, view): @@ -36,6 +48,13 @@ class HasUserAddPermission(permissions.BasePermission): return False return True +class IsSuperUser(permissions.BasePermission): + """ + Allows access only to super users. + """ + + def has_permission(self, request, view): + return request.user and request.user.is_superuser class RegistrationSerializer(serializers.Serializer): '''Register RPC payload''' @@ -244,6 +263,124 @@ class PasswordChange(BaseRpcView): password_change = PasswordChange.as_view() +class UserAutoLoginSerializer(serializers.Serializer): + '''UserAutoLogin RPC payload''' + uuid = serializers.CharField( + required=False, allow_null=True) + email = serializers.EmailField( + required=False, allow_null=True) + ou = serializers.SlugRelatedField( + queryset=get_ou_model().objects.all(), + slug_field='slug', + required=False, allow_null=True) + expire_duration = serializers.IntegerField( + min_value=1, max_value=app_settings.A2_AUTH_TOKEN_MAX_LIFEMENT, + required=False, allow_null=False) + next_url = serializers.CharField( + required=False, allow_null=True) + + def validate(self, data): + User = get_user_model() + if data.get('uuid'): + qs = User.objects.filter(uuid=data['uuid']) + elif data.get('email'): + qs = User.objects.filter(email=data['email']) + else: + raise serializers.ValidationError("you must provide user's uuid or email") + if data.get('ou'): + qs = qs.filter(ou=data.get('ou')) + try: + self.user = qs.get() + data['uuid'] = self.user.uuid + except User.DoesNotExist: + raise serializers.ValidationError('no user found') + except MultipleObjectsReturned: + raise serializers.ValidationError('more than one user found') + return data + + +class UserAutoLogin(BaseRpcView): + permission_classes = (permissions.IsAuthenticated, + IsSuperUser) + + serializer_class = UserAutoLoginSerializer + + def rpc(self, request, serializer): + data = {} + data['uuid'] = serializer.validated_data.get('uuid') + expire_duration = serializer.validated_data.get('expire_duration', app_settings.A2_AUTH_TOKEN_DEFAULT_LIFETIME) + data['expire'] = (now()+datetime.timedelta(seconds=expire_duration)).isoformat() + if serializer.validated_data.get('ou'): + data['ou'] = serializer.validated_data.get('ou') + data[REDIRECT_FIELD_NAME] = serializer.validated_data.get('next_url') + token = signing.dumps(data) + autologinurl = request.build_absolute_uri( + reverse('autologin', kwargs={'token': token})) + return {'autologinurl': autologinurl}, status.HTTP_200_OK + +userautologin = UserAutoLogin.as_view() + +class UserAutoLoginView(RedirectView): + + def __init__(self, *args, **kwargs): + super(UserAutoLoginView, self).__init__(*args, **kwargs) + self.logger = logging.getLogger(__name__) + + def valid_token(self, token): + try: + result = signing.loads(token.replace(' ', '')) + expire = result.get('expire') + if expire: + if now()>=parse_datetime(expire): + self.logger.error(u'Specified token is expired') + else: + cache_first_use_key = 'token-%s' % sha1(token) + if cache.get(cache_first_use_key): + if now()>=(parse_datetime(cache.get(cache_first_use_key)+datetime.timedelta(0,app_settings.A2_AUTH_TOKEN_LIFETIME_AFTER_FIRST_USE))): + self.logger.error(u'Specified token was already used and expired') + return None + else: + cache.set(cache_first_use_key, now().isoformat(), app_settings.A2_AUTH_TOKEN_MAX_LIFEMENT) + return result + else: + self.logger.error(u'No expire in token') + except signing.BadSignature: + self.logger.error(u'Specified token is invalid') + return None + + def get_next_url(self, *args, **kwargs): + if kwargs.get('token') and kwargs.get('token').get(REDIRECT_FIELD_NAME): + return kwargs.get('token').get(REDIRECT_FIELD_NAME) + return reverse('auth_homepage') + + def get(self, request, *args, **kwargs): + token = self.valid_token(kwargs.get('token')) + + url = None + if token: + self.logger.info(u"Valid token : %s" % token) + User = get_user_model() + qs = User.objects.filter(uuid=token['uuid']) + if token.get('ou'): + qs = qs.filter(ou=token['ou']) + try: + user = qs.get() + self.logger.info(u"User : %s" % user) + user.backend = 'authentic2.backends.models_backend.ModelBackend' + login(request, user, 'token') + url = self.get_next_url(token = token) + self.logger.info(u"Redirect to : %s" % url) + except User.DoesNotExist: + self.logger.error(u'User %s from token does not exists' % token['uuid']) + except MultipleObjectsReturned: + self.logger.error(u"More than one user return from token's uuid (%s)" % token['uuid']) + + if not url: + url = reverse('auth_login') + + return http.HttpResponseRedirect(url) + +userautologinview = UserAutoLoginView.as_view() @vary_on_headers('Cookie', 'Origin', 'Referer') @cache_control(private=True, max_age=60) diff --git a/src/authentic2/app_settings.py b/src/authentic2/app_settings.py index 2c1f183..a9910fc 100644 --- a/src/authentic2/app_settings.py +++ b/src/authentic2/app_settings.py @@ -144,6 +144,9 @@ default_settings = dict( A2_PASSWORD_POLICY_REGEX=Setting(default=None, definition='Regular expression for validating passwords'), A2_PASSWORD_POLICY_REGEX_ERROR_MSG=Setting(default=None, definition='Error message to show when the password do not validate the regular expression'), A2_AUTH_PASSWORD_ENABLE=Setting(default=True, definition='Activate login/password authentication', names=('AUTH_PASSWORD',)), + A2_AUTH_TOKEN_DEFAULT_LIFETIME=Setting(default=1800, definition='Default lifetime of autologin tokens'), + A2_AUTH_TOKEN_MAX_LIFEMENT=Setting(default=172800, definition='Maximum allowed lifetime of autologin tokens'), + A2_AUTH_TOKEN_LIFETIME_AFTER_FIRST_USE=Setting(default=3600, definition='Maximum allowed lifetime of autologin tokens after first use'), A2_LOGIN_FAILURE_COUNT_BEFORE_WARNING=Setting(default=0, definition='Failure count before logging a warning to ' 'authentic2.user_login_failure. No warning will be send if value is ' -- 2.1.4