refactored authorization

This commit is contained in:
Anatoly 2019-08-30 11:53:19 +03:00
parent 46faf6ecee
commit 62ee83b1d4
15 changed files with 140 additions and 144 deletions

View File

@ -23,13 +23,14 @@ class UserManager(BaseUserManager):
use_in_migrations = False use_in_migrations = False
def make(self, username: str, email: str, def make(self, username: str, email: str, password: str,
password: str, newsletter: bool) -> object: newsletter: bool, is_active: bool = False) -> object:
"""Register new user""" """Register new user"""
obj = self.model( obj = self.model(
username=username, username=username,
email=email, email=email,
newsletter=newsletter newsletter=newsletter,
is_active=is_active
) )
obj.set_password(password) obj.set_password(password)
obj.save() obj.save()
@ -100,6 +101,11 @@ class User(ImageMixin, AbstractUser):
self.email_confirmed = True self.email_confirmed = True
self.save() self.save()
def approve(self):
"""Set user is_active status to True"""
self.is_active = True
self.save()
def remove_access_tokens(self, source: Union[int, Union[tuple, list]]): def remove_access_tokens(self, source: Union[int, Union[tuple, list]]):
"""Method to remove user access tokens""" """Method to remove user access tokens"""
source = source if isinstance(source, list) else [source, ] source = source if isinstance(source, list) else [source, ]
@ -118,12 +124,12 @@ class User(ImageMixin, AbstractUser):
token.revoke() token.revoke()
@property @property
def get_confirm_email_token(self): def confirm_email_token(self):
"""Make a token for finish signup.""" """Make a token for finish signup."""
return gm_token_generator.make_token(self) return gm_token_generator.make_token(self)
@property @property
def get_reset_password_token(self): def reset_password_token(self):
"""Make a token for finish signup.""" """Make a token for finish signup."""
return password_token_generator.make_token(self) return password_token_generator.make_token(self)
@ -136,9 +142,10 @@ class User(ImageMixin, AbstractUser):
"""Get confirm email template""" """Get confirm email template"""
return render_to_string( return render_to_string(
template_name=settings.CONFIRM_EMAIL_TEMPLATE, template_name=settings.CONFIRM_EMAIL_TEMPLATE,
context={'token': self.get_confirm_email_token, context={'token': self.confirm_email_token,
'uid': self.get_user_uidb64, 'uid': self.get_user_uidb64,
'domain_uri': settings.DOMAIN_URI}) 'domain_uri': settings.DOMAIN_URI,
'site_name': settings.SITE_NAME})
def get_body_email_message(self, subject: str, message: str): def get_body_email_message(self, subject: str, message: str):
"""Prepare the body of the email message""" """Prepare the body of the email message"""
@ -241,5 +248,6 @@ class ResetPasswordToken(PlatformMixin, ProjectBaseMixin):
template_name=settings.RESETTING_TOKEN_TEMPLATE, template_name=settings.RESETTING_TOKEN_TEMPLATE,
context={'token': self.key, context={'token': self.key,
'uidb64': self.user.get_user_uidb64, 'uidb64': self.user.get_user_uidb64,
'domain_uri': settings.DOMAIN_URI}) 'domain_uri': settings.DOMAIN_URI,
'site_name': settings.SITE_NAME})

View File

@ -3,6 +3,9 @@ from fcm_django.models import FCMDevice
from rest_framework import serializers, exceptions from rest_framework import serializers, exceptions
from account import models from account import models
from utils import exceptions as utils_exceptions
from rest_framework_simplejwt import tokens
from django.conf import settings
# User serializers # User serializers
@ -58,3 +61,40 @@ class FCMDeviceSerializer(serializers.ModelSerializer):
instance.user = None instance.user = None
instance.save() instance.save()
return instance return instance
class RefreshTokenSerializer(serializers.Serializer):
"""Serializer for refresh token view"""
refresh_token = serializers.CharField(read_only=True)
access_token = serializers.CharField(read_only=True)
def get_request(self):
"""Return request"""
return self.context.get('request')
def validate(self, attrs):
"""Override validate method"""
refresh_token = self.get_request().COOKIES.get('refresh_token')
if not refresh_token:
raise utils_exceptions.NotValidRefreshTokenError()
token = tokens.RefreshToken(token=refresh_token)
data = {'access_token': str(token.access_token)}
if settings.SIMPLE_JWT.get('ROTATE_REFRESH_TOKENS'):
if settings.SIMPLE_JWT.get('BLACKLIST_AFTER_ROTATION'):
try:
# Attempt to blacklist the given refresh token
token.blacklist()
except AttributeError:
# If blacklist app not installed, `blacklist` method will
# not be present
pass
token.set_jti()
token.set_exp()
data['refresh_token'] = str(token)
return data

View File

@ -22,15 +22,3 @@ def send_reset_password_email(request_id):
logger.error(f'METHOD_NAME: {send_reset_password_email.__name__}\n' logger.error(f'METHOD_NAME: {send_reset_password_email.__name__}\n'
f'DETAIL: Exception occurred for ResetPasswordToken instance: ' f'DETAIL: Exception occurred for ResetPasswordToken instance: '
f'{request_id}') f'{request_id}')
@shared_task
def send_confirm_email(user_id):
"""Send verification email to user."""
try:
obj = models.User.objects.get(id=user_id)
obj.send_email(subject=_('Email confirmation'),
message=obj.get_confirm_email_template())
except:
logger.error(f'METHOD_NAME: {send_confirm_email.__name__}\n'
f'DETAIL: Exception occurred for user: {user_id}')

View File

@ -7,5 +7,5 @@ app_name = 'account'
urlpatterns = [ urlpatterns = [
path('user/', views.UserView.as_view(), name='user-get-update'), path('user/', views.UserView.as_view(), name='user-get-update'),
path('refresh-token/', views.RefreshTokenView.as_view(), name="refresh-token"),
] ]

View File

@ -7,10 +7,6 @@ from account.views import web as views
app_name = 'account' app_name = 'account'
urlpatterns_api = [ urlpatterns_api = [
path('verify/email/', views.VerifyEmailView.as_view(),
name='verify-email'),
path('verify/email/confirm/<uidb64>/<token>/', views.VerifyEmailConfirmView.as_view(),
name='verify-email-confirm'),
path('reset-password/', views.PasswordResetView.as_view(), path('reset-password/', views.PasswordResetView.as_view(),
name='password-reset'), name='password-reset'),
path('form/reset-password/<uidb64>/<token>/', views.FormPasswordResetConfirmView.as_view(), path('form/reset-password/<uidb64>/<token>/', views.FormPasswordResetConfirmView.as_view(),

View File

@ -6,6 +6,7 @@ from rest_framework.response import Response
from account import models from account import models
from account.serializers import common as serializers from account.serializers import common as serializers
from utils.views import JWTGenericViewMixin
# User views # User views
@ -53,3 +54,21 @@ class FCMDeviceViewSet(generics.GenericAPIView):
obj = queryset.filter(**filter).first() obj = queryset.filter(**filter).first()
obj and self.check_object_permissions(self.request, obj) obj and self.check_object_permissions(self.request, obj)
return obj return obj
# Refresh access_token
class RefreshTokenView(JWTGenericViewMixin):
"""Refresh access_token"""
permission_classes = (permissions.IsAuthenticated,)
serializer_class = serializers.RefreshTokenSerializer
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
response = Response(serializer.data, status=status.HTTP_201_CREATED)
access_token = serializer.data.get('access_token')
refresh_token = serializer.data.get('refresh_token')
return self._put_cookies_in_response(
cookies=self._put_data_in_cookies(access_token=access_token,
refresh_token=refresh_token),
response=response)

View File

@ -18,7 +18,6 @@ from rest_framework import views
from rest_framework.response import Response from rest_framework.response import Response
from account import models from account import models
from account import tasks
from account.forms import SetPasswordForm from account.forms import SetPasswordForm
from account.serializers import web as serializers from account.serializers import web as serializers
from utils import exceptions as utils_exceptions from utils import exceptions as utils_exceptions
@ -27,45 +26,6 @@ from utils.views import (JWTCreateAPIView,
JWTGenericViewMixin) JWTGenericViewMixin)
# Email confirmation
class VerifyEmailView(JWTGenericViewMixin):
"""View for confirmation email"""
def post(self, request, *args, **kwargs):
"""Implement POST method"""
user = request.user
if user.email_confirmed:
raise utils_exceptions.EmailConfirmedError()
# Send verification link on user email
if settings.USE_CELERY:
tasks.send_confirm_email.delay(user.id)
else:
tasks.send_confirm_email(user.id)
return Response(status=status.HTTP_200_OK)
class VerifyEmailConfirmView(JWTGenericViewMixin):
"""View for confirmation email"""
permission_classes = (permissions.AllowAny, )
def get(self, request, *args, **kwargs):
"""Implement GET-method"""
uidb64 = kwargs.get('uidb64')
token = kwargs.get('token')
uid = force_text(urlsafe_base64_decode(uidb64))
user_qs = models.User.objects.filter(pk=uid)
if user_qs.exists():
user = user_qs.first()
if not gm_token_generator.check_token(user, token):
raise utils_exceptions.NotValidAccessTokenError()
# Change email status
user.confirm_email()
return Response(status=status.HTTP_200_OK)
else:
raise utils_exceptions.UserNotFoundError()
# Password reset # Password reset
class PasswordResetView(JWTCreateAPIView): class PasswordResetView(JWTCreateAPIView):
"""View for resetting user password""" """View for resetting user password"""

View File

@ -5,6 +5,7 @@ from django.contrib.auth import password_validation as password_validators
from django.db.models import Q from django.db.models import Q
from rest_framework import serializers from rest_framework import serializers
from rest_framework import validators as rest_validators from rest_framework import validators as rest_validators
from authorization import tasks
# JWT # JWT
from rest_framework_simplejwt import tokens from rest_framework_simplejwt import tokens
@ -13,8 +14,6 @@ from authorization.models import Application, BlacklistedAccessToken
from utils import exceptions as utils_exceptions from utils import exceptions as utils_exceptions
from utils import methods as utils_methods from utils import methods as utils_methods
JWT_SETTINGS = settings.SIMPLE_JWT
# Mixins # Mixins
class BaseAuthSerializerMixin(serializers.Serializer): class BaseAuthSerializerMixin(serializers.Serializer):
@ -46,17 +45,6 @@ class JWTBaseSerializerMixin(serializers.Serializer):
return super().to_representation(instance) return super().to_representation(instance)
class LoginSerializerMixin(BaseAuthSerializerMixin):
"""Mixin for login serializers"""
password = serializers.CharField(write_only=True)
class ClassicAuthSerializerMixin(BaseAuthSerializerMixin):
"""Classic authorization serializer mixin"""
password = serializers.CharField(write_only=True)
newsletter = serializers.BooleanField()
# Serializers # Serializers
class SignupSerializer(serializers.ModelSerializer): class SignupSerializer(serializers.ModelSerializer):
"""Signup serializer serializer mixin""" """Signup serializer serializer mixin"""
@ -102,8 +90,12 @@ class SignupSerializer(serializers.ModelSerializer):
username=validated_data.get('username'), username=validated_data.get('username'),
password=validated_data.get('password'), password=validated_data.get('password'),
email=validated_data.get('email'), email=validated_data.get('email'),
newsletter=validated_data.get('newsletter') newsletter=validated_data.get('newsletter'))
) # Send verification link on user email
if settings.USE_CELERY:
tasks.send_confirm_email.delay(obj.id)
else:
tasks.send_confirm_email(obj.id)
return obj return obj
@ -152,43 +144,6 @@ class LoginByUsernameOrEmailSerializer(JWTBaseSerializerMixin, serializers.Model
return super().to_representation(instance) return super().to_representation(instance)
class RefreshTokenSerializer(serializers.Serializer):
"""Serializer for refresh token view"""
refresh_token = serializers.CharField(read_only=True)
access_token = serializers.CharField(read_only=True)
def get_request(self):
"""Return request"""
return self.context.get('request')
def validate(self, attrs):
"""Override validate method"""
refresh_token = self.get_request().COOKIES.get('refresh_token')
if not refresh_token:
raise utils_exceptions.NotValidRefreshTokenError()
token = tokens.RefreshToken(token=refresh_token)
data = {'access_token': str(token.access_token)}
if JWT_SETTINGS.get('ROTATE_REFRESH_TOKENS'):
if JWT_SETTINGS.get('BLACKLIST_AFTER_ROTATION'):
try:
# Attempt to blacklist the given refresh token
token.blacklist()
except AttributeError:
# If blacklist app not installed, `blacklist` method will
# not be present
pass
token.set_jti()
token.set_exp()
data['refresh_token'] = str(token)
return data
class LogoutSerializer(serializers.ModelSerializer): class LogoutSerializer(serializers.ModelSerializer):
"""Serializer class for model Logout""" """Serializer class for model Logout"""

View File

@ -7,3 +7,15 @@ from account import models as account_models
logging.basicConfig(format='[%(levelname)s] %(message)s', level=logging.INFO) logging.basicConfig(format='[%(levelname)s] %(message)s', level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@shared_task
def send_confirm_email(user_id):
"""Send verification email to user."""
try:
obj = account_models.User.objects.get(id=user_id)
obj.send_email(subject=_('Email confirmation'),
message=obj.get_confirm_email_template())
except:
logger.error(f'METHOD_NAME: {send_confirm_email.__name__}\n'
f'DETAIL: Exception occurred for user: {user_id}')

View File

@ -29,9 +29,10 @@ urlpatterns_oauth2 = [
urlpatterns_jwt = [ urlpatterns_jwt = [
path('signup/', views.SignUpView.as_view(), name='signup'), path('signup/', views.SignUpView.as_view(), name='signup'),
path('signup/confirm/<uidb64>/<token>/', views.VerifyEmailConfirmView.as_view(),
name='signup-confirm'),
path('login/', views.LoginByUsernameOrEmailView.as_view(), name='login'), path('login/', views.LoginByUsernameOrEmailView.as_view(), name='login'),
path('refresh-token/', views.RefreshTokenView.as_view(), name="refresh-token"), path('logout/', views.LogoutView.as_view(), name="logout")
path('logout/', views.LogoutView.as_view(), name="logout"),
] ]

View File

@ -3,6 +3,8 @@ import json
from braces.views import CsrfExemptMixin from braces.views import CsrfExemptMixin
from django.conf import settings from django.conf import settings
from django.utils.encoding import force_text
from django.utils.http import urlsafe_base64_decode
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from oauth2_provider.oauth2_backends import OAuthLibCore from oauth2_provider.oauth2_backends import OAuthLibCore
from oauth2_provider.settings import oauth2_settings from oauth2_provider.settings import oauth2_settings
@ -14,6 +16,7 @@ from rest_framework.response import Response
from rest_framework_simplejwt import tokens as jwt_tokens from rest_framework_simplejwt import tokens as jwt_tokens
from rest_framework_social_oauth2.oauth2_backends import KeepRequestCore from rest_framework_social_oauth2.oauth2_backends import KeepRequestCore
from rest_framework_social_oauth2.oauth2_endpoints import SocialTokenServer from rest_framework_social_oauth2.oauth2_endpoints import SocialTokenServer
from utils.models import gm_token_generator
from account.models import User from account.models import User
from authorization.models import Application from authorization.models import Application
@ -166,6 +169,30 @@ class SignUpView(JWTCreateAPIView):
return Response(status=status.HTTP_201_CREATED) return Response(status=status.HTTP_201_CREATED)
class VerifyEmailConfirmView(JWTGenericViewMixin):
"""View for confirmation email"""
permission_classes = (permissions.AllowAny, )
def get(self, request, *args, **kwargs):
"""Implement GET-method"""
uidb64 = kwargs.get('uidb64')
token = kwargs.get('token')
uid = force_text(urlsafe_base64_decode(uidb64))
user_qs = User.objects.filter(pk=uid)
if user_qs.exists():
user = user_qs.first()
if not gm_token_generator.check_token(user, token):
raise utils_exceptions.NotValidTokenError()
# Approve email status
user.confirm_email()
# Set user status as active
user.approve()
return Response(status=status.HTTP_200_OK)
else:
raise utils_exceptions.UserNotFoundError()
# Login by username|email + password # Login by username|email + password
class LoginByUsernameOrEmailView(JWTAuthViewMixin): class LoginByUsernameOrEmailView(JWTAuthViewMixin):
"""Login by email and password""" """Login by email and password"""
@ -187,24 +214,6 @@ class LoginByUsernameOrEmailView(JWTAuthViewMixin):
response=response) response=response)
# Refresh access_token
class RefreshTokenView(JWTGenericViewMixin):
"""Refresh access_token"""
permission_classes = (permissions.IsAuthenticated,)
serializer_class = serializers.RefreshTokenSerializer
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
response = Response(serializer.data, status=status.HTTP_201_CREATED)
access_token = serializer.data.get('access_token')
refresh_token = serializer.data.get('refresh_token')
return self._put_cookies_in_response(
cookies=self._put_data_in_cookies(access_token=access_token,
refresh_token=refresh_token),
response=response)
# Logout # Logout
class LogoutView(JWTGenericViewMixin): class LogoutView(JWTGenericViewMixin):
"""Logout user""" """Logout user"""

View File

@ -70,6 +70,13 @@ class NotValidUsernameError(exceptions.APIException):
default_detail = _('Wrong username') default_detail = _('Wrong username')
class NotValidTokenError(exceptions.APIException):
"""The exception should be thrown when token in url is not valid
"""
status_code = status.HTTP_400_BAD_REQUEST
default_detail = _('Not valid token')
class NotValidAccessTokenError(exceptions.APIException): class NotValidAccessTokenError(exceptions.APIException):
"""The exception should be thrown when access token in url is not valid """The exception should be thrown when access token in url is not valid
""" """

View File

@ -12,8 +12,6 @@ from rest_framework_simplejwt import tokens
class JWTGenericViewMixin(generics.GenericAPIView): class JWTGenericViewMixin(generics.GenericAPIView):
"""JWT view mixin""" """JWT view mixin"""
JWT_SETTINGS = settings.SIMPLE_JWT
ACCESS_TOKEN_HTTP_ONLY = False ACCESS_TOKEN_HTTP_ONLY = False
ACCESS_TOKEN_SECURE = False ACCESS_TOKEN_SECURE = False
@ -42,8 +40,8 @@ class JWTGenericViewMixin(generics.GenericAPIView):
# Set max_age for tokens # Set max_age for tokens
if permanent: if permanent:
access_token_max_age = self.JWT_SETTINGS.get('ACCESS_TOKEN_LIFETIME_SECONDS') access_token_max_age = settings.SIMPLE_JWT.get('ACCESS_TOKEN_LIFETIME_SECONDS')
refresh_token_max_age = self.JWT_SETTINGS.get('REFRESH_TOKEN_LIFETIME_SECONDS') refresh_token_max_age = settings.SIMPLE_JWT.get('REFRESH_TOKEN_LIFETIME_SECONDS')
else: else:
access_token_max_age = settings.COOKIES_MAX_AGE access_token_max_age = settings.COOKIES_MAX_AGE
refresh_token_max_age = settings.COOKIES_MAX_AGE refresh_token_max_age = settings.COOKIES_MAX_AGE

View File

@ -369,7 +369,7 @@ PASSWORD_RESET_TIMEOUT_DAYS = 1
# TEMPLATES # TEMPLATES
CONFIRMATION_PASSWORD_RESET_TEMPLATE = 'account/password_reset_confirm.html' CONFIRMATION_PASSWORD_RESET_TEMPLATE = 'account/password_reset_confirm.html'
RESETTING_TOKEN_TEMPLATE = 'account/password_reset_email.html' RESETTING_TOKEN_TEMPLATE = 'account/password_reset_email.html'
CONFIRM_EMAIL_TEMPLATE = 'account/confirm_email.html' CONFIRM_EMAIL_TEMPLATE = 'authorization/confirm_email.html'
# COOKIES # COOKIES
@ -386,3 +386,6 @@ FILE_UPLOAD_PERMISSIONS = 0o644
SOLO_CACHE_TIMEOUT = 300 SOLO_CACHE_TIMEOUT = 300
SITE_NAME = 'Gault & Millau'

View File

@ -3,7 +3,7 @@
{% trans "Please confirm your email address to complete the registration:" %} {% trans "Please confirm your email address to complete the registration:" %}
{% block signup_confirm %} {% block signup_confirm %}
http://{{ domain_uri }}{% url 'web:account:verify-email-confirm' uidb64=uid token=token %} http://{{ domain_uri }}{% url 'auth:signup-confirm' uidb64=uid token=token %}
{% endblock %} {% endblock %}
{% trans "Thanks for using our site!" %} {% trans "Thanks for using our site!" %}