Django REST frameworkチュートリアル その４

# Tutorial 4: Authentication & Permissions

前回の記事「[Django REST frameworkチュートリアル その３](https://qiita.com/John_sugai/items/6cad505b92f69e415b2f)」の続きです。

今回は認証処理を作っていきます。[公式チュートリアル](https://www.django-rest-framework.org/tutorial/4-authentication-and-permissions/)の実装とは若干違ったトークン認証を実装していきたいと思います。


## django-oauth-toolkitのインストール

`django-oauth-toolkit`を使いますので、`django-oauth-toolkit`の[公式ドキュメント](https://django-oauth-toolkit.readthedocs.io/en/latest/rest-framework/getting_started.html)も適宜ご参照ください。

まず`pip`で仮想環境に`django-oauth-toolkit`をインストールしましょう。

``` bash
pip install django-oauth-toolkit
```


## settings.py

`settings.py`の`INSTALLED_APPS`に`oauth2_provider`を追加します。

``` tutorial/settings.py
INSTALLED_APPS = (
    'django.contrib.admin',
    ...
    'oauth2_provider',
    ...
)
```

さらに以下の設定をファイルの最後に追加します。

``` tutorial/settings.py
OAUTH2_PROVIDER = {
    # this is the list of available scopes
    'SCOPES': {'read': 'Read scope', 'write': 'Write scope'}
}

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
    ),    
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    )
}
```

`OAUTH2_PROVIDER.SCOPES`で認証の時にチェックするスコープを設定することができます。`read`、`write`を設定していますが、独自に追加することができます。
`REST_FRAMEWORK`にはデフォルトで使用する認証と認可のクラスを設定します。


## urls.py (Root)

次は、ルートの`urls.py`を編集して、認証のためのエンドポイントを作成しましょう。

``` tutorial/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('oauth2/', include('oauth2_provider.urls', namespace='oauth2_provider')),
    path('', include('snippets.urls')),
]
```

`oauth2/`のエンドポイントを追加しました。このエンドポイントから、後ほど認証のアプリ登録やトークン発行などを行います。


## models.py

次に、`models.py`を編集して、`Snippet`を`User`に紐付けます。ユーザーがある記事（スニペット）を投稿するようなイメージです。

```snippets/models.py
from django.db import models


class Snippet(models.Model):
    created = models.DateTimeField(auto_now_add=True)
    title = models.CharField(max_length=100, blank=True, default='')
    code = models.TextField()
    owner = models.ForeignKey('auth.User', on_delete=models.CASCADE)

    class Meta:
        ordering = ('created',)
```

外部キーの`owner`フィールドを追加しました。

モデルを変更したので、一旦前回までのDBの中身を消して、再度マイグレーションしましょう。

``` bash
rm -f db.sqlite3
rm -r snippets/migrations
python manage.py makemigrations snippets
python manage.py migrate
```

さらにテスト用に`superuser`を作っておいて、`admin/`から何人か適当なユーザーを作っておきます。

``` bash
python manage.py createsuperuser
# ユーザー名の入力
# パスワードの入力
# パスワード再確認
python manage.py runserver
# http://localhost:8000/にアクセスして、上で入力したユーザー名・パスワードでログイン.
# 適当にユーザーを作成する.
```

ここまで大丈夫でしょうか。編集するファイルが多くてDjangoは大変ですね。休憩を入れつつゆっくり進めていきましょう。


## serializers.py

次は、APIの方からもユーザーを作成したり、削除したりできるようにしたいので、その機能を作っていきます。
まず`seirializers.py`から追記していきましょう。

``` snippets/serializers.py
from rest_framework import serializers
from snippets.models import Snippet
from django.contrib.auth.models import User


class SnippetSerializer(serializers.ModelSerializer):
    owner = serializers.ReadOnlyField(source='owner.username')

    class Meta:
        model = Snippet
        fields = ('id', 'title', 'code', 'owner')


class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ('id', 'username', 'email')
```

`UserSerializer`クラスを追加しました。


## views.py

そうしましたら、次は`views.py`です。前回にならってクラスベースのビューを追加しましょう。

``` snippets/views.py
from rest_framework import generics
from rest_framework.response import Response
from snippets.models import Snippet
from django.contrib.auth.models import User
from snippets.serializers import SnippetSerializer, UserSerializer


class SnippetList(generics.ListCreateAPIView):
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer


class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer


class UserList(generics.ListCreateAPIView):
    permission_classes = [permissions.AllowAny]
    queryset = User.objects.all()
    serializer_class = UserSerializer


class UserDetails(generics.RetrieveUpdateDestroyAPIView):
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]
    queryset = User.objects.all()
    serializer_class = UserSerializer
```

`UserList`と`UserDetail`クラスを追加しました。
それとすべてのクラスに`permission_classes`という属性を追加しました。
ここで使用した`permissions`のクラスを軽く説明すると、

* `permissions.AllowAny`はその名の通り、トークンを持っていなくても誰でも認証が通ります。
* `permissions.IsAuthenticatedOrReadOnly`は`GET`メソッドに対しては誰でも認可します。`POST`、`PUT`、`PATCH`、`DELETE`などの安全でないメソッドに対しては認証を要求します。

Permissionのクラスについては他にもたくさんあるので、こちらの[公式ドキュメント](https://www.django-rest-framework.org/api-guide/permissions/)をご参照ください。


## urls.py (Project)

次は`urls.py`です。ビューをエンドポイントに紐付けます。

``` snippets/urls.py
from django.urls import path
from rest_framework.urlpatterns import format_suffix_patterns
from snippets import views

urlpatterns = [
    path('snippets/', views.SnippetList.as_view()),
    path('snippets/<int:pk>/', views.SnippetDetail.as_view()),
    path('users/', views.UserList.as_view()),
    path('users/<int:pk>/', views.UserDetails.as_view()),
]

urlpatterns = format_suffix_patterns(urlpatterns)
```

`users/`と`users/<pk>/`のエンドポイントを追加です。


## 完成

これで認証処理が一通り完成しました。
期待する動作としては、以下です。

* ユーザーの新規作成・一覧取得は誰でもアクセス可能。
* 特定ユーザー情報の取得は誰でもアクセス可能だが、変更・削除については認証が必要。
* スニペットの一覧取得は誰でも可能だが、新規作成には認証が必要。
* 特定スニペット情報の取得は誰でもアクセス可能だが、変更・削除については認証が必要。


## テスト


### 認証するアプリの登録

次は、簡単なテストをしてみましょう。
まずローカルサーバーを起動します。

``` bash
python manage.py runserver
```

一回`http://localhost:8000/admin/`から先ほど作った管理者権限のアカウントでログインしてください。（これをしないと次のアプリ登録の画面に行けなかったです。）

次に`http://localhost:8000/oauth2/applications/`にアクセスしてみてください。

<img width="574" alt="スクリーンショット 2019-05-25 17.39.03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259118/dc15b851-7834-ba1f-29e7-5531fe125ecc.png">

こんな画面が出たかと思います。（hogeは私が登録したアプリなので、最初にアクセスした時はhogeはないです。）
New Applicationをクリックして認証トークンを発行するアプリを登録します。（ここではこれまで作ってきたAPIアプリを登録します。）
それぞれの項目は以下のように設定します。

* Name: 適当な名前
* Client Type: confidential
* Authorization Grant Type: Resource owner password-based

すると、Client idとClient secretが取得できます。これらは、この後使います。


### トークン発行

これでトークンを発行する準備が整いました。
早速トークンを発行してみましょう。以下のリクエストを送ります。

``` bash
curl -X POST -d "grant_type=password&username=<user_name>&password=<password>" -u"<client_id>:<client_secret>" http://localhost:8000/oauth2/token/
```

`<username>`のところに登録したユーザー名を、`<password>`のところにそのユーザーのパスワードを入れます。`<client_id>`と`<client_secret>`には先ほど取得したものを入れます。

``` json
{
    "access_token": "<your_access_token>",
    "token_type": "Bearer",
    "expires_in": 36000,
    "refresh_token": "<your_refresh_token>",
    "scope": "read write"
}
```

こんなレスポンスが返ってきましたでしょうか。
いい感じですね。この`access token`を使ってユーザーを認証します。（その他のパラメータについてはここでは説明しません。ごめんなさい。。）


### リクエスト

今度はヘッダーに`access token`をつけてリクエストを送ってみましょう。

``` bash
curl -H "Authorization: Bearer <your_access_token>" http://localhost:8000/users/
```

ユーザー一覧のレスポンスが返ってきたでしょうか。
ただこのエンドポイントは`permission_classes`に`permissions.AllowAny`を指定しましたので、ヘッダーに`access token`をつけなくても同じ結果が返ってきます。

こっちのリクエストはどうでしょうか。

``` bash
curl -X PATCH -H 'Content-Type:application/json' -H "Authorization: Bearer <your_access_token>" -d '{"code":"This is modified."}' http://localhost:8000/snippets/1/
```

こちらは`access token`なしだとはじかれてしまいますね。
認証処理が正しく動作しているのが確認できました。
余裕があれば他のエンドポイントの他のメソッドについても同様に試してみてください。


# TODO  

ここまでで認証処理が実装できました。
が、認証周りでもう少し改善しなければいけないところは次のようなところです。

* トークンを持っていれば他の人のユーザー情報やスニペットの中身を書き換えることができてしまいます。変更や削除などのリクエストを送った人がそのデータのオーナーなのかをチェックしたいです。
* スニペットを登録するときに自動でリクエストを送った人が`owner`となるように登録したいです。

ここら辺の話はまた追って別記事で書きたいと思います。
ここまでありがとうございました。