DRFの認証処理(TokenAuthentication)

# 概要

Django REST Frameworkのトークン認証の方法についてまとめました。

## Django REST Framework
Token Authenticationのドキュメントは以下

https://www.django-rest-framework.org/api-guide/authentication/#tokenauthentication

## トークン認証とは？
トークン認証とは、ユーザーがユーザーIDとパスワードなどの自身の情報をリクエストすることで、アクセストークンを受け取り、その後のリクエストは受け取ったトークンを使用し、トークンの有効期間内はアクセスを可能とするような認証方法です。

# GitHub

今回解説するサンプルソースは、GitHubにて公開しています。

https://github.com/ryo-keima/drf-authentication-demo

# 環境

## 各種バージョン

- macOS Monterey 12.4
- Apple M1 Max
- Python v3.10.9
- Django v4.1.4
- djangorestframework v3.14.0

## ディレクトリ構成

```text
drf-authentication-demo/
 ├build/
 │ └Dockerfile
 ├projects/
 │ └token_auth/
 ├venv/
 ├docker-compose.yml 
 └requirements.txt
```

# 実践

## 仮想環境構築

仮想環境を構築し、アクティベートします。

```
drf-authentication-demo % python -m venv venv
drf-authentication-demo % source venv/bin/activate
```

requirements.txtを作成し、以下の内容を記載します。

```
Django==4.1.4
djangorestframework==3.14.0
```

必要なパッケージをインストールします。

```
(venv) drf-authentication-demo % pip install -r requirements.txt
```

## Djangoプロジェクト作成

projectsディレクトリにtoken_authディレクトリを生成し、token_authディレクトリ以下でDjango プロジェクトを作成します。

```
(venv) drf-authentication-demo % cd projects/token_auth
(venv) token_auth % django-admin startproject project .
```

一度、開発サーバーを起動し、デフォルト画面が表示できるかを確認すると良いでしょう。以下のコマンドを実行後、ブラウザで`http://127.0.0.1:8000`にアクセスし、Djangoのデフォルト画面が表示されればOKです。

```
(venv) token_auth % python manage.py runserver
```

## アプリ作成

トークン認証を用いてユーザー情報を取得するアプリを作成します。

```
(venv) token_auth % python manage.py startapp account
```

カスタムユーザーを定義します。
```python:token_auth/account/models.py
from django.db import models
from django.contrib.auth.models import (BaseUserManager,
                                        AbstractBaseUser,
                                        PermissionsMixin)
from django.utils.translation import gettext_lazy as _

from django.conf import settings
from django.db.models.signals import post_save
from django.dispatch import receiver
from rest_framework.authtoken.models import Token


@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_auth_token(sender, instance=None, created=False, **kwargs):
    if created and instance is not None:
        Token.objects.create(user=instance)


class UserManager(BaseUserManager):
    def _create_user(self, email, username, password, **extra_fields):
        email = self.normalize_email(email)
        user = self.model(email=email, username=username, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        user.full_clean()

        Token.objects.create(user=user)

        return user

    def create_user(self, email, username, password=None, **extra_fields):
        extra_fields.setdefault('is_active', True)
        extra_fields.setdefault('is_staff', False)
        extra_fields.setdefault('is_superuser', False)
        return self._create_user(
            email=email,
            username=username,
            password=password,
            **extra_fields,
        )

    def create_superuser(self, email, username, password, **extra_fields):
        extra_fields['is_active'] = True
        extra_fields['is_staff'] = True
        extra_fields['is_superuser'] = True
        return self._create_user(
            email=email,
            username=username,
            password=password,
            **extra_fields,
        )


class User(AbstractBaseUser, PermissionsMixin):

    username = models.CharField(
        verbose_name=_("username"),
        unique=True,
        max_length=150
    )
    email = models.EmailField(
        verbose_name=_("Email Address"),
        unique=True
    )
    age = models.IntegerField(
        verbose_name=_("age"),
        null=True,
        blank=True
    )
    is_superuser = models.BooleanField(
        verbose_name=_("is_superuer"),
        default=False
    )
    is_staff = models.BooleanField(
        _('staff status'),
        default=False,
    )
    is_active = models.BooleanField(
        _('active'),
        default=True,
    )

    objects = UserManager()

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

    def __str__(self):
        return self.username
```

models.pyについて解説します。

```python
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_auth_token(sender, instance=None, created=False, **kwargs):
    if created and instance is not None:
        Token.objects.create(user=instance)
```
ユーザーを作成すると同時にトークン認証用のトークンを作成する処理です。

```python
class User(AbstractBaseUser, PermissionsMixin):

    username = models.CharField(
        verbose_name=_("username"),
        unique=True,
        max_length=150
    )
    email = models.EmailField(
        verbose_name=_("Email Address"),
        unique=True
    )
    age = models.IntegerField(
        verbose_name=_("age"),
        null=True,
        blank=True
    )
    is_superuser = models.BooleanField(
        verbose_name=_("is_superuer"),
        default=False
    )
    is_staff = models.BooleanField(
        _('staff status'),
        default=False,
    )
    is_active = models.BooleanField(
        _('active'),
        default=True,
    )

    objects = UserManager()

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

    def __str__(self):
        return self.username
```
カスタムユーザーを定義します。

```python
class UserManager(BaseUserManager):
    def _create_user(self, email, username, password, **extra_fields):
        email = self.normalize_email(email)
        user = self.model(email=email, username=username, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        user.full_clean()

        return user

    def create_user(self, email, username, password=None, **extra_fields):
        extra_fields.setdefault('is_active', True)
        extra_fields.setdefault('is_staff', False)
        extra_fields.setdefault('is_superuser', False)
        return self._create_user(
            email=email,
            username=username,
            password=password,
            **extra_fields,
        )

    def create_superuser(self, email, username, password, **extra_fields):
        extra_fields['is_active'] = True
        extra_fields['is_staff'] = True
        extra_fields['is_superuser'] = True
        return self._create_user(
            email=email,
            username=username,
            password=password,
            **extra_fields,
        )
```
カスタムユーザーを作成する際は、ユーザーマネージャーの方もカスタマイズする必要があります。

ここではユーザーやスーパーユーザーの作成の際に呼び出されるメソッドを定義します。

settings.pyを編集します。
```python:token_auth/project/settings.py
...

ALLOWED_HOSTS = ["*"] # ワイルドカードを追加

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework', # djangorestframeworkを追加
    'rest_framework.authtoken', # token認証用

    'account' # 作成したaccountアプリを追加
]

...

AUTH_USER_MODEL = "account.User" # accountアプリのUserモデルをデフォルトで使用する認証ユーザーモデルとして設定する
```

ここまでできましたら、マイグレートします。
```
(venv) token_auth % python manage.py makemigrations
(venv) token_auth % python manage.py migrate
```

ここでスーパーユーザーを作成し、管理画面にログインできるか確認します。任意のユーザー情報で作成してください。 

```
(venv) token_auth % python manage.py createsuperuser
Username (leave blank to use 'testuser'): testuser
Email address: test@example.com
Password: 
Password (again):
Superuser created successfully.
```

管理画面にログインできるか確認するため、開発サーバーを起動します。
```
(venv) token_auth % python manage.py runserver
```
ブラウザで`http://127.0.0.1:8000/admin/`にアクセスし、ログインフォームから作成したスーパーユーザー情報を用いてログインできればOKです。

ログイン画面
![Screen Shot 2022-12-28 at 5.15.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2951524/250bf04c-48be-78e3-3647-b73b989d7063.png)

ログイン後の画面
![Screen Shot 2023-01-12 at 20.40.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2951524/b0c5017d-0414-76f2-7241-1f9e3f2aa7ab.png)

ここで、Tokensを開き、先ほど作成したスーパーユーザーのトークンが作成されていることを確認できます。

![Screen Shot 2023-01-12 at 20.55.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2951524/182ed68e-09ac-99d4-1484-9f8eb5ba3045.png)

続いて、serializers.pyを作成します。
```python:token_auth/account/serializers.py
from rest_framework import serializers
from .models import User


class UserSerializer(serializers.ModelSerializer):

    class Meta:
        model = User
        fields = "__all__"
```

views.pyを編集します。

```python:token_auth/account/views.pyを
from rest_framework import viewsets
from rest_framework.authentication import TokenAuthentication
from rest_framework.permissions import IsAuthenticated

from .models import User
from .serializers import UserSerializer


class UserViewSet(viewsets.ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    authentication_classes = (TokenAuthentication,)
    permission_classes = (IsAuthenticated, )
```
UserViewSetのauthentication_classesにTokenAuthenticationを、

permission_classesにIsAuthenticatedを設定します。

これにより、このViewSetはトークン認証が必須となります。

accountアプリでurls.pyを作成し、以下の内容を書きます。

```python:token_auth/account/urls.py
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from .views import UserViewSet

router = DefaultRouter()
router.register(r'account', UserViewSet)

urlpatterns = [
    path('', include(router.urls))
]
```

project/urls.pyにaccountアプリのパスを追加します。

```python:token_auth/project/urls.py
from django.contrib import admin
from django.urls import path, include
from rest_framework.authtoken import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/v1/', include('account.urls')),
    path('api-token-auth/', views.obtain_auth_token),
]
```

api-token-auth/は、クライアントがトークンを取得するためのエンドポイントになります。

ここまでできましたら、トークン認証により、ユーザー情報を取得するAPIを実行します。

今回は、Postmanを利用して動作確認します。
Postmanをダウンロードしていない方は、以下のサイトよりダウンロードしてください。

https://www.postman.com/downloads/

Postmanにて、**作成したスーパーユーザーのトークンの取得**を試してみます。

![Screen Shot 2023-01-12 at 21.00.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2951524/7df9774d-64c4-5aaa-f097-212a5520e92f.png)

上図のように、

- POSTリクエスト
- `/api-token-auth`エンドポイント
- リクエストボディにユーザー名とパスワード

を設定します。

そしてSendボタンによりリクエストをすると、下図のようにトークンが取得できるはずです。

![Screen Shot 2023-01-12 at 21.05.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2951524/4ba08663-321f-caed-f396-b6e334b330fb.png)

続いて、受け取ったトークンを利用して、ユーザー一覧を取得するAPIを実行したいと思います。

まずは、トークン認証をせず、単に`/api/v1/account`にGETリクエストを実行します。
![Screen Shot 2023-01-12 at 21.06.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2951524/c214a1e0-be42-2dab-ac36-5d10c55ff46b.png)

すると、レスポンスは401 Unauthorizedで、

```json
{
    "detail": "Authentication credentials were not provided."
}
```
が返ってきました。

リクエストにトークンが設定されていないため、失敗しています。

続いて、リクエストヘッダーにトークンを設定します。![Screen Shot 2023-01-12 at 21.10.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2951524/e524c3b4-9924-c477-e433-dbd3e25b0b5b.png)
KEYに`Authorization`、VALUEに`Token <トークン>`を設定してください。

そしてSendボタンで実行すると、
![Screen Shot 2023-01-12 at 21.11.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2951524/dd7fd50f-df48-2a9e-a007-c2dfbec65f97.png)

トークン認証が通り、ユーザー情報が取得できるようになりました🎉

## Dockerによる起動

この環境をdockerで起動するようにします。
Dockerfileを作成し、以下を記載します。

```dockerfile:build/Dockerfile
FROM python:3.10-buster

ENV PYTHONUNBUFFERED 1

WORKDIR /app
ADD requirements.txt /app/
RUN pip install --upgrade pip \
  && pip install -r requirements.txt

ADD ./projects /app/
```

docker-compose.ymlファイルを作成し、以下を記載します。
```yaml:docker-compose.yml
version: '3'

services:
  token:
    container_name: token-auth-app
    restart: always
    build:
      context: .
      dockerfile: ./build/Dockerfile
    command: python3 token_auth/manage.py runserver 0.0.0.0:8000
    volumes:
      - ./projects:/app
    ports:
      - "8002:8000"
    tty: true
```
以下のコマンドを実行して、起動します。
```
% docker-compose up --build
```

Postmanで`http://127.0.0.1:8002/api/v1/account/`にリクエストし、レスポンスが返ってくればOKです🙆‍♂️

# 関連記事

https://qiita.com/ryo-keima/items/96096c87c9b5c2280a49