diff --git a/auth_app/models.py b/auth_app/models.py index 168c408..b5c8a21 100644 --- a/auth_app/models.py +++ b/auth_app/models.py @@ -2,14 +2,33 @@ from django.utils import timezone from rest_framework.authtoken.models import Token from django.db import models from join.settings import TOKEN_EXPIRATION_TIME +from rest_framework.authentication import TokenAuthentication +from rest_framework.exceptions import AuthenticationFailed class ExpiringToken(Token): expires_at = models.DateTimeField(null=True, blank=True) def is_expired(self): - return self.expires_at and self.expires_at < timezone.now() + if self.expires_at is None: + return True + return self.expires_at < timezone.now() def save(self, *args, **kwargs): if not self.expires_at: self.expires_at = timezone.now() + TOKEN_EXPIRATION_TIME - super().save(*args, **kwargs) \ No newline at end of file + super().save(*args, **kwargs) + +class ExpiringTokenAuthentication(TokenAuthentication): + model = ExpiringToken + + def authenticate_credentials(self, key): + token = self.model.objects.filter(key=key).first() + + if not token: + raise AuthenticationFailed({"error": "Invalid token."}) + + if token.is_expired(): + token.delete() + raise AuthenticationFailed({"error": "No active session or token already expired"}) + + return (token.user, token) \ No newline at end of file diff --git a/auth_app/serializer.py b/auth_app/serializer.py index d0d5d0b..55562fe 100755 --- a/auth_app/serializer.py +++ b/auth_app/serializer.py @@ -1,8 +1,9 @@ from rest_framework import serializers +from django.core.validators import MinLengthValidator class LoginSerializer(serializers.Serializer): email = serializers.EmailField() - password = serializers.CharField(write_only=True) + password = serializers.CharField(validators=[MinLengthValidator(limit_value=8)]) def validate(self, data): email = data.get('email') diff --git a/auth_app/tests.py b/auth_app/tests.py index 7ce503c..b4a8db8 100644 --- a/auth_app/tests.py +++ b/auth_app/tests.py @@ -1,3 +1,161 @@ -from django.test import TestCase +import json +from django.contrib.auth import get_user_model +from django.utils import timezone +from rest_framework.test import APITestCase +from rest_framework import status +from join.settings import TOKEN_EXPIRATION_TIME +from .models import ExpiringToken -# Create your tests here. +User = get_user_model() + +class AuthTests(APITestCase): + def setUp(self): + self.user = User.objects.create_user( + email="test@example.com", + password="securepassword", + first_name="Test" + ) + self.token = ExpiringToken.objects.create(user=self.user, expires_at=timezone.now() + TOKEN_EXPIRATION_TIME) + self.login_url = "/auth/login/" + self.logout_url = "/auth/logout/" + self.auth_url = "/auth/" + + def test_login_success(self): + response = self.client.post( + self.login_url, + data=json.dumps({"email": "test@example.com", "password": "securepassword"}), + content_type="application/json" + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_login_invalid_credentials(self): + response = self.client.post( + self.login_url, + data=json.dumps({"email": "test@example.com", "password": "wrongpassword"}), + content_type="application/json" + ) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_login_inactive_user(self): + self.user.is_active = False + self.user.save() + + response = self.client.post( + self.login_url, + data=json.dumps({"email": "test@example.com", "password": "securepassword"}), + content_type="application/json" + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_logout_success(self): + self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token.key}') + response = self.client.post(self.logout_url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn("message", response.data) + + def test_logout_without_token(self): + response = self.client.post(self.logout_url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_auth_valid_token(self): + self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token.key}') + response = self.client.get(self.auth_url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["user_id"], self.user.id) + + def test_auth_expired_token(self): + self.token.expires_at = timezone.now() - TOKEN_EXPIRATION_TIME + self.token.save() + self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token.key}') + response = self.client.get(self.auth_url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertIn("error", response.data) + + def test_auth_no_token(self): + response = self.client.get(self.auth_url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertIn("error", response.data) + + def test_auth_valid_token_with_fresh_expiration(self): + self.token.expires_at = timezone.now() + timezone.timedelta(minutes=10) + self.token.save() + + self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token.key}') + response = self.client.get(self.auth_url) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["user_id"], self.user.id) + + def test_auth_token_exactly_at_expiration(self): + self.token.expires_at = timezone.now() + self.token.save() + + self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token.key}') + response = self.client.get(self.auth_url) + + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertIn("error", response.data) + self.assertEqual(response.data["error"], "Token expired, please log in again") + + def test_logout_with_expired_token(self): + self.token.expires_at = timezone.now() - timezone.timedelta(days=1) + self.token.save() + + self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token.key}') + response = self.client.post(self.logout_url) + + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertIn("error", response.data) + self.assertEqual(response.data["error"], "No active session or token already expired") + + def test_login_invalid_email_format(self): + response = self.client.post( + self.login_url, + data=json.dumps({"email": "invalidemail", "password": "securepassword"}), + content_type="application/json" + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("email", response.data) + + def test_login_password_too_short(self): + data = {'email': 'user@example.com', 'password': 'short'} + response = self.client.post(self.login_url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_logout_token_deletion(self): + self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token.key}') + response = self.client.post(self.logout_url) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token.key}') + response = self.client.get(self.auth_url) + + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertIn("detail", response.data) + self.assertEqual(response.data["detail"], "Invalid token.") + + def test_auth_inactive_user(self): + self.user.is_active = False + self.user.save() + + self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token.key}') + response = self.client.get(self.auth_url) + + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertIn("detail", response.data) + self.assertEqual(response.data["detail"], "User inactive or deleted.") + + def test_token_for_different_users(self): + user2 = User.objects.create_user( + email="test2@example.com", + password="securepassword2", + first_name="Test2" + ) + response = self.client.post( + self.login_url, + data=json.dumps({"email": "test2@example.com", "password": "securepassword2"}), + content_type="application/json" + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertNotEqual(response.data["user_id"], self.user.id) \ No newline at end of file diff --git a/auth_app/views.py b/auth_app/views.py index 1624730..1bea24c 100644 --- a/auth_app/views.py +++ b/auth_app/views.py @@ -6,7 +6,7 @@ from join.settings import TOKEN_EXPIRATION_TIME from .serializer import LoginSerializer from rest_framework.permissions import IsAuthenticated from django.utils import timezone -from .models import ExpiringToken +from .models import ExpiringToken, ExpiringTokenAuthentication class LoginView(APIView): @@ -42,18 +42,20 @@ class LoginView(APIView): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) class LogoutView(APIView): - authentication_classes = [authentication.TokenAuthentication] + authentication_classes = [ExpiringTokenAuthentication] permission_classes = [IsAuthenticated] def post(self, request): token = getattr(request.user, 'auth_token', None) - if token is None: - return Response({"error": "No active session or token already expired"}, status=status.HTTP_400_BAD_REQUEST) + if token is None or not isinstance(token, ExpiringToken): + return Response({"detail": "Invalid token."}, status=status.HTTP_401_UNAUTHORIZED) + if token.is_expired(): + return Response({"error": "No active session or token already expired"}, status=status.HTTP_401_UNAUTHORIZED) token.delete() return Response({"message": "Successfully logged out"}, status=status.HTTP_200_OK) - + class AuthView(APIView): authentication_classes = [authentication.TokenAuthentication]