Django rest frameworkを使ったAPI経由のパスワードリセットを行う方法


Djangoでrest frameworkをつかってAPIを実装していて、ユーザのパスワードリセット機能を実装しようとした時の備忘録です
Djangoではdjango.contrib.authという強力なauth機能がありますが、今回はAPI実装なので簡単にはいきませんでした
API用のライブラリdjango-rest-authがあったので、こちらを使って進めていきます

パスワードリセットの流れ

ユーザ

  • トップページからパスワードリセット画面に遷移
  • メールアドレス入力・送信
  • URLを含んだメール受信
  • パスワード入力・送信

API

  • メールアドレスを含むリクエスト受信
  • リセット画面へのURLを含むメール送信
  • パスワードを含むリクエスト受信

の流れになります。よくある流れです
django.contrib.authをそのまま使うならテンプレートを設定するだけでほぼできあがるのですが、今回APIのエンドポイント作成になるので、ライブラリを利用していきます

django-rest-authとは

Documentationによると、ユーザ登録、ログインログアウト、パスワード変更やリセットまでを行ってくれるライブラリのようです
便利だけどドキュメントが絶妙に不親切だったので、ソースコード読みながら実装しました

ひとまず

pip install django-rest-auth

django-rest-authにおける実装

API

ライブラリにまかせておくなら、urls.pyに次の2行を追加すれば動きます

urls.py
url(r'^rest-auth/', include('rest_auth.urls')),
url(r'^', include('django.contrib.auth.urls')),

このままだとメールのテンプレートが設定できません
テンプレートの設定を行わないと、メールに記載されるURLのドメインの設定ができませんでした

設定するためにはdjango-rest-authのserializerをOverrideする必要があるようです
get_email_optionsemail_template_nameなどを指定します。他にも設定できる項目はあるようです

serializers.py
from rest_auth.serializers import PasswordResetSerializer

class CustomPasswordResetSerializer(PasswordResetSerializer):
    def get_email_options(self):
        data = {
            'email_template_name': 'email/password_reset.html',
            'subject_template_name': 'email/password_reset_subject.txt',
        }
        return data

Emailテンプレートはdjangoのデフォルトのテンプレートがあるので、これを参考にしてみてください
{{ protocol }}://{{ domain }}でフロントのURLに誘導してください

django/django/contrib/admin/templates/registration/password_reset_email.html
{% load i18n %}{% autoescape off %}
{% blocktrans %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktrans %}

{% trans "Please go to the following page and choose a new password:" %}
{% block reset_link %}
{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}
{% endblock %}
{% trans "Your username, in case you've forgotten:" %} {{ user.get_username }}

{% trans "Thanks for using our site!" %}

{% blocktrans %}The {{ site_name }} team{% endblocktrans %}

{% endautoescape %}

そしてviews.pyもこれに合わせて調整します
ここを参考にしました

views.py
from rest_framework.generics import GenericAPIView
from user.serializers import CustomPasswordResetSerializer

class PasswordResetView(GenericAPIView):
    permission_classes = (permissions.AllowAny,)
    serializer_class = CustomPasswordResetSerializer

    def post(self, request):
        serializer = self.get_serializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response('Password reset e-mail has been sent.', status=200)
        return Response(serializer.errors, status=400)

最後にurls.pyでこのviewのルーティングを行います(1行目)

urls.py
url(r'^/password/reset/$', PasswordResetView.as_view()),
url(r'^rest-auth/', include('rest_auth.urls')),
url(r'^', include('django.contrib.auth.urls')),

これで2つのAPIができました

  • /password/reset/: メールアドレス送信用
  • /rest-auth/password/reset/confirm/: パスワード送信用

パスワード送信用はdjango-rest-authにまかせて問題ないと思います

フロント

フロント側で準備するのは下の2つのようです(他に遷移元・先なども準備してください)
ビューで登録させる内容と、APIは次の通りです

メールアドレスを入力するビュー

メールアドレスを入力するフォームを作成します

/password/reset/

  • email

パスワードを入力するビュー

パスワードを入力するフォームを確認用も含めて2つ作成します

/rest-auth/password/reset/confirm/

  • new_password1
  • new_password2
  • uid
  • token

uidとtokenはメールに記載されたURLに含まれている文字列を指定します。

https://***/reset/{ uid }/{ token }/

まとめ

かなり強引な方法の気がします
もっと良い方法がないかなあと考えてはいますが