├── README.md ├── jwt_token_authentication ├── __init__.py ├── asgi.py ├── settings.py ├── urls.py └── wsgi.py ├── manage.py └── members ├── __init__.py ├── admin.py ├── apps.py ├── migrations └── __init__.py ├── models.py ├── serializers.py ├── urls.py ├── utils.py └── views.py /README.md: -------------------------------------------------------------------------------- 1 | # JWT Token Authentication 2 | 3 | Using JWT authentication with Django Rest Framework. 4 | 5 | 6 | ## API endpoints: 7 | 8 | * `/api/user/register/` 9 | * `/api/user/login/` 10 | * `/api/user/` 11 | * `/api/user/logout/` 12 | 13 | 14 | ## Example usage 15 | 16 | This repo can be used from the command line with the following steps: 17 | 18 | ``` 19 | python manage.py makemigrations 20 | python manage.py migrate 21 | python manage.py runserver 22 | 23 | ``` 24 | 25 | You can also use it with **httpie** as follows: 26 | 27 | ``` 28 | http GET http://127.0.0.1:8000/api/user/ "Cookie:access_token=" 29 | ``` 30 | -------------------------------------------------------------------------------- /jwt_token_authentication/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotja/jwt_token_authentication/b32b348f3d76eb005f9acea8f1692af047e60f1d/jwt_token_authentication/__init__.py -------------------------------------------------------------------------------- /jwt_token_authentication/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for jwt_token_authentication 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/3.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', 'jwt_token_authentication.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /jwt_token_authentication/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for jwt_token_authentication project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.2.4. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.2/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | from datetime import timedelta 15 | 16 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 17 | BASE_DIR = Path(__file__).resolve().parent.parent 18 | 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = 'django-insecure-&o*y(slzh!mvh*%+z1dl3jvtdx+*xigc^lo9fsjox16ex7jo5w' 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = True 28 | 29 | ALLOWED_HOSTS = [] 30 | 31 | ## custom user model 32 | AUTH_USER_MODEL = 'members.User' 33 | 34 | 35 | # Application definition 36 | 37 | INSTALLED_APPS = [ 38 | 'django.contrib.admin', 39 | 'django.contrib.auth', 40 | 'django.contrib.contenttypes', 41 | 'django.contrib.sessions', 42 | 'django.contrib.messages', 43 | 'django.contrib.staticfiles', 44 | 'rest_framework', 45 | 'corsheaders', 46 | 'members.apps.MembersConfig', 47 | ] 48 | 49 | MIDDLEWARE = [ 50 | 'django.middleware.security.SecurityMiddleware', 51 | 'django.contrib.sessions.middleware.SessionMiddleware', 52 | 'django.middleware.common.CommonMiddleware', 53 | 'django.middleware.csrf.CsrfViewMiddleware', 54 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 55 | 'django.contrib.messages.middleware.MessageMiddleware', 56 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 57 | ] 58 | 59 | ROOT_URLCONF = 'jwt_token_authentication.urls' 60 | 61 | 62 | 63 | TEMPLATES = [ 64 | { 65 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 66 | 'DIRS': [], 67 | 'APP_DIRS': True, 68 | 'OPTIONS': { 69 | 'context_processors': [ 70 | 'django.template.context_processors.debug', 71 | 'django.template.context_processors.request', 72 | 'django.contrib.auth.context_processors.auth', 73 | 'django.contrib.messages.context_processors.messages', 74 | ], 75 | }, 76 | }, 77 | ] 78 | 79 | WSGI_APPLICATION = 'jwt_token_authentication.wsgi.application' 80 | 81 | 82 | # Database 83 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 84 | 85 | DATABASES = { 86 | 'default': { 87 | 'ENGINE': 'django.db.backends.sqlite3', 88 | 'NAME': BASE_DIR / 'db.sqlite3', 89 | } 90 | } 91 | 92 | 93 | # Password validation 94 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 95 | 96 | AUTH_PASSWORD_VALIDATORS = [ 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 99 | }, 100 | { 101 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 102 | }, 103 | { 104 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 105 | }, 106 | { 107 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 108 | }, 109 | ] 110 | 111 | 112 | # Internationalization 113 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 114 | 115 | LANGUAGE_CODE = 'en-us' 116 | 117 | TIME_ZONE = 'UTC' 118 | 119 | USE_I18N = True 120 | 121 | USE_L10N = True 122 | 123 | USE_TZ = True 124 | 125 | 126 | # Static files (CSS, JavaScript, Images) 127 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 128 | 129 | STATIC_URL = '/static/' 130 | 131 | # Default primary key field type 132 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 133 | 134 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 135 | -------------------------------------------------------------------------------- /jwt_token_authentication/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path, include 3 | 4 | urlpatterns = [ 5 | path('admin/', admin.site.urls), 6 | path('api/', include('members.urls')), 7 | ] 8 | -------------------------------------------------------------------------------- /jwt_token_authentication/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for jwt_token_authentication 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/3.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', 'jwt_token_authentication.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 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', 'jwt_token_authentication.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() 23 | -------------------------------------------------------------------------------- /members/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotja/jwt_token_authentication/b32b348f3d76eb005f9acea8f1692af047e60f1d/members/__init__.py -------------------------------------------------------------------------------- /members/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /members/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MembersConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'members' 7 | -------------------------------------------------------------------------------- /members/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotja/jwt_token_authentication/b32b348f3d76eb005f9acea8f1692af047e60f1d/members/migrations/__init__.py -------------------------------------------------------------------------------- /members/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.base_user import BaseUserManager 3 | from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin 4 | 5 | 6 | class CustomUserManager(BaseUserManager): 7 | def create_user(self, email, password=None): 8 | if not email: 9 | raise ValueError('A user email is needed.') 10 | 11 | if not password: 12 | raise ValueError('A user password is needed.') 13 | 14 | email = self.normalize_email(email) 15 | user = self.model(email=email) 16 | user.set_password(password) 17 | user.save() 18 | return user 19 | 20 | def create_superuser(self, email, password=None): 21 | if not email: 22 | raise ValueError('A user email is needed.') 23 | 24 | if not password: 25 | raise ValueError('A user password is needed.') 26 | 27 | user = self.create_user(email, password) 28 | user.is_superuser = True 29 | user.is_staff = True 30 | user.save() 31 | return user 32 | 33 | 34 | class User(AbstractBaseUser, PermissionsMixin): 35 | user_id = models.AutoField(primary_key=True) 36 | email = models.EmailField(max_length=100, unique=True) 37 | username = models.CharField(max_length=100) 38 | is_active = models.BooleanField(default=True) 39 | is_staff = models.BooleanField(default=False) 40 | date_joined = models.DateField(auto_now_add=True) 41 | USERNAME_FIELD = 'email' 42 | REQUIRED_FIELDS = ['username'] 43 | objects = CustomUserManager() 44 | 45 | def __str__(self): 46 | return self.email 47 | 48 | 49 | -------------------------------------------------------------------------------- /members/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from django.contrib.auth import get_user_model 3 | 4 | 5 | class UserRegistrationSerializer(serializers.ModelSerializer): 6 | password = serializers.CharField(max_length=100, min_length=8, style={'input_type': 'password'}) 7 | class Meta: 8 | model = get_user_model() 9 | fields = ['email', 'username', 'password'] 10 | 11 | def create(self, validated_data): 12 | user_password = validated_data.get('password', None) 13 | db_instance = self.Meta.model(email=validated_data.get('email'), username=validated_data.get('username')) 14 | db_instance.set_password(user_password) 15 | db_instance.save() 16 | return db_instance 17 | 18 | 19 | 20 | class UserLoginSerializer(serializers.Serializer): 21 | email = serializers.CharField(max_length=100) 22 | username = serializers.CharField(max_length=100, read_only=True) 23 | password = serializers.CharField(max_length=100, min_length=8, style={'input_type': 'password'}) 24 | token = serializers.CharField(max_length=255, read_only=True) 25 | 26 | -------------------------------------------------------------------------------- /members/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from members.views import ( 3 | UserRegistrationAPIView, 4 | UserLoginAPIView, 5 | UserViewAPI, 6 | UserLogoutViewAPI 7 | ) 8 | 9 | 10 | urlpatterns = [ 11 | path('user/register/', UserRegistrationAPIView.as_view()), 12 | path('user/login/', UserLoginAPIView.as_view()), 13 | path('user/', UserViewAPI.as_view()), 14 | path('user/logout/', UserLogoutViewAPI.as_view()), 15 | ] 16 | -------------------------------------------------------------------------------- /members/utils.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from datetime import datetime, timedelta 3 | import jwt 4 | 5 | 6 | def generate_access_token(user): 7 | payload = { 8 | 'user_id': user.user_id, 9 | 'exp': datetime.utcnow() + timedelta(days=1, minutes=0), 10 | 'iat': datetime.utcnow(), 11 | } 12 | 13 | access_token = jwt.encode(payload, settings.SECRET_KEY, algorithm='HS256') 14 | return access_token -------------------------------------------------------------------------------- /members/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from members.serializers import UserRegistrationSerializer, UserLoginSerializer 3 | from rest_framework.views import APIView 4 | from rest_framework.authentication import TokenAuthentication 5 | from rest_framework.permissions import AllowAny, IsAuthenticated 6 | from rest_framework.response import Response 7 | from rest_framework import status 8 | from rest_framework.exceptions import AuthenticationFailed 9 | from django.contrib.auth import authenticate 10 | from django.conf import settings 11 | from django.contrib.auth import get_user_model 12 | from .utils import generate_access_token 13 | import jwt 14 | 15 | 16 | 17 | class UserRegistrationAPIView(APIView): 18 | serializer_class = UserRegistrationSerializer 19 | authentication_classes = (TokenAuthentication,) 20 | permission_classes = (AllowAny,) 21 | 22 | def get(self, request): 23 | content = { 'message': 'Hello!' } 24 | return Response(content) 25 | 26 | def post(self, request): 27 | serializer = self.serializer_class(data=request.data) 28 | if serializer.is_valid(raise_exception=True): 29 | new_user = serializer.save() 30 | if new_user: 31 | access_token = generate_access_token(new_user) 32 | data = { 'access_token': access_token } 33 | response = Response(data, status=status.HTTP_201_CREATED) 34 | response.set_cookie(key='access_token', value=access_token, httponly=True) 35 | return response 36 | return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 37 | 38 | 39 | 40 | class UserLoginAPIView(APIView): 41 | serializer_class = UserLoginSerializer 42 | authentication_classes = (TokenAuthentication,) 43 | permission_classes = (AllowAny,) 44 | 45 | def post(self, request): 46 | email = request.data.get('email', None) 47 | user_password = request.data.get('password', None) 48 | 49 | if not user_password: 50 | raise AuthenticationFailed('A user password is needed.') 51 | 52 | if not email: 53 | raise AuthenticationFailed('An user email is needed.') 54 | 55 | user_instance = authenticate(username=email, password=user_password) 56 | 57 | if not user_instance: 58 | raise AuthenticationFailed('User not found.') 59 | 60 | if user_instance.is_active: 61 | user_access_token = generate_access_token(user_instance) 62 | response = Response() 63 | response.set_cookie(key='access_token', value=user_access_token, httponly=True) 64 | response.data = { 65 | 'access_token': user_access_token 66 | } 67 | return response 68 | 69 | return Response({ 70 | 'message': 'Something went wrong.' 71 | }) 72 | 73 | 74 | 75 | class UserViewAPI(APIView): 76 | authentication_classes = (TokenAuthentication,) 77 | permission_classes = (AllowAny,) 78 | 79 | def get(self, request): 80 | user_token = request.COOKIES.get('access_token') 81 | 82 | if not user_token: 83 | raise AuthenticationFailed('Unauthenticated user.') 84 | 85 | payload = jwt.decode(user_token, settings.SECRET_KEY, algorithms=['HS256']) 86 | 87 | user_model = get_user_model() 88 | user = user_model.objects.filter(user_id=payload['user_id']).first() 89 | user_serializer = UserRegistrationSerializer(user) 90 | return Response(user_serializer.data) 91 | 92 | 93 | 94 | class UserLogoutViewAPI(APIView): 95 | authentication_classes = (TokenAuthentication,) 96 | permission_classes = (AllowAny,) 97 | 98 | def get(self, request): 99 | user_token = request.COOKIES.get('access_token', None) 100 | if user_token: 101 | response = Response() 102 | response.delete_cookie('access_token') 103 | response.data = { 104 | 'message': 'Logged out successfully.' 105 | } 106 | return response 107 | response = Response() 108 | response.data = { 109 | 'message': 'User is already logged out.' 110 | } 111 | return response 112 | 113 | 114 | --------------------------------------------------------------------------------