https://github.com/husein14azimi/jwt
jwt auth template project
https://github.com/husein14azimi/jwt
django-rest-framework djoser jwt
Last synced: about 1 month ago
JSON representation
jwt auth template project
- Host: GitHub
- URL: https://github.com/husein14azimi/jwt
- Owner: husein14azimi
- Created: 2024-11-19T08:14:52.000Z (6 months ago)
- Default Branch: main
- Last Pushed: 2025-01-10T08:26:43.000Z (4 months ago)
- Last Synced: 2025-02-06T04:58:04.266Z (3 months ago)
- Topics: django-rest-framework, djoser, jwt
- Language: Python
- Homepage:
- Size: 67.4 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: readme.md
Awesome Lists containing this project
README
IN THE NAME OF GOD
# jwt project π»
this is a simple django project with two web applications:
1. main app (core)
2. profile app (account)> it is actually a template project for authenticating via jwt
### technologies used in the project:
* RESTful apis
* (Authentication): `jwt` (as the authenticatoin backend) and `djoser` (for pre-written `views` and `urls`)
* (dev environment:) Visual Studio Code on Windows### TO-DO list for the future:
* create-new-user api endpoint
* email validation implementation> [!IMPORTANT]
> In this documentation, it is assumed that you are a django amateur or above. If you are a beginner, you can use AI chatbots to guide you through these steps.# creating this project, step by step:
create a project named `core` and then rename the project to whatever you like. renaming the project is easier than renaming the main application of the project.
**note:** in this doc, in each file's code snippet, the full content of the file is typed; but in some cases (such as `settings.py`), only the code that should be modified is there.
#### editing the `core.settings`
register `core` as a web app in this project:
```
# core.settingsINSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'core',
]
```
if you're working on local host, write:
```
ALLOWED_HOSTS = ['127.0.0.1', 'localhost',]
```you can also edit the time zone:
```
TIME_ZONE = 'Asia/Tehran'
```### virtual environment
virtual environment is dependent on the address of its directory; that's why it is created after renaming the project. change `` to your desired name:```
python -m venv
```install django for your venv
```
pip install django
```## the `user` model
extend the abstract user in the `core.models` so extra fields based on the project requirements can be added to the django base `User` model.
the `username` in this project has no place and the field required for login is `email`. to achieve that, the `username` field is set to `blank=True`. so far, `core.User` does not require any `username` field; but there is some code in the default django codes that still require `username` (which is the `UserManager`). we will change it and use it in the `core.User`.
```
# core.modelsfrom django.db import models
from django.contrib.auth.models import AbstractUser as BaseAbstractUser
from django.contrib.auth.models import BaseUserManager
from django.core.validators import RegexValidatorclass UserManager(BaseUserManager):
def create_user(self, email, phone_number, password=None, **extra_fields):
"""Create and return a 'User' with an email, phone number and password."""
if not email:
raise ValueError('The Email field must be set')
if not phone_number:
raise ValueError('The Phone number field must be set')
email = self.normalize_email(email)
user = self.model(email=email, phone_number=phone_number, **extra_fields)
user.set_password(password) # Use set_password to hash the password
user.save(using=self._db)
return userdef create_superuser(self, email, phone_number, password=None, **extra_fields):
"""Create and return a superuser with an email, phone number and password."""
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)if extra_fields.get('is_staff') is not True:
raise ValueError('Superuser must have is_staff=True.')
if extra_fields.get('is_superuser') is not True:
raise ValueError('Superuser must have is_superuser=True.')return self.create_user(email, phone_number, password, **extra_fields)
class User(BaseAbstractUser ):
username = models.CharField(max_length=255, unique=False, blank=True, null=True)
email = models.EmailField(unique=True)
phone_regex = RegexValidator(regex=r'^09\d{9}$', message="Phone number must start with 09 and be exactly 11 characters.")
phone_number = models.CharField(validators=[phone_regex], max_length=11, unique=True)USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['phone_number']objects = UserManager()
def __str__(self):
return f'{self.first_name} {self.last_name}: {self.email}'
```### admin-registering
we also register this specific `User` to the admin panel. since there are some modified fields in it, they have to be shown to django:
```
# core.adminfrom django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from .models import Userclass UserAdmin(BaseUserAdmin):
model = User
list_display = ('email', 'first_name', 'last_name', 'is_staff')
list_filter = ('is_staff', 'is_active')
ordering = ('email',)
fieldsets = (
(None, {'fields': ('email', 'password')}),
('Personal info', {'fields': ('first_name', 'last_name', 'phone_number')}),
('Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}),
('Important dates', {'fields': ('last_login',)}),
)
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('email', 'password1', 'password2', 'phone_number', 'is_staff', 'is_active')}
),
)
search_fields = ('email',)
filter_horizontal = ('groups', 'user_permissions',)admin.site.register(User, UserAdmin)
```add to the `core.settings`:
```
# core.settingsAUTH_USER_MODEL= 'core.User'
```## migrating
maybe in the first migration, the `core` app is not recognized; therefore, it is recommended to run:
```
python manage.py makemigrations core
```and then run the global migration:
```
python manage.py makemigrations
```
```
python manage.py migrate
```#### creating a superuser
> [!CAUTION]
> If you want to follow this documentation to the end, this action is not recommended because the `Person` model following has a one-to-one relationship with `core.User` and you'll have to delete the user instance manually.if in this step, the additional fields you have added to the `core.User` take part, then you have changed the *auth flow* successfully.
```
python manage.py createsuperuser
```
you can see/create users in the
```
localhost:8000/admin
```## REST framework
install it first:
```
pip install djangorestframework
```
add it to the installed apps in the `core.settings````
# core.settingsINSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'core',
]
```now, you have rest framework in your project.
**map:**
there are two models in the auth flow: `core.User` and `account.Person`. one includes the required fields for authentication such as email, phone number, password; so it stays the same in each project; but the `account.Person` will have different fields based on the project's requirements. we will build do the stuff related to `core.User`, then build the `account.Person` and its configuration and at last, we will connect them together so the user does not need two different forms to update their profile. the `core.User` uses djoser for the view-writing to prevent over-coding and `account.Person` will need its serializers and views. to implement the connection between the two, one approach is developing a new serializer-view-url and the other one is to write the serializer-view for the `account.Person` in a way that fetches the data from `core.User`, combine them with the `account.Person` data and show them all together to the user.therefore, there will be 3 steps:
1. creating the user model and its configurations
2. creating the person model and its configurations
3. connecting the two### install djoser and jwt
run:
```
pip install djangorestframework_simplejwt
```
```
pip install djoser
```register djoser as an app:
```
# core.settingsINSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'djoser',
'core',
]
```add the djoser url endpoints:
```
# core.urlsfrom django.contrib import admin
from django.urls import path, includeurlpatterns = [
path('admin/', admin.site.urls),
path('auth/', include('djoser.urls')),
path('auth/', include('djoser.urls.jwt')),
]
```set jwt as the authenticatoin backend (you can add this at the bottom of the `core.settings`) π:
> [!NOTE]
> this setting sets the jwt authentication to auth layers of rest framework. without this, I don't think rest framework has any default auth security π.```
# core.settingsREST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
),
}
```and add:
```
# core.settingsSIMPLE_JWT = {
'AUTH_HEADER_TYPES': ('JWT',),
}
```> [!NOTE]
> with the code snippet above π, we determine in what format the jwt auth headers should be.or if you want more customized settings:
```
# core.settingsfrom datetime import timedelta
SIMPLE_JWT = {
'AUTH_HEADER_TYPES': ('JWT',),
"ACCESS_TOKEN_LIFETIME": timedelta(days=3),
"REFRESH_TOKEN_LIFETIME": timedelta(days=30),
}
```the expiration date is three days for access token and a month for refresh token. the timedelta import and use is required because jwt can't work with int.
**map:** the `core.User` and auth configuration is implemented. now, we implement the `person`(profile) model.
> [!NOTE]
> to get/test the current user's data, use the following url. this url will return the data of the user in the django auth system (`core.User`).
```
localhost:8000/auth/users/me/
```> [!NOTE]
> with the current implementation, djoser has three main urls:
1. `auth/jwt/verify` for verifying the access token,
2. `auth/jwt/refresh` for getting a new access token (you'll need to provide a valid refresh token) and
3. `auth/jwt/create` for getting a new pair of access and refesh tokens (providing the login credentials (i.e., **email** and **password**)).## `account` app and `Person` (profile) model
run:
```
python manage.py startapp account
```in `core.urls`, add the url for account app:
```
# core.urlsfrom django.contrib import admin
from django.urls import path, includeurlpatterns = [
path('admin/', admin.site.urls),
path('auth/', include('djoser.urls')),
path('auth/', include('djoser.urls.jwt')),
path('account/', include('account.urls')),
]```
create the `urls` file in the `account` app (does not include any urls for now):
```
# account.urlsurlpatterns = [
]
```add it to the installed apps:
```
# core.settingsINSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'core',
'account',
]
```create the `person` model:
```
# account.modelsfrom django.db import models
from django.conf import settingsgender_choices = (
('M', 'Male'),
('F', 'Female'),
)class Person(models.Model):
user = models.OneToOneField(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
gender = models.CharField(max_length=1, choices=gender_choices, blank=True, null=True)
birth_date = models.DateField(blank=True, null=True)
bio = models.TextField(blank=True)
updated_at = models.DateTimeField(auto_now=True)def __str__(self):
return f'{self.user.first_name} {self.user.last_name}: {self.user.email}'
```(optional) register the `account.Person` for the `admin` panel:
```
# account.adminfrom django.contrib import admin
from .models import Personadmin.site.register(Person)
```run the migrations afterwise.
**note:**
the following serializer is a complex serializer that combines the `User` and `Person` models. how? well, first it fetches the `User` data from the main app and then, combines it with the `Person` model.write the needed serializer-view-url:
```
# account.serializersfrom django.contrib.auth import get_user_model
from rest_framework import serializersUser = get_user_model()
class CombinedUserPersonSerializer(serializers.ModelSerializer):
bio = serializers.CharField(source='person.bio', allow_blank=True)
birth_date = serializers.DateField(source='person.birth_date', allow_null=True)
gender = serializers.CharField(source='person.gender', allow_null=True)
updated_at = serializers.DateTimeField(source='person.updated_at', read_only=True)class Meta:
model = User
fields = ['id', 'username', 'email', 'phone_number', 'date_joined', 'last_login', 'bio', 'birth_date', 'gender', 'updated_at',]
read_only_fields = ['id', 'username', 'date_joined', 'last_login',]
``````
# account.viewsfrom rest_framework import viewsets
from rest_framework.mixins import RetrieveModelMixin, UpdateModelMixin
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from .serializers import CombinedUserPersonSerializer
from django.contrib.auth import get_user_modelUser = get_user_model()
class CombinedUserProfileViewSet(RetrieveModelMixin, UpdateModelMixin, viewsets.GenericViewSet):
serializer_class = CombinedUserPersonSerializer
# the permission is to prevent the error caused by anonymous user (that has no email) to enter the view.
permission_classes = [IsAuthenticated]
def get_queryset(self):
# Allow only the user or admin to access their profile
if self.request.user.is_staff:
return User.objects.all()
return User.objects.filter(id=self.request.user.id)@action(detail=False, methods=['get', 'put', 'patch'])
def me(self, request):
user = request.user
if request.method == 'GET':
serializer = self.get_serializer(user)
return Response(serializer.data)
elif request.method in ['PUT', 'PATCH']:
serializer = self.get_serializer(user, data=request.data, partial=request.method == 'PATCH')
serializer.is_valid(raise_exception=True)
self.update_user_profile(user, serializer.validated_data)
return Response(serializer.data)def update_user_profile(self, user, validated_data):
# Handle nested person data
person_data = validated_data.pop('person', {})# Update the user instance
user = super().update(user, validated_data)# Update the person instance if it exists
person = user.person
if person_data:
for attr, value in person_data.items():
setattr(person, attr, value)
person.save()return user
```
about the `me` actionπ: this funtion returns the profile associated with the user model. therefore, the url `/account/persons/me` returns the user's profile in a rest response. also, if the user is admin, he can see other profiles using `/account/persons/` (this thing is done by the `get_queryset` method). this url also works if you own the profile with the ``.```
# account.urlsfrom django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import CombinedUserProfileViewSetrouter = DefaultRouter()
router.register(r'persons', CombinedUserProfileViewSet, basename='user')urlpatterns = [
path(r'', include(router.urls)),
]
```**map:** both the profile and user models and their configurations are implemented; but they are not connected together. the next part (signals) takes care of that so when a user model is created, a profile model associated to it is automatically created. (this means that the `Person` and `User` models had to be connected manually before)
## connecting the `User` and `Person` models
in the `account` app, write in the `signals` file:
```
# account.signalsfrom django.db.models.signals import post_save
from django.dispatch import receiver
from django.conf import settings
from .models import Person
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_person(sender, instance, created, **kwargs):
if created:
Person.objects.create(user=instance)
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def save_person(sender, instance, **kwargs):
instance.person.save()
```to get this `signal` run automatically, get it in the ready state:
```
# account.appsfrom django.apps import AppConfig
class AccountConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'account'def ready(self) -> None:
import account.signals
```**map:** now, the url `account/persons/me` containing the proper access token in the request header will return the `core.User` and `account.Person` all together.
**note:** the email field in the `account.serializers` is not `read_only`, as some users may decide to change their emails. you can change it there.
congrats! you got yourself a jwt auth django project!
## making the auth backend able to read the tokens from the cookies
in the `account` app, create the `authentication.py`. this module reads the jwt token from the request cookies. the cookie has two main fields: `key` and `value`. the key must be `'jwt_access'` (it is modifiable and you can change it in this module) and the value must be the access token. Here comes the module:
```
#account.authentication.pyfrom rest_framework_simplejwt.authentication import JWTAuthentication
from rest_framework.exceptions import AuthenticationFailedclass CookieJWTAuthentication(JWTAuthentication):
def authenticate(self, request):
# First, try to authenticate using the standard method (from headers)
auth_result = super().authenticate(request)if auth_result is not None:
return auth_result# If no authentication was found in headers, check cookies
token = request.COOKIES.get('jwt_access')
if not token:
return None # No token found in cookies# Validate the token
validated_token = self.get_validated_token(token)return self.get_user(validated_token), validated_token
```add this layer of auth to the rest framework auth layers (currently it is just jwt-reading-from-the-header layer). the order matters, so when the cookie layer is above the header layer, the project will look for the jwt token in the request cookies.
```
# core.settingsREST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'account.authentication.CookieJWTAuthentication', # the custom cookie-reading authentication class
'rest_framework_simplejwt.authentication.JWTAuthentication', # Fallback to default
),
}
```
> [!NOTE]
> the project still is checking the request headers. if you don't want it, you can remove it from the `REST_FRAMEWORK` settings.> [!NOTE]
> the cookie containing the jwt access token must be named `jwt_access`. if you want to change it, modify it in the `account.authentication`.> [!IMPORTANT]
> this project does not set/save the cookie on the client (frontend can do it); therefore, if you want to test the implementation, you have to create the cookie manually.> [!IMPORTANT]
> when retrieving a person model in the rest response (e.g. the `account/persons/me/` url), the `id` field is for the `core.User`, not the `account.Person`.
a big thanks to the AIs that helped me in this project;
[blackbox.ai](https://www.blackbox.ai)
[perplexity.ai](https://www.perplexity.ai/)
and
chatgpt and copilot (not quite a lot)
this markdown text was created on https://markdownlivepreview.com/