[PYTHON] Create APIs around user authentication with Django REST Framework

I made the following API using Django REST Framework, so make a note.

--Login --User created --Get user information --User information update (including password update) --User deletion

Even though I made it, the part I made with a relatively large atmosphere is large, and there is also the problem of whether this is optimal, so it is for reference only.

Creating a model

Regarding the model, you can use the default user model, but if you want to create an actual application, you will need to customize something, so I created the following model

user/models.py


from django.db import models
from django.contrib.auth.models import (
    BaseUserManager, AbstractBaseUser, _user_has_perm
)
from django.core import validators
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone


class AccountManager(BaseUserManager):
    def create_user(self, request_data, **kwargs):
        now = timezone.now()
        if not request_data['email']:
            raise ValueError('Users must have an email address.')

        profile = ""
        if request_data.get('profile'):
            profile = request_data['profile']

        user = self.model(
            username=request_data['username'],
            email=self.normalize_email(request_data['email']),
            is_active=True,
            last_login=now,
            date_joined=now,
            profile=profile
        )

        user.set_password(request_data['password'])
        user.save(using=self._db)
        return user

    def create_superuser(self, username, email, password, **extra_fields):
        request_data = {
            'username': username,
            'email': email,
            'password': password
        }
        user = self.create_user(request_data)
        user.is_active = True
        user.is_staff = True
        user.is_admin = True
        user.save(using=self._db)
        return user


class Account(AbstractBaseUser):
    username    = models.CharField(_('username'), max_length=30, unique=True)
    first_name  = models.CharField(_('first name'), max_length=30, blank=True)
    last_name   = models.CharField(_('last name'), max_length=30, blank=True)
    email       = models.EmailField(verbose_name='email address', max_length=255, unique=True)
    profile     = models.CharField(_('profile'), max_length=255, blank=True)
    is_active   = models.BooleanField(default=True)
    is_staff    = models.BooleanField(default=False)
    is_admin    = models.BooleanField(default=False)
    date_joined = models.DateTimeField(_('date joined'), default=timezone.now)

    objects = AccountManager()

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['username']

    def user_has_perm(user, perm, obj):
        return _user_has_perm(user, perm, obj)

    def has_perm(self, perm, obj=None):
        return _user_has_perm(self, perm, obj=obj)

    def has_module_perms(self, app_label):
        return self.is_admin

    def get_short_name(self):
        return self.first_name

    @property
    def is_superuser(self):
        return self.is_admin

    class Meta:
        db_table = 'api_user'
        swappable = 'AUTH_USER_MODEL'

You can create the admin user as usual with $ python manage.py createsuperuser. The following two points are different from the default.

  1. Change the field to combine password with login from username to email This is applied in the following part.
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username']
  1. Add a field to enter a profile (profile)
profile     = models.CharField(_('profile'), max_length=255, blank=True)

that's all.

Implement API for login

This time, we implemented token authentication by JWT. Install django-rest-framework-jwt with pip.

$ pip install djangorestframework-jwt

After installation, add the following to settings.py.

djangoproject/settings.py


JWT_AUTH = {
    'JWT_VERIFY_EXPIRATION': False,
    'JWT_AUTH_HEADER_PREFIX': 'JWT',
}


REST_FRAMEWORK = { 
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    ),  
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
    ),  
    'NON_FIELD_ERRORS_KEY': 'detail',
    'TEST_REQUEST_DEFAULT_FORMAT': 'json'
}

If you set 'JWT_VERIFY_EXPIRATION': False,, the token expiration date will be permanent.
Next, go to url.py as follows

djangoproject/urls.py


from django.conf.urls import url

from rest_framework_jwt.views import obtain_jwt_token

urlpatterns = [
    url(r'^login/', obtain_jwt_token),
]

With this alone, you can get a token by POSTing to / login using email and password.

Implement user-created API

Add url

First of all, we need an API endpoint, so create urls.py in the application and

user/urls.py


from django.conf.urls import include, url
from rest_framework import routers
from .views import AuthRegister

urlpatterns = [
    url(r'^register/$', AuthRegister.as_view()),
]

Load it into urls.py on the project side.

djangoproject/urls.py


from django.conf.urls import include, url
from django.contrib import admin

from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
    url(r'^login/', obtain_jwt_token),
    url(r'^api/', include('authentication.urls')),
]

POST to / api / register / to allow user creation

Create View

user/views.py


from django.contrib.auth import authenticate
from django.db import transaction
from django.http import HttpResponse, Http404
from rest_framework import authentication, permissions, generics
from rest_framework_jwt.settings import api_settings
from rest_framework.exceptions import AuthenticationFailed
from rest_framework.response import Response
from rest_framework import status, viewsets, filters
from rest_framework.views import APIView
from .serializer import AccountSerializer
from .models import Account, AccountManager

#User-created View(POST)
class AuthRegister(generics.CreateAPIView):
    permission_classes = (permissions.AllowAny,)
    queryset = Account.objects.all()
    serializer_class = AccountSerializer

    @transaction.atomic
    def post(self, request, format=None):
        serializer = AccountSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

By using generics, you can limit the HTTP methods that the class can accept. Check here for more information on generics (http://www.django-rest-framework.org/api-guide/generic-views/). CreateAPIView is used for create-only endpoints.

Also, as a matter of course, for user creation, it is necessary to POST to the endpoint even if it is not authenticated, so permission_classes = (permissions.AllowAny,) is set.

Creating a serializer

from django.contrib.auth import update_session_auth_hash
from rest_framework import serializers

from .models import Account, AccountManager


class AccountSerializer(serializers.ModelSerializer):
    password = serializers.CharField(write_only=True, required=False)

    class Meta:
        model = Account
        fields = ('id', 'username', 'email', 'profile', 'password')

    def create(self, validated_data):
        return Account.objects.create_user(request_data=validated_data)

Roughly speaking, it feels like calling a user-created method defined in Model.

For the time being, if you POST the json of email, password, profile (optional) to / api / register / in the above state, the user will be created.

Acquisition of login user information

Regarding the acquisition of user information, it depends on the application, but for the time being, the logged-in user can only acquire his own information. If you want to get information about other users, you need to create another view and endpoint, so don't worry.

Editing urls.py

Edit urls.py on the application side.

user/urls.py


from django.conf.urls import include, url
from rest_framework import routers
from .views import AuthRegister, AuthInfoGetView

urlpatterns = [
    url(r'^register/$', AuthRegister.as_view()),
    url(r'^mypage/$', AuthInfoGetView.as_view()),
]

Get user information when throwing GET to / api / mypage.

Edit View

Add the following to views.py earlier

user/views.py


#View of user information acquisition(GET)
class AuthInfoGetView(generics.RetrieveAPIView):
    permission_classes = (permissions.IsAuthenticated,)
    queryset = Account.objects.all()
    serializer_class = AccountSerializer

    def get(self, request, format=None):
        return Response(data={
            'username': request.user.username,
            'email': request.user.email,
            'profile': request.user.profile,
            },
            status=status.HTTP_200_OK)

RetrieveAPIView is an endpoint dedicated to the GET method. Also, since I am trying to get my own information, I am trying to get it only when I am logged in with permission_classes = (permissions.IsAuthenticated,).

In this state, add {'Content-Type':'application / json','Authorization':'JWT [token obtained at login]'} to the header and throw the GET method to log in. You can get the username / email / profile of the user you are.

Login user information update (including password update)

There is a talk about whether to use PUT or PATCH when updating information, but for the time being, I used PUT this time, but now that I think about it, I think that PATCH is good. However, since I made it, I will talk about it in PUT.

Editing urls.py

Create an endpoint called / auth_update and call the view AuthInfoUpdateView.

user/urls.py


from django.conf.urls import include, url
from rest_framework import routers
from .views import AuthRegister, AuthInfoGetView, AuthInfoUpdateView

urlpatterns = [
    url(r'^register/$', AuthRegister.as_view()),
    url(r'^mypage/$', AuthInfoGetView.as_view()),
    url(r'^auth_update/$', AuthInfoUpdateView.as_view()),
]

Edit View

Add the following to views.py. Implementation of AuthInfoUpdateView, a view that is called when there is a PUT in / auth_update. Since email is used as the key, it is necessary to include the email of the logged-in user in JSON when sending PUT.

user/views.py


#View of user information update(PUT)
class AuthInfoUpdateView(generics.UpdateAPIView):
    permission_classes = (permissions.IsAuthenticated,)
    serializer_class = AccountSerializer
    lookup_field = 'email'
    queryset = Account.objects.all()

    def get_object(self):
        try:
            instance = self.queryset.get(email=self.request.user)
            return instance
        except Account.DoesNotExist:
            raise Http404

As the name suggests, UpdateAPIView accepts PUT / PATCH as an update-only view.

Editing Serializer

Edit AccountSerializer in serializers.py as follows.

user/serializers.py


class AccountSerializer(serializers.ModelSerializer):
    password = serializers.CharField(write_only=True, required=False)

    class Meta:
        model = Account
        fields = ('id', 'username', 'email', 'profile', 'password')

    def create(self, validated_data):
        return Account.objects.create_user(request_data=validated_data)

    def update(self, instance, validated_data):
        if 'password' in validated_data:
            instance.set_password(validated_data['password'])
        else:
            instance = super().update(instance, validated_data)
        instance.save()
        return instance

If the password is updated, it is necessary to hash the received password, so use the set_password method. As with getting user information, you need to PUT with {'Content-Type':'application / json','Authorization':'JWT [token obtained at login]'} in the header.

Delete user

Finally, we will implement the deletion of the logged-in user.

Editing urls.py

I create an endpoint called / delete and call AuthInfoDeleteView.

user/urls.py


from django.conf.urls import include, url
from rest_framework import routers
from .views import AuthRegister, AuthInfoGetView, AuthInfoUpdateView, AuthInfoDeleteView

urlpatterns = [
    url(r'^register/$', AuthRegister.as_view()),
    url(r'^mypage/$', AuthInfoGetView.as_view()),
    url(r'^auth_update/$', AuthInfoUpdateView.as_view()),
    url(r'^delete/$', AuthInfoDeleteView.as_view()),
]

Edit View

An implementation of AuthInfoDeleteView.

user/views.py


#User Delete View(DELETE)
class AuthInfoDeleteView(generics.DestroyAPIView):
    permission_classes = (permissions.IsAuthenticated,)
    serializer_class = AccountSerializer
    lookup_field = 'email'
    queryset = Account.objects.all()

    def get_object(self):
        try:
            instance = self.queryset.get(email=self.request.user)
            return instance
        except Account.DoesNotExist:
            raise Http404

By setting generics.DestroyAPIView, it will be a view that accepts only the DELETE method. By sending DELETE with {'Content-Type':'application / json','Authorization':'JWT [token obtained at login]'} in the header as well as getting and updating user information You can delete the logged-in user.

that's all.

Issues

Actually, email cannot be updated in the current state. No, I can do it, but since the token I have on the client side is the one logged in with the old email, the behavior will be strange if the client side does not have the token obtained with the new email. You can issue a new token with a new email by POSTing to / login with a password and a new email, but that's nonsense. You need to have the password in clear text on the client side. For the time being, I would like to investigate this in a little more detail.

Also, I think it is necessary to prepare an API for reissuing passwords in order to forget the password, but that was annoying </ s>, so I decided not to do it this time.

Also, the obvious problem is that if the login token is obtained in some way, the user information can be easily tampered with or deleted. However, I think the problem of maliciously robbing tokens is a matter of JWT. This time the token expiration date is infinite, but I wonder if it is necessary to set an expiration date and issue a refresh token when sending a request to the API when actually publishing the application. I think.

In addition, the one that combines the above API and Angular on the front is in the following repository. https://github.com/xKxAxKx/auth_django_and_angular2

Recommended Posts

Create APIs around user authentication with Django REST Framework
Create RESTful APIs with Django Rest Framework
Implementing authentication in Django REST Framework with djoser
Implementation of custom user model authentication in Django REST Framework with djoser
Django REST framework with Vue.js
Login with django rest framework
Create a Todo app with Django REST Framework + Angular
[Django] Use MessagePack with Django REST framework
CRUD POST with Nuxt & Django REST Framework
CRUD GET with Nuxt & Django REST Framework ①
Create a REST API to operate dynamodb with the Django REST Framework
CRUD PUT, DELETE with Nuxt & Django REST Framework
Django REST framework basics
Django Rest Framework Tips
When you want to filter with Django REST framework
Implement APIs at explosive speed using Django REST Framework
Implement hierarchical URLs with drf-nested-routers in Django REST framework
Create an API with Django
Create a homepage with django
Django REST framework stumbling block
Easily create authentication, user management, and multilingual systems with Flask-AppBuilder
I'm trying to create an authentication / authorization process with Django
Create an authentication feature with django-allauth and CustomUser in Django
Create user authentication function in Airflow
Implementation of JWT authentication functionality in Django REST Framework using djoser
Implementation of CRUD using REST API with Python + Django Rest framework + igGrid
Create a file uploader with Django
How to deal with garbled characters in json of Django REST Framework
Logical deletion in Django, DRF (Django REST Framework)
Understand the benefits of the Django Rest Framework
ng-admin + Django REST framework ready-to-create administration tool
Miscellaneous notes about the Django REST framework
Create an update screen with Django Updateview
Create your first app with Django startproject
Django REST Framework + Clean Architecture Design Consideration
I want to create an API that returns a model with a recursive relationship in the Django REST Framework
How to automatically generate API document with Django REST framework & POST from document screen
Django: Record User Agent and manage with Admin
Create a dashboard for Network devices with Django!
Create Nginx + uWSGI + Python (Django) environment with docker
Django REST framework A little useful to know.
Create a one-file hello world application with django
Implement JWT login functionality in Django REST framework
How to create a Rest Api in Django
Web App Development Practice: Create a Shift Creation Page with Django! (Authentication system processing)
Sometimes you want to access View information from Serializer with DRF (Django REST Framework)