Skip to content

Commit f952b32

Browse files
committed
feat: JWT Authentication implementation login & register endpoints
1 parent 36a9ca5 commit f952b32

23 files changed

+864
-0
lines changed

core/__init__.py

Whitespace-only changes.

core/asgi.py

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""
2+
ASGI config for core project.
3+
4+
It exposes the ASGI callable as a module-level variable named ``application``.
5+
6+
For more information on this file, see
7+
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
8+
"""
9+
10+
import os
11+
12+
from django.core.asgi import get_asgi_application
13+
14+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
15+
16+
application = get_asgi_application()

core/settings.py

+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
"""
2+
Django settings for core project.
3+
4+
Generated by 'django-admin startproject' using Django 4.2.6.
5+
6+
For more information on this file, see
7+
https://docs.djangoproject.com/en/4.2/topics/settings/
8+
9+
For the full list of settings and their values, see
10+
https://docs.djangoproject.com/en/4.2/ref/settings/
11+
"""
12+
13+
from pathlib import Path
14+
15+
# Build paths inside the project like this: BASE_DIR / 'subdir'.
16+
BASE_DIR = Path(__file__).resolve().parent.parent
17+
18+
AUTH_USER_MODEL = 'users.User'
19+
# Quick-start development settings - unsuitable for production
20+
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
21+
22+
# SECURITY WARNING: keep the secret key used in production secret!
23+
SECRET_KEY = 'django-insecure-j95s6tsmb8ppxgv+g-802x8z2#2+x)^#z9vxtc@o(-ho7$^=3z'
24+
25+
# SECURITY WARNING: don't run with debug turned on in production!
26+
DEBUG = True
27+
28+
ALLOWED_HOSTS = []
29+
30+
31+
# Application definition
32+
33+
INSTALLED_APPS = [
34+
'django.contrib.admin',
35+
'django.contrib.auth',
36+
'django.contrib.contenttypes',
37+
'django.contrib.sessions',
38+
'django.contrib.messages',
39+
'django.contrib.staticfiles',
40+
'rest_framework',
41+
'users',
42+
]
43+
44+
MIDDLEWARE = [
45+
'django.middleware.security.SecurityMiddleware',
46+
'django.contrib.sessions.middleware.SessionMiddleware',
47+
'django.middleware.common.CommonMiddleware',
48+
'django.middleware.csrf.CsrfViewMiddleware',
49+
'django.contrib.auth.middleware.AuthenticationMiddleware',
50+
'django.contrib.messages.middleware.MessageMiddleware',
51+
'django.middleware.clickjacking.XFrameOptionsMiddleware',
52+
]
53+
54+
55+
REST_FRAMEWORK = {
56+
# 'EXCEPTION_HANDLER': 'users.exceptions.responses.core_exception_handler',
57+
# 'NON_FIELD_ERRORS_KEY': 'error',
58+
# 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
59+
'DEFAULT_AUTHENTICATION_CLASSES': [
60+
'users.authentication.backends.JWTAuthentication',
61+
],
62+
'DEFAULT_PERMISSION_CLASSES': [
63+
'rest_framework.permissions.IsAuthenticated',
64+
],
65+
}
66+
67+
ROOT_URLCONF = 'core.urls'
68+
69+
TEMPLATES = [
70+
{
71+
'BACKEND': 'django.template.backends.django.DjangoTemplates',
72+
'DIRS': [],
73+
'APP_DIRS': True,
74+
'OPTIONS': {
75+
'context_processors': [
76+
'django.template.context_processors.debug',
77+
'django.template.context_processors.request',
78+
'django.contrib.auth.context_processors.auth',
79+
'django.contrib.messages.context_processors.messages',
80+
],
81+
},
82+
},
83+
]
84+
85+
WSGI_APPLICATION = 'core.wsgi.application'
86+
87+
88+
# Database
89+
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
90+
91+
DATABASES = {
92+
'default': {
93+
'ENGINE': 'django.db.backends.sqlite3',
94+
'NAME': BASE_DIR / 'db.sqlite3',
95+
}
96+
}
97+
98+
99+
# Password validation
100+
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
101+
102+
AUTH_PASSWORD_VALIDATORS = [
103+
{
104+
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
105+
},
106+
{
107+
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
108+
},
109+
{
110+
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
111+
},
112+
{
113+
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
114+
},
115+
]
116+
117+
118+
# Internationalization
119+
# https://docs.djangoproject.com/en/4.2/topics/i18n/
120+
121+
LANGUAGE_CODE = 'en-us'
122+
123+
TIME_ZONE = 'UTC'
124+
125+
USE_I18N = True
126+
127+
USE_TZ = True
128+
129+
130+
# Static files (CSS, JavaScript, Images)
131+
# https://docs.djangoproject.com/en/4.2/howto/static-files/
132+
133+
STATIC_URL = 'static/'
134+
135+
# Default primary key field type
136+
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
137+
138+
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

core/urls.py

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""
2+
URL configuration for core project.
3+
4+
The `urlpatterns` list routes URLs to views. For more information please see:
5+
https://docs.djangoproject.com/en/4.2/topics/http/urls/
6+
Examples:
7+
Function views
8+
1. Add an import: from my_app import views
9+
2. Add a URL to urlpatterns: path('', views.home, name='home')
10+
Class-based views
11+
1. Add an import: from other_app.views import Home
12+
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
13+
Including another URLconf
14+
1. Import the include() function: from django.urls import include, path
15+
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
16+
"""
17+
from django.contrib import admin
18+
from django.urls import include,path
19+
20+
urlpatterns = [
21+
path('admin/', admin.site.urls),
22+
path("auth/", include("users.urls", namespace="users")),
23+
]

core/wsgi.py

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""
2+
WSGI config for core project.
3+
4+
It exposes the WSGI callable as a module-level variable named ``application``.
5+
6+
For more information on this file, see
7+
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
8+
"""
9+
10+
import os
11+
12+
from django.core.wsgi import get_wsgi_application
13+
14+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
15+
16+
application = get_wsgi_application()

manage.py

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#!/usr/bin/env python
2+
"""Django's command-line utility for administrative tasks."""
3+
import os
4+
import sys
5+
6+
7+
def main():
8+
"""Run administrative tasks."""
9+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
10+
try:
11+
from django.core.management import execute_from_command_line
12+
except ImportError as exc:
13+
raise ImportError(
14+
"Couldn't import Django. Are you sure it's installed and "
15+
"available on your PYTHONPATH environment variable? Did you "
16+
"forget to activate a virtual environment?"
17+
) from exc
18+
execute_from_command_line(sys.argv)
19+
20+
21+
if __name__ == '__main__':
22+
main()

requirements.txt

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
asgiref==3.7.2
2+
Django==4.2.6
3+
djangorestframework==3.14.0
4+
pytz==2023.3.post1
5+
sqlparse==0.4.4
6+
typing_extensions==4.8.0
7+
tzdata==2023.3
8+
PyJWT==2.7.0

users/__init__.py

Whitespace-only changes.

users/admin.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from django.contrib import admin
2+
from users.models import User
3+
4+
5+
admin.site.register(User)

users/apps.py

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from django.apps import AppConfig
2+
3+
4+
class UsersConfig(AppConfig):
5+
default_auto_field = 'django.db.models.BigAutoField'
6+
name = 'users'

users/authentication/__init__.py

Whitespace-only changes.

users/authentication/backends.py

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import os
2+
import jwt
3+
4+
from django.conf import settings
5+
6+
from rest_framework import (
7+
authentication,
8+
exceptions
9+
)
10+
11+
from users.models import User
12+
13+
14+
SECRET_KEY = getattr(settings, 'SECRET_KEY', None)
15+
16+
class JWTAuthentication(authentication.BaseAuthentication):
17+
authentication_header_prefix = 'Bearer'
18+
19+
def authenticate(self, request):
20+
"""
21+
The `authenticate` method is called on every request regardless of
22+
whether the endpoint requires authentication.
23+
24+
`authenticate` has two possible return values:
25+
26+
1) `None` - We return `None` if we do not wish to authenticate. Usually
27+
this means we know authentication will fail. An example of
28+
this is when the request does not include a token in the
29+
headers.
30+
31+
2) `(user, token)` - We return a user/token combination when
32+
authentication is successful.
33+
34+
If neither case is met, that means there's an error
35+
and we do not return anything.
36+
We simple raise the `AuthenticationFailed`
37+
exception and let Django REST Framework
38+
handle the rest.
39+
"""
40+
request.user = None
41+
# print('\033[32m' + 'start' + '\033[0m')
42+
# `auth_header` should be an array with two elements: 1) the name of
43+
# the authentication header (in this case, "Bearer") and 2) the JWT
44+
# that we should authenticate against.
45+
auth_header = authentication.get_authorization_header(request).split()
46+
auth_header_prefix = self.authentication_header_prefix.lower()
47+
48+
49+
if not auth_header:
50+
return None
51+
52+
53+
if len(auth_header) == 1:
54+
# Invalid token header. No credentials provided. Do not attempt to authenticate.
55+
56+
return None
57+
58+
elif len(auth_header) > 2:
59+
# Invalid token header. The Token string should not contain spaces. Do
60+
# not attempt to authenticate.
61+
return None
62+
63+
# The JWT library we're using can't handle the `byte` type, which is
64+
# commonly used by standard libraries in Python 3. To get around this,
65+
# we simply have to decode `prefix` and `token`. This does not make for
66+
# clean code, but it is a good decision because we would get an error
67+
# if we didn't decode these values.
68+
prefix = auth_header[0].decode('utf-8')
69+
token = auth_header[1].decode('utf-8')
70+
prefix = auth_header[0].decode('utf-8') if isinstance(auth_header[0], bytes) else auth_header[0]
71+
token = auth_header[1].decode('utf-8') if isinstance(auth_header[1], bytes) else auth_header[1]
72+
73+
if prefix.lower() != auth_header_prefix:
74+
# The auth header prefix is not what we expected. Do not attempt to
75+
# authenticate.
76+
return None
77+
78+
# By now, we are sure there is a *chance* that authentication will
79+
# succeed. We delegate the actual credentials authentication to the
80+
# method below.
81+
return self._authenticate_credentials(request, token)
82+
83+
def _authenticate_credentials(self, request, token):
84+
"""
85+
Try to authenticate the given credentials. If authentication is
86+
successful, return the user and token. If not, throw an error.
87+
"""
88+
try:
89+
payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
90+
91+
except Exception as e:
92+
msg = 'Invalid authentication. Could not decode token.'
93+
raise exceptions.AuthenticationFailed()
94+
95+
try:
96+
user = User.objects.get(pk=payload['id'])
97+
except User.DoesNotExist:
98+
msg = 'No user matching this token was found.'
99+
raise exceptions.AuthenticationFailed(msg)
100+
101+
if not user.is_active:
102+
msg = 'This user has been deactivated.'
103+
raise exceptions.AuthenticationFailed(msg)
104+
105+
return (user, token)

users/exceptions/__init__.py

Whitespace-only changes.

users/managers.py

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from django.contrib.auth.base_user import BaseUserManager
2+
from django.utils.translation import gettext_lazy as _
3+
4+
5+
class CustomUserManager(BaseUserManager):
6+
"""
7+
Custom user model manager where email is the unique identifier
8+
for authentication instead of usernames.
9+
"""
10+
11+
def create_user(self, email, password, **extra_fields):
12+
extra_fields.setdefault("is_verified", True)
13+
extra_fields.setdefault("username", email)
14+
15+
if not extra_fields.get("role"):
16+
raise ValueError("User must have a role assigned.")
17+
18+
if not email:
19+
raise ValueError("Users must have an email address")
20+
21+
if self.model.objects.filter(email=email).exists():
22+
raise ValueError('User email already exists.')
23+
24+
email = self.normalize_email(email)
25+
user = self.model(email=email, **extra_fields)
26+
user.set_password(password)
27+
user.save(using=self._db)
28+
return user
29+
30+
31+
def create_superuser(self, email, password, **extra_fields):
32+
extra_fields.setdefault("is_staff", True)
33+
extra_fields.setdefault("is_superuser", True)
34+
extra_fields.setdefault("is_active", True)
35+
extra_fields.setdefault("role", 'admin')
36+
37+
if extra_fields.get("is_staff") is not True:
38+
raise ValueError("Superuser must have is_staff=True.")
39+
if extra_fields.get("is_superuser") is not True:
40+
raise ValueError("Superuser must have is_superuser=True.")
41+
42+
# Set email and password explicitly
43+
email = self.normalize_email(email)
44+
45+
return self.create_user(email=email, password=password, **extra_fields)

0 commit comments

Comments
 (0)