From 5403f0d32550f97bd7860ad0ab09ade8cda8942e Mon Sep 17 00:00:00 2001 From: Anatoly Date: Wed, 4 Sep 2019 12:42:23 +0300 Subject: [PATCH] refactored: login, logout, refresh token, change email, confirm email, reset password endpoints --- apps/account/models.py | 29 ++++---- apps/account/serializers/web.py | 67 +++++++++++++------ apps/account/urls/web.py | 3 + apps/account/views/web.py | 45 ++++++++++++- .../migrations/0005_auto_20190904_0821.py | 22 ++++++ ...006_remove_jwtaccesstoken_refresh_token.py | 17 +++++ .../0007_jwtaccesstoken_refresh_token.py | 19 ++++++ apps/authorization/models.py | 23 ++++--- apps/authorization/serializers/common.py | 64 +++++++----------- apps/authorization/views/common.py | 43 +++++------- apps/utils/exceptions.py | 49 +++++++++----- apps/utils/permissions.py | 8 +-- apps/utils/serializers.py | 9 +++ apps/utils/tokens.py | 13 ++-- apps/utils/views.py | 10 --- 15 files changed, 269 insertions(+), 152 deletions(-) create mode 100644 apps/authorization/migrations/0005_auto_20190904_0821.py create mode 100644 apps/authorization/migrations/0006_remove_jwtaccesstoken_refresh_token.py create mode 100644 apps/authorization/migrations/0007_jwtaccesstoken_refresh_token.py diff --git a/apps/account/models.py b/apps/account/models.py index e7531a43..2f411466 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -1,5 +1,4 @@ """Account models""" -from typing import Union from django.conf import settings from django.contrib.auth.models import AbstractUser, UserManager as BaseUserManager @@ -14,8 +13,9 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework.authtoken.models import Token from authorization.models import Application -from utils.models import ImageMixin, ProjectBaseMixin, PlatformMixin from utils.models import GMTokenGenerator +from utils.models import ImageMixin, ProjectBaseMixin, PlatformMixin +from utils.tokens import GMRefreshToken class UserManager(BaseUserManager): @@ -93,18 +93,21 @@ class User(ImageMixin, AbstractUser): self.is_active = switcher self.save() - def expire_access_token(self, jti): - # todo: add platform to func parameter - platform = PlatformMixin.WEB - access_token_qs = self.access_tokens.by_jti(jti=jti)\ - .by_platform(platform=platform) - if access_token_qs.exists(): - access_token_qs.first().expire() + def create_jwt_tokens(self, source: int): + """Create JWT tokens for user""" + token = GMRefreshToken.for_user_by_source(self, source) + return { + 'access_token': str(token.access_token), + 'refresh_token': str(token), + } - def expire_refresh_token(self, jti): - refresh_token_qs = self.refresh_tokens.by_jti(jti=jti) - if refresh_token_qs.exists(): - refresh_token_qs.first().expire() + def expire_access_tokens(self): + """Expire all access tokens""" + self.access_tokens.update(expires_at=timezone.now()) + + def expire_refresh_tokens(self): + """Expire all refresh tokens""" + self.refresh_tokens.update(expires_at=timezone.now()) def confirm_email(self): """Method to confirm user email address""" diff --git a/apps/account/serializers/web.py b/apps/account/serializers/web.py index 7af38b9a..9d6c4c8c 100644 --- a/apps/account/serializers/web.py +++ b/apps/account/serializers/web.py @@ -2,12 +2,13 @@ from django.conf import settings from django.contrib.auth import password_validation as password_validators from django.db.models import Q -from django.utils import timezone +from django.utils.translation import gettext_lazy as _ from rest_framework import serializers from account import models, tasks from authorization.models import JWTRefreshToken from utils import exceptions as utils_exceptions +from utils.serializers import SourceSerializerMixin from utils.tokens import GMRefreshToken @@ -26,9 +27,11 @@ class PasswordResetSerializer(serializers.ModelSerializer): def validate(self, attrs): """Override validate method""" user = self.context.get('request').user - username_or_email = attrs.pop('username_or_email') if user.is_anonymous: + username_or_email = attrs.get('username_or_email') + if not username_or_email: + raise serializers.ValidationError(_('Username or Email not requested')) # Check user in DB user_qs = models.User.objects.filter(Q(email=username_or_email) | Q(username=username_or_email)) @@ -122,6 +125,10 @@ class ChangePasswordSerializer(serializers.ModelSerializer): # Update user password from instance instance.set_password(validated_data.get('password')) instance.save() + + # Expire tokens + instance.expire_access_tokens() + instance.expire_refresh_tokens() return instance @@ -132,17 +139,12 @@ class ChangeEmailSerializer(serializers.ModelSerializer): """Meta class""" model = models.User fields = ( - 'id', 'email', ) - read_only_fields = ( - 'id', - ) def validate_email(self, value): """Validate email value""" if value == self.instance.email: - # todo: add custom exception raise serializers.ValidationError() return value @@ -150,7 +152,6 @@ class ChangeEmailSerializer(serializers.ModelSerializer): """Override validate method""" email_confirmed = self.instance.email_confirmed if not email_confirmed: - # todo: add custom exception raise serializers.ValidationError() return attrs @@ -166,11 +167,39 @@ class ChangeEmailSerializer(serializers.ModelSerializer): tasks.confirm_new_email_address.delay(instance.id) else: tasks.confirm_new_email_address(instance.id) - instance.revoke_access_token() return instance -class RefreshTokenSerializer(serializers.Serializer): +class ConfirmEmailSerializer(serializers.ModelSerializer): + """Confirm user email serializer""" + + class Meta: + """Meta class""" + model = models.User + fields = ( + 'email', + ) + + def validate(self, attrs): + """Override validate method""" + email_confirmed = self.instance.email_confirmed + if email_confirmed: + raise serializers.ValidationError() + return attrs + + def update(self, instance, validated_data): + """ + Override update method + """ + # Send verification link on user email for change email address + if settings.USE_CELERY: + tasks.confirm_new_email_address.delay(instance.id) + else: + tasks.confirm_new_email_address(instance.id) + return instance + + +class RefreshTokenSerializer(SourceSerializerMixin): """Serializer for refresh token view""" refresh_token = serializers.CharField(read_only=True) access_token = serializers.CharField(read_only=True) @@ -181,34 +210,28 @@ class RefreshTokenSerializer(serializers.Serializer): def validate(self, attrs): """Override validate method""" - cookie_refresh_token = self.get_request().COOKIES.get('refresh_token') + cookie_refresh_token = self.get_request().COOKIES.get('refresh_token') # Check if refresh_token in COOKIES if not cookie_refresh_token: raise utils_exceptions.NotValidRefreshTokenError() refresh_token = GMRefreshToken(cookie_refresh_token) - refresh_token_qs = JWTRefreshToken.objects.valid()\ + refresh_token_qs = JWTRefreshToken.objects.valid() \ .by_jti(jti=refresh_token.payload.get('jti')) # Check if the user has refresh token if not refresh_token_qs.exists(): raise utils_exceptions.NotValidRefreshTokenError() - # Expire existing refresh token old_refresh_token = refresh_token_qs.first() + source = old_refresh_token.source user = old_refresh_token.user # Expire existing tokens old_refresh_token.expire() - user.access_tokens.by_refresh_token_jti(jti=old_refresh_token.jti)\ - .valid()\ - .update(expires_at=timezone.now()) + old_refresh_token.access_token.expire() # Create new one for user - refresh_token = GMRefreshToken.for_user(user) - refresh_token['user'] = user.get_user_info() + response = user.create_jwt_tokens(source=source) - return { - 'access_token': str(refresh_token.access_token), - 'refresh_token': str(refresh_token), - } + return response diff --git a/apps/account/urls/web.py b/apps/account/urls/web.py index 5f4bdf45..5c483d39 100644 --- a/apps/account/urls/web.py +++ b/apps/account/urls/web.py @@ -17,6 +17,9 @@ urlpatterns_api = [ path('change-email/', views.ChangeEmailView.as_view(), name='change-email'), path('change-email/confirm///', views.ChangeEmailConfirmView.as_view(), name='change-email-confirm'), + path('confirm-email/', views.ConfirmEmailView.as_view(), name='confirm-email'), + path('confirm-email///', views.ConfirmInactiveEmailView.as_view(), + name='inactive-email-confirm'), ] urlpatterns = urlpatterns_api + \ diff --git a/apps/account/views/web.py b/apps/account/views/web.py index 3465b1f1..39dd11fc 100644 --- a/apps/account/views/web.py +++ b/apps/account/views/web.py @@ -12,9 +12,11 @@ from django.utils.translation import gettext_lazy as _ from django.views.decorators.cache import never_cache from django.views.decorators.debug import sensitive_post_parameters from django.views.generic.edit import FormView +from rest_framework import generics from rest_framework import permissions from rest_framework import status from rest_framework import views +from rest_framework.permissions import AllowAny from rest_framework.response import Response from account import models @@ -22,7 +24,6 @@ from account.forms import SetPasswordForm from account.serializers import web as serializers from utils import exceptions as utils_exceptions from utils.models import GMTokenGenerator -from utils.permissions import IsRefreshTokenValid from utils.views import (JWTCreateAPIView, JWTUpdateAPIView, JWTGenericViewMixin) @@ -90,7 +91,7 @@ class ChangePasswordView(JWTUpdateAPIView): class ChangeEmailView(JWTGenericViewMixin): - """Change user email view""" + """Change user email view.""" serializer_class = serializers.ChangeEmailSerializer queryset = models.User.objects.all() @@ -105,11 +106,43 @@ class ChangeEmailView(JWTGenericViewMixin): return Response(status=status.HTTP_200_OK) +class ConfirmEmailView(ChangeEmailView): + """Confirm email view.""" + serializer_class = serializers.ConfirmEmailSerializer + + class ChangeEmailConfirmView(JWTGenericViewMixin): """View for confirm changing 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 GMTokenGenerator(GMTokenGenerator.CHANGE_EMAIL).check_token( + user, token): + raise utils_exceptions.NotValidTokenError() + # Approve email status + user.confirm_email() + # Expire user tokens + user.expire_access_tokens() + user.expire_refresh_tokens() + + return Response(status=status.HTTP_200_OK) + else: + raise utils_exceptions.UserNotFoundError() + + +class ConfirmInactiveEmailView(generics.GenericAPIView): + """View for confirm inactive email""" + + permission_classes = (permissions.AllowAny,) + def get(self, request, *args, **kwargs): """Implement GET-method""" uidb64 = kwargs.get('uidb64') @@ -130,7 +163,7 @@ class ChangeEmailConfirmView(JWTGenericViewMixin): class RefreshTokenView(JWTGenericViewMixin): """Refresh access_token""" - permission_classes = (IsRefreshTokenValid, ) + permission_classes = (AllowAny, ) serializer_class = serializers.RefreshTokenSerializer def post(self, request, *args, **kwargs): @@ -227,6 +260,12 @@ class FormPasswordResetConfirmView(PasswordContextMixin, FormView): def form_valid(self, form): # Saving form form.save() + user = form.user + + # Expire user tokens + user.expire_access_tokens() + user.expire_refresh_tokens() + # Pop session token del self.request.session[self.INTERNAL_RESET_SESSION_TOKEN] return super().form_valid(form) diff --git a/apps/authorization/migrations/0005_auto_20190904_0821.py b/apps/authorization/migrations/0005_auto_20190904_0821.py new file mode 100644 index 00000000..720ab83a --- /dev/null +++ b/apps/authorization/migrations/0005_auto_20190904_0821.py @@ -0,0 +1,22 @@ +# Generated by Django 2.2.4 on 2019-09-04 08:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authorization', '0004_delete_blacklistedaccesstoken'), + ] + + operations = [ + migrations.RemoveField( + model_name='jwtaccesstoken', + name='source', + ), + migrations.AddField( + model_name='jwtrefreshtoken', + name='source', + field=models.PositiveSmallIntegerField(choices=[(0, 'Mobile'), (1, 'Web')], default=0, verbose_name='Source'), + ), + ] diff --git a/apps/authorization/migrations/0006_remove_jwtaccesstoken_refresh_token.py b/apps/authorization/migrations/0006_remove_jwtaccesstoken_refresh_token.py new file mode 100644 index 00000000..7378b18d --- /dev/null +++ b/apps/authorization/migrations/0006_remove_jwtaccesstoken_refresh_token.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.4 on 2019-09-04 08:22 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('authorization', '0005_auto_20190904_0821'), + ] + + operations = [ + migrations.RemoveField( + model_name='jwtaccesstoken', + name='refresh_token', + ), + ] diff --git a/apps/authorization/migrations/0007_jwtaccesstoken_refresh_token.py b/apps/authorization/migrations/0007_jwtaccesstoken_refresh_token.py new file mode 100644 index 00000000..985cdcc7 --- /dev/null +++ b/apps/authorization/migrations/0007_jwtaccesstoken_refresh_token.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.4 on 2019-09-04 08:24 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('authorization', '0006_remove_jwtaccesstoken_refresh_token'), + ] + + operations = [ + migrations.AddField( + model_name='jwtaccesstoken', + name='refresh_token', + field=models.OneToOneField(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='access_token', to='authorization.JWTRefreshToken'), + ), + ] diff --git a/apps/authorization/models.py b/apps/authorization/models.py index 73290063..69416420 100644 --- a/apps/authorization/models.py +++ b/apps/authorization/models.py @@ -38,7 +38,8 @@ class Application(PlatformMixin, AbstractApplication): class JWTAccessTokenManager(models.Manager): """Manager for AccessToken model.""" - def add_to_db(self, user, access_token: AccessToken, refresh_token: RefreshToken): + def add_to_db(self, user, access_token: AccessToken, + refresh_token: RefreshToken): """Create generated tokens to DB""" refresh_token_qs = JWTRefreshToken.objects.filter(user=user, jti=refresh_token.payload.get('jti')) @@ -72,14 +73,15 @@ class JWTAccessTokenQuerySet(models.QuerySet): return self.filter(refresh_token__jti=jti) -class JWTAccessToken(PlatformMixin, ProjectBaseMixin): +class JWTAccessToken(ProjectBaseMixin): """GM access token model.""" user = models.ForeignKey('account.User', related_name='access_tokens', on_delete=models.CASCADE) - refresh_token = models.ForeignKey('JWTRefreshToken', - related_name='access_tokens', - on_delete=models.DO_NOTHING) + refresh_token = models.OneToOneField('JWTRefreshToken', + related_name='access_token', + on_delete=models.CASCADE, + null=True, blank=True, default=None) created_at = models.DateTimeField(null=True, blank=True) expires_at = models.DateTimeField(verbose_name=_('Expiration datetime')) jti = models.CharField(unique=True, max_length=255) @@ -105,13 +107,14 @@ class JWTAccessToken(PlatformMixin, ProjectBaseMixin): class JWTRefreshTokenManager(models.Manager): """Manager for model RefreshToken.""" - def add_to_db(self, user, token: RefreshToken): + def add_to_db(self, user, token: RefreshToken, source: int): """Added generated refresh token to db""" jti = token[settings.SIMPLE_JWT.get('JTI_CLAIM')] exp = token['exp'] obj = self.model( user=user, jti=jti, + source=source, created_at=token.current_time, expires_at=utils.datetime_from_epoch(exp), ) @@ -123,15 +126,19 @@ class JWTRefreshTokenQuerySet(models.QuerySet): """QuerySets for model RefreshToken.""" def valid(self): - """Return only balid refresh tokens""" + """Return only valid refresh tokens""" return self.filter(expires_at__gte=timezone.now()) def by_jti(self, jti: str): """Filter by jti field""" return self.filter(jti=jti) + def by_source(self, source): + """Return access tokens by source""" + return self.filter(source=source) -class JWTRefreshToken(ProjectBaseMixin): + +class JWTRefreshToken(PlatformMixin, ProjectBaseMixin): """GM refresh token model.""" user = models.ForeignKey('account.User', related_name='refresh_tokens', diff --git a/apps/authorization/serializers/common.py b/apps/authorization/serializers/common.py index d764eeca..5db0e5ed 100644 --- a/apps/authorization/serializers/common.py +++ b/apps/authorization/serializers/common.py @@ -8,43 +8,9 @@ from rest_framework import validators as rest_validators from account import models as account_models from authorization import tasks -from authorization.models import Application from utils import exceptions as utils_exceptions from utils import methods as utils_methods -from utils import tokens - - -# Mixins -class BaseAuthSerializerMixin(serializers.Serializer): - """Base authorization serializer mixin""" - source = serializers.ChoiceField(choices=Application.SOURCES) - - -class JWTBaseSerializerMixin(serializers.Serializer): - """ - Mixin for JWT authentication. - Uses in serializers when need give in response access and refresh token - """ - # RESPONSE - refresh_token = serializers.CharField(read_only=True) - access_token = serializers.CharField(read_only=True) - - def get_tokens(self): - """Create JWT token""" - user = self.instance - token = tokens.GMRefreshToken.for_user(user) - token['user'] = user.get_user_info() - return { - 'access_token': str(token.access_token), - 'refresh_token': str(token), - } - - def to_representation(self, instance): - """Override to_representation method""" - tokens = self.get_tokens() - setattr(instance, 'access_token', tokens.get('access_token')) - setattr(instance, 'refresh_token', tokens.get('refresh_token')) - return super().to_representation(instance) +from utils.serializers import SourceSerializerMixin # Serializers @@ -101,14 +67,20 @@ class SignupSerializer(serializers.ModelSerializer): return obj -class LoginByUsernameOrEmailSerializer(JWTBaseSerializerMixin, serializers.ModelSerializer): +class LoginByUsernameOrEmailSerializer(SourceSerializerMixin, + serializers.ModelSerializer): """Serializer for login user""" + # REQUEST username_or_email = serializers.CharField(write_only=True) password = serializers.CharField(write_only=True) - # for cookie properties (Max-Age) + # For cookie properties (Max-Age) remember = serializers.BooleanField(write_only=True) + # RESPONSE + refresh_token = serializers.CharField(read_only=True) + access_token = serializers.CharField(read_only=True) + class Meta: """Meta-class""" model = account_models.User @@ -116,8 +88,9 @@ class LoginByUsernameOrEmailSerializer(JWTBaseSerializerMixin, serializers.Model 'username_or_email', 'password', 'remember', + 'source', 'refresh_token', - 'access_token' + 'access_token', ) def validate(self, attrs): @@ -138,12 +111,23 @@ class LoginByUsernameOrEmailSerializer(JWTBaseSerializerMixin, serializers.Model self.instance = user return attrs + def to_representation(self, instance): + """Override to_representation method""" + tokens = instance.create_jwt_tokens(source=self.validated_data.get('source')) + setattr(instance, 'access_token', tokens.get('access_token')) + setattr(instance, 'refresh_token', tokens.get('refresh_token')) + return super().to_representation(instance) + + +class LogoutSerializer(SourceSerializerMixin): + """Serializer for Logout endpoint.""" + # OAuth -class OAuth2Serialzier(BaseAuthSerializerMixin): +class OAuth2Serialzier(SourceSerializerMixin): """Serializer OAuth2 authorization""" token = serializers.CharField(max_length=255) -class OAuth2LogoutSerializer(BaseAuthSerializerMixin): +class OAuth2LogoutSerializer(SourceSerializerMixin): """Serializer for logout""" diff --git a/apps/authorization/views/common.py b/apps/authorization/views/common.py index 6591a643..97c1e4b8 100644 --- a/apps/authorization/views/common.py +++ b/apps/authorization/views/common.py @@ -3,7 +3,6 @@ import json from braces.views import CsrfExemptMixin from django.conf import settings -from django.utils import timezone from django.utils.encoding import force_text from django.utils.http import urlsafe_base64_decode from django.utils.translation import gettext_lazy as _ @@ -14,16 +13,17 @@ from rest_framework import generics from rest_framework import permissions from rest_framework import status from rest_framework.response import Response +from rest_framework_simplejwt.tokens import AccessToken from rest_framework_social_oauth2.oauth2_backends import KeepRequestCore from rest_framework_social_oauth2.oauth2_endpoints import SocialTokenServer from account.models import User from authorization.models import Application -from authorization.models import JWTRefreshToken +from authorization.models import JWTAccessToken from authorization.serializers import common as serializers from utils import exceptions as utils_exceptions -from utils import tokens as jwt_tokens from utils.models import GMTokenGenerator +from utils.permissions import IsAuthenticatedAndTokenIsValid from utils.views import (JWTGenericViewMixin, JWTCreateAPIView) @@ -109,18 +109,12 @@ class OAuth2SignUpView(OAuth2ViewMixin, JWTGenericViewMixin): permission_classes = (permissions.AllowAny, ) serializer_class = serializers.OAuth2Serialzier - def get_jwt_token(self, user: User): - """Get JWT token""" - token = jwt_tokens.GMRefreshToken.for_user(user) - # Adding additional information about user to payload - token['user'] = user.get_user_info() - return token - def post(self, request, *args, **kwargs): # Preparing request data serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) request_data = self.prepare_request_data(serializer.validated_data) + source = serializer.validated_data.get('source') request_data.update({ 'grant_type': settings.OAUTH2_SOCIAL_AUTH_GRANT_TYPE, 'backend': settings.OAUTH2_SOCIAL_AUTH_BACKEND_NAME @@ -135,18 +129,17 @@ class OAuth2SignUpView(OAuth2ViewMixin, JWTGenericViewMixin): url, headers, body, oauth2_status = self.create_token_response(request._request) body = json.loads(body) - # Get JWT token + # Check OAuth2 response if oauth2_status != status.HTTP_200_OK: - raise ValueError('status isn\'t 200') + raise utils_exceptions.OAuth2Error() # Get authenticated user user = User.objects.by_oauth2_access_token(token=body.get('access_token'))\ .first() - # Create JWT token and put oauth2 token (access, refresh tokens) in payload - token = self.get_jwt_token(user=user) - access_token = str(token.access_token) - refresh_token = str(token) + # Create JWT token + tokens = user.create_jwt_tokens(source) + access_token, refresh_token = tokens.get('access_token'), tokens.get('refresh_token') response = Response(data={'access_token': access_token, 'refresh_token': refresh_token}, status=status.HTTP_200_OK) @@ -220,22 +213,16 @@ class LoginByUsernameOrEmailView(JWTAuthViewMixin): # Logout class LogoutView(JWTGenericViewMixin): """Logout user""" + permission_classes = (IsAuthenticatedAndTokenIsValid, ) def post(self, request, *args, **kwargs): """Override create method""" - # Get token objs by JTI - refresh_token_key = request.COOKIES.get('refresh_token') - refresh_token = jwt_tokens.GMRefreshToken(refresh_token_key) - refresh_token_qs = JWTRefreshToken.objects.valid()\ - .by_jti(jti=refresh_token.payload.get('jti')) - if not refresh_token_qs.exists(): - raise utils_exceptions.NotValidRefreshTokenError() - - refresh_token_obj = refresh_token_qs.first() - access_token_qs = refresh_token_obj.access_tokens + # Get access token objs by JTI + access_token = AccessToken(request.COOKIES.get('access_token')) + access_token_obj = JWTAccessToken.objects.get(jti=access_token.payload.get('jti')) # Expire tokens - refresh_token_obj.expire() - access_token_qs.update(expires_at=timezone.now()) + access_token_obj.expire() + access_token_obj.refresh_token.expire() return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apps/utils/exceptions.py b/apps/utils/exceptions.py index f8d81366..a3db74ba 100644 --- a/apps/utils/exceptions.py +++ b/apps/utils/exceptions.py @@ -16,20 +16,25 @@ class ProjectBaseException(exceptions.APIException): super().__init__() +class AuthErrorMixin(exceptions.APIException): + """Authentication exception error mixin.""" + status_code = status.HTTP_401_UNAUTHORIZED + + class ServiceError(ProjectBaseException): """Service error.""" status_code = status.HTTP_503_SERVICE_UNAVAILABLE default_detail = _('Service is temporarily unavailable') -class UserNotFoundError(ProjectBaseException): +class UserNotFoundError(AuthErrorMixin, ProjectBaseException): """The exception should be thrown when the user cannot get""" - status_code = status.HTTP_401_UNAUTHORIZED default_detail = _('User not found') class PasswordRequestResetExists(ProjectBaseException): - """The exception should be thrown when request for reset password + """ + The exception should be thrown when request for reset password is already exists and valid """ status_code = status.HTTP_400_BAD_REQUEST @@ -50,7 +55,8 @@ class EmailSendingError(exceptions.APIException): class LocaleNotExisted(exceptions.APIException): - """The exception should be thrown when passed locale isn't in model Language + """ + The exception should be thrown when passed locale isn't in model Language """ status_code = status.HTTP_400_BAD_REQUEST default_detail = _('Locale not found in database (%s)') @@ -64,49 +70,58 @@ class LocaleNotExisted(exceptions.APIException): class NotValidUsernameError(exceptions.APIException): - """The exception should be thrown when passed username has @ symbol + """ + The exception should be thrown when passed username has @ symbol """ status_code = status.HTTP_400_BAD_REQUEST default_detail = _('Wrong username') class NotValidTokenError(exceptions.APIException): - """The exception should be thrown when token in url is not valid + """ + 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): - """The exception should be thrown when access token in url is not valid +class NotValidAccessTokenError(AuthErrorMixin): + """ + The exception should be thrown when access token in url is not valid """ - status_code = status.HTTP_401_UNAUTHORIZED default_detail = _('Not valid access token') -class NotValidRefreshTokenError(exceptions.APIException): - """The exception should be thrown when refresh token is not valid +class NotValidRefreshTokenError(AuthErrorMixin): + """ + The exception should be thrown when refresh token is not valid """ - status_code = status.HTTP_401_UNAUTHORIZED default_detail = _('Not valid refresh token') +class OAuth2Error(AuthErrorMixin): + """OAuth2 error""" + default_detail = _('OAuth2 Error') + + class PasswordsAreEqual(exceptions.APIException): - """The exception should be raised when passed password is the same as old ones + """ + The exception should be raised when passed password is the same as old ones """ status_code = status.HTTP_400_BAD_REQUEST default_detail = _('Password is already in use') class EmailConfirmedError(exceptions.APIException): - """The exception should be raised when user email status is already confirmed + """ + The exception should be raised when user email status is already confirmed """ status_code = status.HTTP_400_BAD_REQUEST default_detail = _('Email address is already confirmed') -class WrongAuthCredentials(exceptions.APIException): - """The exception should be raised when credentials is not valid for this user +class WrongAuthCredentials(AuthErrorMixin): + """ + The exception should be raised when credentials is not valid for this user """ - status_code = status.HTTP_401_UNAUTHORIZED default_detail = _('Wrong authorization credentials') diff --git a/apps/utils/permissions.py b/apps/utils/permissions.py index 9c3e814c..09b24ecd 100644 --- a/apps/utils/permissions.py +++ b/apps/utils/permissions.py @@ -1,9 +1,9 @@ """Project custom permissions""" from rest_framework.permissions import BasePermission -from authorization.models import JWTRefreshToken - from rest_framework_simplejwt.tokens import AccessToken -from utils.tokens import RefreshToken + +from authorization.models import JWTRefreshToken +from utils.tokens import GMRefreshToken class IsAuthenticatedAndTokenIsValid(BasePermission): @@ -32,7 +32,7 @@ class IsRefreshTokenValid(BasePermission): """Check permissions by refresh token and default REST permission IsAuthenticated""" refresh_token = request.COOKIES.get('refresh_token') if refresh_token: - refresh_token = RefreshToken(refresh_token) + refresh_token = GMRefreshToken(refresh_token) refresh_token_qs = JWTRefreshToken.objects.valid()\ .by_jti(jti=refresh_token.payload.get('jti')) return refresh_token_qs.exists() diff --git a/apps/utils/serializers.py b/apps/utils/serializers.py index e3d44cac..33463ebe 100644 --- a/apps/utils/serializers.py +++ b/apps/utils/serializers.py @@ -1,6 +1,15 @@ """Utils app serializer.""" from rest_framework import serializers +from utils.models import PlatformMixin + class EmptySerializer(serializers.Serializer): """Empty Serializer""" + + +class SourceSerializerMixin(serializers.Serializer): + """Base authorization serializer mixin""" + source = serializers.ChoiceField(choices=PlatformMixin.SOURCES, + default=PlatformMixin.WEB, + write_only=True) diff --git a/apps/utils/tokens.py b/apps/utils/tokens.py index 5d8776b2..e686b42b 100644 --- a/apps/utils/tokens.py +++ b/apps/utils/tokens.py @@ -2,7 +2,6 @@ from rest_framework_simplejwt.settings import api_settings from rest_framework_simplejwt.tokens import Token, AccessToken, RefreshToken, BlacklistMixin -from account.models import User from authorization.models import JWTRefreshToken, JWTAccessToken @@ -34,13 +33,12 @@ class GMBlacklistMixin(BlacklistMixin): """ @classmethod - def for_user(cls, user): - """ - Adds this token to the outstanding token list. - """ + def for_user_by_source(cls, user, source: int): + """Create a refresh token.""" token = super().for_user(user) + token['user'] = user.get_user_info() # Create a record in DB - JWTRefreshToken.objects.add_to_db(user=user, token=token) + JWTRefreshToken.objects.add_to_db(user=user, token=token, source=source) return token @@ -54,6 +52,8 @@ class GMRefreshToken(GMBlacklistMixin, GMToken, RefreshToken): claims present in this refresh token to the new access token except those claims listed in the `no_copy_claims` attribute. """ + from account.models import User + access_token = AccessToken() # Use instantiation time of refresh token as relative timestamp for @@ -74,4 +74,3 @@ class GMRefreshToken(GMBlacklistMixin, GMToken, RefreshToken): access_token=access_token, refresh_token=self) return access_token - diff --git a/apps/utils/views.py b/apps/utils/views.py index c89812b6..d01d30cd 100644 --- a/apps/utils/views.py +++ b/apps/utils/views.py @@ -4,7 +4,6 @@ from django.conf import settings from rest_framework import generics from rest_framework import status from rest_framework.response import Response -from utils import tokens # JWT @@ -19,15 +18,6 @@ class JWTGenericViewMixin(generics.GenericAPIView): REFRESH_TOKEN_SECURE = False COOKIE = namedtuple('COOKIE', ['key', 'value', 'http_only', 'secure', 'max_age']) - def _create_jwt_token(self, user) -> dict: - """Return dictionary with pairs access and refresh tokens""" - token = tokens.GMRefreshToken.for_user(user) - token['user'] = user.get_user_info() - return { - 'access_token': str(token.access_token), - 'refresh_token': str(token), - } - def _put_data_in_cookies(self, access_token: str = None, refresh_token: str = None,