-
Notifications
You must be signed in to change notification settings - Fork 27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added authentication endpoints #72
base: develop
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
from django.contrib import admin | ||
|
||
# Register your models here. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
from django.apps import AppConfig | ||
|
||
|
||
class AuthenticationConfig(AppConfig): | ||
default_auto_field = 'django.db.models.BigAutoField' | ||
name = 'authentication' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
from rest_framework_simplejwt.authentication import JWTAuthentication | ||
from django.conf import settings | ||
|
||
from rest_framework.authentication import CSRFCheck | ||
from rest_framework import exceptions | ||
|
||
|
||
def enforce_csrf(request): | ||
""" | ||
Enforce CSRF validation. | ||
""" | ||
|
||
def dummy_get_response(request): # pragma: no cover | ||
return None | ||
|
||
check = CSRFCheck(dummy_get_response) | ||
# populates request.META['CSRF_COOKIE'], which is used in process_view() | ||
check.process_request(request) | ||
reason = check.process_view(request, None, (), {}) | ||
if reason: | ||
# CSRF failed, bail with explicit error message | ||
raise exceptions.PermissionDenied('CSRF Failed: %s' % reason) | ||
|
||
|
||
class CustomAuthentication(JWTAuthentication): | ||
|
||
def authenticate(self, request): | ||
header = self.get_header(request) | ||
|
||
if header is None: | ||
raw_token = request.COOKIES.get(settings.SIMPLE_JWT['AUTH_COOKIE']) or None | ||
else: | ||
raw_token = self.get_raw_token(header) | ||
if raw_token is None: | ||
return None | ||
|
||
validated_token = self.get_validated_token(raw_token) | ||
# enforce_csrf(request) | ||
return self.get_user(validated_token), validated_token |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
from django.db import models | ||
|
||
# Create your models here. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
from rest_framework import serializers | ||
|
||
from accounts.models import CustomUser | ||
|
||
|
||
class AuthUserSerializer(serializers.ModelSerializer): | ||
class Meta: | ||
model = CustomUser | ||
fields = ["username", "password"] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
from django.test import TestCase | ||
from django.urls import reverse | ||
from rest_framework import status | ||
from rest_framework.test import APIClient | ||
from accounts.models import CustomUser | ||
from django.conf import settings | ||
|
||
class LoginViewTestCase(TestCase): | ||
def setUp(self): | ||
LOGIN_URL = 'api/v2/auth/login/' | ||
self.client = APIClient() | ||
self.active_user = CustomUser.objects.create_user(username="activeuser", password="password123") | ||
self.active_user.is_active = True | ||
self.active_user.save() | ||
|
||
self.inactive_user = CustomUser.objects.create_user(username="inactiveuser", password="password123") | ||
self.inactive_user.is_active = False | ||
self.inactive_user.save() | ||
|
||
self.url = LOGIN_URL | ||
|
||
def test_login_successful(self): | ||
response = self.client.post(self.url, {"username": "activeuser", "password": "password123"}) | ||
self.assertEqual(response.status_code, status.HTTP_200_OK) | ||
self.assertIn("access", response.data) | ||
self.assertIn(settings.SIMPLE_JWT["AUTH_COOKIE"], response.cookies) | ||
|
||
def test_login_inactive_user(self): | ||
response = self.client.post(self.url, {"username": "inactiveuser", "password": "password123"}) | ||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) | ||
self.assertEqual(response.data, {"details": "This account is not active."}) | ||
|
||
def test_login_invalid_credentials(self): | ||
response = self.client.post(self.url, {"username": "wronguser", "password": "wrongpassword"}) | ||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) | ||
self.assertEqual(response.data, {"details": "Account with given credentials not found."}) | ||
|
||
def test_login_missing_fields(self): | ||
response = self.client.post(self.url, {"username": "activeuser"}) | ||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) | ||
self.assertEqual(response.data, {"details": "Account with given credentials not found."}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
from django.urls import path | ||
from . import views | ||
|
||
urlpatterns = [ | ||
path("login/", views.LoginView.as_view(), name='login'), | ||
path("logout/", views.LogoutView.as_view(), name='logout'), | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
from rest_framework_simplejwt.tokens import RefreshToken | ||
from rest_framework.response import Response | ||
from django.contrib.auth import authenticate | ||
from rest_framework import status, generics | ||
from django.conf import settings | ||
|
||
from authentication.serializers import AuthUserSerializer | ||
|
||
|
||
def get_tokens_for_user(user): | ||
refresh = RefreshToken.for_user(user) | ||
|
||
return { | ||
"refresh": str(refresh), | ||
"access": str(refresh.access_token), | ||
} | ||
|
||
|
||
class LoginView(generics.GenericAPIView): | ||
permission_classes = [] | ||
authentication_classes = [] | ||
serializer_class = AuthUserSerializer | ||
|
||
def post(self, request, format=None): | ||
data = request.data | ||
response = Response() | ||
username = data.get("username", None) | ||
password = data.get("password", None) | ||
user = authenticate(username=username, password=password) | ||
|
||
if user is not None: | ||
if user.is_active: | ||
data = get_tokens_for_user(user) | ||
response.set_cookie( | ||
key=settings.SIMPLE_JWT["AUTH_COOKIE"], | ||
value=data["access"], | ||
secure=settings.SIMPLE_JWT["AUTH_COOKIE_SECURE"], | ||
httponly=settings.SIMPLE_JWT["AUTH_COOKIE_HTTP_ONLY"], | ||
samesite=settings.SIMPLE_JWT["AUTH_COOKIE_SAMESITE"], | ||
max_age=823396, | ||
# domain='example.com' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's remove these unnecessary comments. We can add these back in later if we need them There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another point, I just thought of @maunga-et . Let's make the access token valid for 10 mins and then, once we have a refresh token, we can set it to be valid for 2 weeks. This will allow users to keep logged in without having to reauthenticate as long as they are still using the site. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay I am on it thanks. |
||
) | ||
response.data = data | ||
response.status_code = status.HTTP_200_OK | ||
return response | ||
else: | ||
return Response({"details": "This account is not active."}, status=status.HTTP_400_BAD_REQUEST) | ||
else: | ||
return Response({"details": "Account with given credentials not found."}, status=status.HTTP_400_BAD_REQUEST) | ||
|
||
|
||
class LogoutView(generics.GenericAPIView): | ||
permission_classes = [] | ||
authentication_classes = [] | ||
serializer_class = None | ||
|
||
def post(self, request): | ||
response = Response() | ||
response.set_cookie( | ||
key=settings.SIMPLE_JWT["AUTH_COOKIE"], | ||
max_age=0, | ||
# domain='example.com', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @maunga-et Same as above, we can remove this comment. |
||
secure=settings.SIMPLE_JWT["AUTH_COOKIE_SECURE"], | ||
expires="Thu, 01 Jan 1970 00:00:00 GMT", | ||
samesite=settings.SIMPLE_JWT["AUTH_COOKIE_SAMESITE"] | ||
) | ||
response.data = {"detail": "Logout successful."} | ||
return response |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@maunga-et let's not change the api version yet. I recommend we name the JWT endpoints something like
api/token/
. Then we can have endpoints like:api/token/
for generating a token keypair or loginapi/token/refresh/
to generate new refresh tokens andapi/token/logout
to invalidate & blacklist tokens and then log the user out.