Djangoで独自のトークン認証を実装する

# はじめに

WebアプリをSPAで実装したときにサーバーとのデータのやり取りはWebAPIになり、JSONでのデータの受け渡しが主だと思います。
Djangoでサーバーを構築するとWebAPIもとても簡単に実装することができます。
Webアプリにログイン機能などをもたせたい場合、その管理方法に思慮することになりますが、DjangoのRestFrameworkを使用すれば、認証などはデフォルトで色々と用意されています。

今回実現したいのは以下のような機能です。

- メールアドレスとパスワードによるログイン
- ユーザ情報はDjangoのUserモデルを使用する
- 無操作によるタイムアウトをサーバー側で管理する
- 同一ユーザーの重複ログインは不可とする（後勝ち）

Webアプリではよくあるような仕様ですが、これをDjangoのWebAPIではどのように実装すればよいのかを考えました。
同一ユーザーの重複ログインは先勝ちが理想ですが、経験上それは難しいので後勝ちとします。
TokenAuthencationの拡張でも実装は可能だと思いますが、勉強も兼ねてAuthenticationを独自に実装することにしました。

Python、Djangoともに経験が１ヶ月程度ですので、問題などはご指摘いただけると非常に助かります。

# 検証環境
OS：Windows7 Professional
Pythonがすでにインストールされていて、パスが登録されていること。
Macの場合はコマンドなどを少々読み替えてください。ソースコードは同じで問題ありません。

```
$ python --version
Python 3.6.6
```

仮想環境管理にvevnを使用していますが、必須ではありません。使用しない場合はvenvの記載は無視して問題ありません。

# Djangoプロジェクトの作成と設定
### プロジェクトの作成
venvでプロジェクト管理したいので、プロジェクトフォルダを作成して、仮想環境を作成。

```
プロジェクトフォルダ作成
$ mkdir example_auth

プロジェクトフォルダに移動
$ cd example_auth

仮想環境作成（venvを使用する場合）
$ python -m venv venv

仮想環境を有効化（venvを使用する場合）
$ venv\Scripts\activate
```

仮想環境ができたところで、djangoとrest-frameworkをインストールします。

```
djangoとrest-frameworkをインストール
$ pip install django djangorestframework
（省略）

インストールされたパッケージを確認
$ pip freeze
Django==2.1.1
djangorestframework==3.8.2
pytz==2018.5
```

djangoのプロジェクトを作成する。

```
$ django-admin.py startproject example_auth .
```

最後にピリオドを入力することで、カレントフォルダにプロジェクトを作成します。
プロジェクトを作成すると、以下のようなフォルダ構成になります。

```
example_auth
├ manage.py
├ example_auth
│ ├ __init__.py
│ ├ settings.py
│ ├ urls.py
│ └ wsgi.py
└ venv
   └ （割愛）
```

プロジェクトの作成が完了しました。

### データベース作成

本記事ではデフォルトのSQLiteを使用します。

```
$ manage.py migrate
（省略）
```

カレントフォルダにdb.sqlite3というファイルが作成されます。

### スーパーユーザーの作成

以下のコマンドでスーパーユーザーを作成します。

```
$ manage.py createsuperuser
Username (leave blank to use 'admin'): admin
Email address: admin@example.com
Password: admin
Password (again): admin
（省略）
Superuser created successfully.
```

2回目のパスワードを入力したあとに「パスワードが簡単すぎる」と言われますが、無視して「y」と入力してください。
スーパーユーザーの設定値は任意ですが、上記の設定が行われた前提で、話を進めます。

### 言語とタイムゾーンの設定
example_authフォルダの中にあるsettings.pyの言語とタイムゾーンの設定を編集します。

```python:example_auth/settings.py
LANGUAGE_CODE = 'ja'                       # en-us → ja

TIME_ZONE = 'Asia/Tokyo'                   # UTC → Asia/Tokyo
```

### Webサーバーの起動

以下のコマンドでWebサーバーを起動します。

```
$ manage.py runserver
Performing system checks...

System check identified no issues (0 silenced).
September 05, 2018 - 12:59:10
Django version 2.1.1, using settings 'example_auth.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.
```

ブラウザから「[http://127.0.0.1:8000/](http://127.0.0.1:8000/)」にアクセスしてWebサーバーが起動していることを確認します。

|<img src="https://qiita-image-store.s3.amazonaws.com/0/269311/c07332d6-8d81-4b89-075d-2b2aa5d15ad5.png">|
|---|

また「[http://127.0.0.1/admin/](http://127.0.0.1/admin/)」にアクセスして、管理画面に先ほど作成したスーパーユーザーでログインできることを確認します。

|<img src="https://qiita-image-store.s3.amazonaws.com/0/269311/c3ff13ca-5515-2710-38c4-9c5fbf415359.png">|<img src="https://qiita-image-store.s3.amazonaws.com/0/269311/fccb89ed-1350-591f-8c91-99c31d6ab1c9.png">|
|---|---|

# WebAPI作成の準備

### アプリケーションの作成

今回は通常のWebサーバーが扱うHtmlではなく、WebAPIでJSONの送受信を行うアプリケーションを生成します。アプリケーション名は「api」とします。
以下のコマンドでアプリケーションを作成します。

```
$ manage.py startapp api
```

以下のフォルダとファイルが作成されます。

```
api
├ __init__.py
├ admin.py
├ apps.py
├ admin.py
├ models.py
├ tests.py
├ views.py
└ migrations
   └ __init__.py
```

作成したアプリケーションをsettings.pyに登録します。

```python:example_auth/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'api',                          # 追加
]
```

### モデルの作成

アプリケーションを作成する準備が整ったので、まずトークンを管理するクラスを作成します。トークンはユーザーごとに管理するため、このクラスはユーザーと紐付かせます。

```python:api/models.py
from django.db import models
from django.contrib.auth.models import User

class ExampleToken(models.Model):
    # 紐付くユーザー
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    # アクセストークン
    token = models.CharField(max_length=40)
    # アクセス日時
    access_datetime = models.DateTimeField()

    def __str__(self):
        # メールアドレスとアクセス日時、トークンが見えるようにする
        dt = timezone.localtime(self.access_datetime).strftime("%Y/%m/%d %H:%M:%S")
        return self.user.email + '(' + dt + ') - ' + self.token
```

クラスのメンバ変数は以下の用途で使用します。


user : トークンに紐づくユーザー。ユーザーとは1:1の関係とします。
token : アクセストークン。ログイン後はこのトークンを使用してユーザーの認証を行います。max_lengthが40に設定されている理由は、トークンはsha1でハッシュ化した文字列を設定するためです。
access_datetime : 最後にアクセスした日時を格納します。サーバーにアクセスするたびに更新します。　

次に作成したモデルをデータベースに反映します。

```
$ manage.py makemigrations
（省略）

$ manage.py migrate
（省略）
```

### モデルを管理画面に追加

作成したモデルを管理画面で参照できるようにadmin.pyも編集します。

```python:api/admin.py
from django.contrib import admin
from .models import ExampleToken

class ExampleTokenAdmin(admin.ModelAdmin):
    fields = ['user', 'token', 'access_datetime',]

admin.site.register(ExampleToken, ExampleTokenAdmin)
```

設定が出来たらブラウザから[設定画面](http://127.0.0.1:8000/admin/)にアクセスして、ExampleTokenモデルが追加されていることを確認します。データベースにはまだレコードが出来ていないので、中身は空っぽです。

|<img src="https://qiita-image-store.s3.amazonaws.com/0/269311/9d18da10-f975-deeb-5b33-4d05e5606b11.png">|<img src="https://qiita-image-store.s3.amazonaws.com/0/269311/e2caee26-ff01-1ece-963d-f9a5ab1e8d17.png">|
|---|---|

# ログイン機能の作成

データベースが完成したので、次にログイン機能を作成します。
ログイン機能の仕様としては、メールアドレスとパスワードがJSON形式でポストされ、一致するユーザーがいた場合はExampleTokenにレコードを追加してトークンを返却します。一致するユーザーがいない場合はエラーを返却します。

WebAPIでの実装となるため、rest-frameworkをsettings.pyに追加します。

```python:example_token.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',               # 追加
    'api',
]
```

### ログイン処理の作成

ログインしたらExampleTokenにレコードを追加しますので、まずその処理を作成します。
実装方法は色々あると思いますが、個人的な趣味でExampleTokenクラスの静的メソッドとして作成します。models.pyを以下のように編集してください。

```python:api/models.py
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone              # 追加

import hashlib                                 # 追加

class ExampleToken(models.Model):

    # このメソッドを追加
    @staticmethod
    def create(user: User):
        # ユーザの既存のトークンを取得
        if ExampleToken.objects.filter(user=user).exists():
            # トークンが既に存在している場合は削除する
            ExampleToken.objects.get(user=user).delete()
        
        # トークン生成（メールアドレス + パスワード + システム日付のハッシュ値とする）
        dt = timezone.now()
        str = user.email + user.password + dt.strftime('%Y%m%d%H%M%S%f')
        hash = hashlib.sha1(str.encode('utf-8')).hexdigest()    # utf-8でエンコードしないとエラーになる

        # トークンをデータベースに追加
        token = ExampleToken.objects.create(
            user = user,
            token = hash,
            access_datetime = dt)

        return token
```

importが追加になっているのと、createメソッドを新たに作成しました。
ユーザーを引数として呼び出し元から渡してもらい、そのユーザーに対するトークンを作成します。
トークンは使い捨てとして、ログイン時に新しく作成します。古いトークンがあった場合は削除します。
トークンは同一のものが存在すると困るので、一致しなそうな文字列をsha1でハッシュ化した文字列を使用しています。

作成したcreateメソッドを使用して、ログイン処理のWebAPIを作成します。views.pyを以下のように編集してください。

```python:api/views.py
from django.contrib.auth.models import User
from django.http.response import JsonResponse
from rest_framework.views import APIView
from .models import ExampleToken

import json

class Login(APIView):
    def post(self, request, format=None):
        # リクエストボディのJSONを読み込み、メールアドレス、パスワードを取得
        try:
            data = json.loads(request.body)
            email = data['email']
            password = data['password']
        except:
            # JSONの読み込みに失敗
            return JsonResponse({'message': 'Post data injustice'}, status=400)

        # メールアドレスからユーザを取得
        if not User.objects.filter(email=email).exists():
            # 存在しない場合は403を返却
            return JsonResponse({'message': 'Login failure.'}, status=403)

        user = User.objects.get(email=email)

        # パスワードチェック
        if not user.check_password(password):
            # チェックエラー
            return JsonResponse({'message': 'Login failure.'}, status=403)

        # ログインOKの場合は、トークンを生成
        token = ExampleToken.create(user)

        # トークンを返却
        return JsonResponse({'token': token.token})
```

作成したWebAPIをアクセス可能とするため、urls.pyを編集します。

```python:example_auth/urls.py
from django.contrib import admin
from django.urls import path, include         # includeを追加

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('api.urls')),        # 追加
]
```

apiフォルダにurls.pyを新たに作成し、以下の内容に編集してください。

```python:api/urls.py
from django.urls import path
from .views import Login

urlpatterns = [
    path('login', Login.as_view(), name='login'),
]
```

これで「[http://127.0.0.1:8000/api/login](http://127.0.0.1:8000/api/login)」にアクセスが可能になりました。
許容するメソッドはPOSTのみですので、普通にブラウザでアクセスしてもエラーとなります。
本記事でのWebAPIへのアクセスはcurlを使用します。（Windowsには標準で入っていないので、別途インストールが必要です。）

### ログインAPIの実行

以下のコマンドで作成したログインAPIを実行します。
リクエストメソッドはPOSTで、POSTデータとしてメールアドレスとパスワードを送信します。

```
$ curl -i -X POST -d "{\"email\":\"admin@example.com\",\"password\":\"admin\"}" http://127.0.0.1:8000/api/login
HTTP/1.1 200 OK
Date: Wed, 05 Sep 2018 0５:39:12 GMT
Server: WSGIServer/0.2 CPython/3.6.6
Content-Type: application/json
Vary: Accept, Cookie
Allow: POST, OPTIONS
X-Frame-Options: SAMEORIGIN
Content-Length: 53

{"token": "356dc257775c0fbf50bc0855c113de0065005f72"}
```

ステータスコードが200が返却され、トークンが取得できました。
この状態で[管理画面](http://127.0.0.1:8000/admin)でExampleTokenにレコードが追加されたことが確認できます。

|<img src="https://qiita-image-store.s3.amazonaws.com/0/269311/5259a838-a269-c460-1aa3-9d57fdedf8a6.png">|
|---|

もちろん、存在しないメールアドレスや、パスワードが間違っている場合は403エラーとなります。
今後はこのAPIで取得したトークンをサーバーに送信して認証を行います。

# 認証処理の作成

### 認証処理用のAPIを作成

認証処理を作成するために、まず簡単なAPIを作成します。
アクセスされたら「Yes」というメッセージを返すだけのイエスマンAPIです。以下のクラスをviews.pyに追加してください。

```python:api/views.py
class YesMan(APIView):                              # YesManクラスを追加
    def post(self, request, format=None):
        return JsonResponse({'message': 'Yes'})
```

このAPIにアクセスするためにurls.pyを編集します。

```python:api/urls.py
from django.urls import path
from .views import Login, YesMan           # YesManを追加

urlpatterns = [
    path('login', Login.as_view()),
    path('yesman', YesMan.as_view()),      # 追加
]
```

curlで追加したAPIにアクセスします。

```
$ curl -X POST http://127.0.0.1:8000/api/yesman
{"message": "Yes"}
```

当然ですが、このままでは認証はおろか、リクエストにトークンを含めなくても「Yes」と言われます。
では、認証されていないユーザには「Yes」と言われないようにします。
認証される条件は以下とします。

- リクエストに正しいトークンが含まれること
- トークンの最終アクセス日時から30分以内であること

上記のチェック処理をExampleTokenクラスに追加します。

```python:api/models.py
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
from datetime import timedelta

import hashlib

class ExampleToken(models.Model):
    # 既存のコードは変更なし。以下のコードを追加

    @staticmethod
    def get(token_str: str):
        # 引数のトークン文字列が存在するかチェック
        if ExampleToken.objects.filter(token=token_str).exists():
            # 存在した場合はトークンを返却
            return ExampleToken.objects.get(token=token_str)
        else:
            # 存在しない場合はNoneを返却
            return None

    def check_valid_token(self):
        # このトークンが有効かどうかをチェック
        delta = timedelta(minutes=30)   # 有効時間は30分
        if(delta < timezone.now() - self.access_datetime):
            # 最終アクセス時間から30分以上経過している場合はFalseを返却
            return False
        
        return True

    def update_access_datetime(self):
        # 最終アクセス日時を現在日時で更新
        self.access_datetime = timezone.now()
        self.save()
```

追加したメソッドの使い方は以下のようなイメージです。

1. クライアントからトークン文字列が送られてくるので、getメソッドを使用してトークン文字列からトークンクラスのインスタンスを取得。
1. トークンの有効期限は最終アクセス時間から30分なので、check_valid_tokenメソッドで有効かどうかをチェック。
1. トークンが有効だった場合は、update_access_datetimeメソッドを使用して、最終アクセス時間を現在日時で更新。

これらのメソッドを使用して、認証機能を作成します。

### 認証機能の作成

apiフォルダ内にauthentication.pyを新たに作成して、内容を以下のように編集します。

```python:api/authentication.py
from django.contrib.auth.models import User
from rest_framework import authentication
from rest_framework import exceptions
from rest_framework import status
from .models import ExampleToken

class ExampleAuthentication(authentication.BaseAuthentication):
    def authenticate(self, request):
        # リクエストヘッダからトークン文字列を取得
        token_str = request.META.get('HTTP_X_AUTH_TOKEN')
        if not token_str:
            # リクエストヘッダにトークンが含まれない場合はエラー
            raise exceptions.AuthenticationFailed({'message': 'token injustice.'})
        
        # トークン文字列からトークンを取得する
        token = ExampleToken.get(token_str)
        if token == None:
            # トークンが取得できない場合はエラー
            raise exceptions.AuthenticationFailed({'message': 'Token not found.'})
        
        # トークンが取得できた場合は、有効期間をチェック
        if not token.check_valid_token():
            # 有効期限切れの場合はエラー
            raise exceptions.AuthenticationFailed({'message': 'Token expired.'})
        
        # トークンが有効な場合は、アクセス日時を更新
        token.update_access_datetime()

        return (token.user, None)
```

リクエストヘッダからトークン文字列を取得し、そのトークン文字列の整合性をチェックします。チェックNGの場合は、例外をスローして認証失敗としています。
認証機能を作成しただけではAPIには適応されないため、先ほど作成したイエスマンに認証機能を付与します。

```python:api/views.py
from django.contrib.auth.models import User
from django.http.response import JsonResponse
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated       # 追加
from .models import ExampleToken
from .authentication import ExampleAuthentication            # 追加

import json

# Loginクラスは修正なし

class YesMan(APIView):
    authentication_classes = (ExampleAuthentication,)        # 追加
    permission_classes = (IsAuthenticated,)                  # 追加

    def post(self, request, format=None):
        return JsonResponse({'message': 'Yes'})
```

authentication_classesで使用する認証クラスを指定します。
permission_classesにはこのAPIを使用するための権限を設定します。IsAuthenticatedはauthentication_classesで設定した認証が行えた場合にこのAPIにアクセス可能となります。

この状態で、先ほどのcurlで送信したリクエストを、もう一度送信します。

```
$ curl -i -X POST http://127.0.0.1:8000/api/yesman
HTTP/1.1 403 Forbidden
Date: Wed, 05 Sep 2018 08:17:42 GMT
Server: WSGIServer/0.2 CPython/3.6.6
Content-Type: application/json
Vary: Accept
Allow: POST, OPTIONS
X-Frame-Options: SAMEORIGIN
Content-Length: 30

{"message":"token injustice."}
```

今度は認証機能が働いているため、トークン不正でステータスコード403が返却されました。
リクエストヘッダにトークンを含めて送信してみます。ここで送信するトークンは、ログインAPIで返却されたトークンです。

```
$ curl -i -X POST -H "X-AUTH-TOKEN:356dc257775c0fbf50bc0855c113de0065005f72" http://127.0.0.1:8000/api/yesman
HTTP/1.1 403 Forbidden
Date: Wed, 05 Sep 2018 08:19:52 GMT
Server: WSGIServer/0.2 CPython/3.6.6
Content-Type: application/json
Vary: Accept
Allow: POST, OPTIONS
X-Frame-Options: SAMEORIGIN
Content-Length: 28

{"message":"Token expired."}
```

先ほどログインした時間から30分以上経過している場合は、トークンの有効期限切れでエラーとなります。
こうなった場合は、再度ログインして新しいトークンを取得してからイエスマンを呼ぶと、正常に「Yes」と返事をしてくれます。

```
$ curl -X POST -d "{\"email\":\"admin@example.com\",\"password\":\"admin\"}" http://127.0.0.1:8000/api/login
{"token": "7348aa895d486360369652c1121538b900a819e3"}

C:\curl>curl -X POST -H "X-AUTH-TOKEN:7348aa895d486360369652c1121538b900a819e3" http://127.0.0.1:8000/api/yesman
{"message": "Yes"}
```

認証に成功したユーザー情報は、request.userで使用することが出来ます。
イエスマンAPIに認証成功したユーザーのメールアドレスもレスポンスに含めるには以下のようにします。

```python:api/views.py
class YesMan(APIView):
    authentication_classes = (ExampleAuthentication,)
    permission_classes = (IsAuthenticated,)

    def post(self, request, format=None):
        return JsonResponse({'message': 'Yes', 'email': request.user.email})     # 修正
```

request.userにはExampleAuthenticationクラスのauthenticationメソッドが返却したユーザー情報が格納されます。

```
$ curl -X POST -H "X-AUTH-TOKEN:7348aa895d486360369652c1121538b900a819e3" http://127.0.0.1:8000/api/yesman
{"message": "Yes", "email": "admin@example.com"}
```

### 認証処理をデフォルト設定にする

現在の状態は、認証を行いたいAPIに対して、authentication_classesとpermission_classesを登録していますが、大規模なアプリケーションになると、すべてのAPIに対して登録するのは冗長なので、デフォルトで認証が行われるようにすることも出来ます。
setting.pyに以下のコードを追加します。

```python:example_token/settings.py
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'api.authentication.ExampleAuthentication',
    ),
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    )
}
```

これでデフォルトで認証が行われるようになったので、API単位での認証設定を変更します。

```python:api/views.py
from django.contrib.auth.models import User
from django.http.response import JsonResponse
from rest_framework.views import APIView
# from rest_framework.permissions import IsAuthenticated     # 削除
from .models import ExampleToken
# from .authentication import ExampleAuthentication          # 削除

import json

class Login(APIView):
    authentication_classes = ()                              # 追加
    permission_classes = ()                                  # 追加

    # 省略

class YesMan(APIView):
    # authentication_classes = (ExampleAuthentication,)      # 削除
    # permission_classes = (IsAuthenticated,)                # 削除

    def post(self, request, format=None):
        return JsonResponse({'message': 'Yes', 'email': request.user.email})
```

YesManクラスからは認証設定を削除しています。
Loginクラスには空の認証情報を追加していますが、ログイン時には認証は不要なので、デフォルトの認証を打ち消しています。

以上でWebAPIを使用した独自のトークン認証の実装は終了です。
BaseAuthenticationクラスを継承したクラスの作り次第で、どんな認証も可能だと思います。

# WebAPIのテスト

以下のように、APITestCaseクラスを使用すると簡単にテストコードが作成できます。

```python:api/test.py
from django.contrib.auth.models import User
from django.urls import reverse
from django.utils import timezone
from django.test import TestCase
from rest_framework import status
from rest_framework.test import APITestCase
from .models import ExampleToken
from datetime import timedelta
from time import sleep

import json

def create_user():
    admin = User.objects.create_superuser('admin', 'admin@example.com', 'admin')
    user = User.objects.create_user('test', 'test@example.com', 'test')
    return (admin, user)
    
def create_token():
    admin = User.objects.get(username='admin')
    ExampleToken.objects.create(
        user = admin,
        token = '1111111111222222222233333333334444444444',
        access_datetime = timezone.now()
    )
    test = User.objects.get(username='test')
    ExampleToken.objects.create(
        user = test,
        token = 'aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd',
        access_datetime = timezone.now() - timedelta(minutes=29.9)
    )

def create_token_expired():
    admin = User.objects.get(username='admin')
    ExampleToken.objects.create(
        user = admin,
        token = '1111111111222222222233333333334444444444',
        access_datetime = timezone.now() - timedelta(minutes=30)
    )

class ExampleTokenTests(TestCase):
    def test_create_token(self):
        (admin, user) = create_user()

        # adminのトークンを作成
        ExampleToken.create(admin)

        # トークンが作成されたかを確認
        self.assertTrue(ExampleToken.objects.filter(user=admin).exists(), 'Token not created.')

        # トークンの長さが40文字であるかを確認
        token = ExampleToken.objects.get(user=admin)
        self.assertEqual(len(token.token), 40, 'Token length not 40.')

        # トークン再作成でトークンが変更するかを確認
        token_old = token.token
        sleep(0.1)                  # 同一時刻だとトークンも同じなので、一瞬待機
        token = ExampleToken.create(admin)
        self.assertNotEqual(token_old, token.token)

class LoginTests(APITestCase):

    def test_login_success(self):
        create_user()

        # スーパーユーザーのログイン確認
        response = self.client.post(reverse('login'), {'email':'admin@example.com','password':'admin'}, format='json')
        self.assertEqual(response.status_code, status.HTTP_200_OK, json.loads(response.content))

        # 一般ユーザーのログイン確認
        response = self.client.post(reverse('login'), {'email':'test@example.com','password':'test'}, format='json')
        self.assertEqual(response.status_code, status.HTTP_200_OK, json.loads(response.content))

    def test_login_failure(self):
        create_user()

        # POSTデータなし（400エラー）
        response = self.client.post(reverse('login'))
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, json.loads(response.content))

        # POSTデータにパスワードなし（400エラー）
        response = self.client.post(reverse('login'), {'email':'admin@example.com'}, format='json')
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, json.loads(response.content))

        # POSTデータにメールアドレス無し（400エラー）
        response = self.client.post(reverse('login'), {'password':'admin'}, format='json')
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, json.loads(response.content))

        # メールアドレス不正（403エラー）
        response = self.client.post(reverse('login'), {'email':'dammy@example.com','password':'admin'}, format='json')
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, json.loads(response.content))

        # パスワード不正（403エラー）
        response = self.client.post(reverse('login'), {'email':'admin@example.com','password':'dummy'}, format='json')
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, json.loads(response.content))
                
class YesManTests(APITestCase):
    def test_yesman_success(self):
        create_user()
        create_token()

        # トークン作成直後
        response = self.client.post(reverse('yesman'), HTTP_X_AUTH_TOKEN='1111111111222222222233333333334444444444')
        self.assertEqual(response.status_code, status.HTTP_200_OK, json.loads(response.content))

        # トークンの有効期限すれすれ
        response = self.client.post(reverse('yesman'), HTTP_X_AUTH_TOKEN='aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd')
        self.assertEqual(response.status_code, status.HTTP_200_OK, json.loads(response.content))

    def test_yesman_failure(self):
        create_user()
        create_token_expired()

        # トークンが含まれない（403エラー）
        response = self.client.post(reverse('yesman'))
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, json.loads(response.content))

        # トークンが間違っている（403エラー）
        response = self.client.post(reverse('yesman'), HTTP_X_AUTH_TOKEN='aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd')
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, json.loads(response.content))

        # トークンの有効期限が切れている（403エラー）
        response = self.client.post(reverse('yesman'), HTTP_X_AUTH_TOKEN='1111111111222222222233333333334444444444')
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, json.loads(response.content))

```

ExampleTokenのテストは途中で面倒になって手抜きです。