akiyoko blog

akiyoko の IT技術系ブログです

Django 組み込みのパスワード再設定(パスワードリセット)の仕組み

この投稿は 「Calendar for Django | Advent Calendar 2021 - Qiita」 1日目の記事です。


akiyoko です。
この記事では、Django 組み込みで提供されているパスワード再設定(パスワードリセット)の仕組みを深掘りします。 Django のパスワード再設定機能に関しては、利用方法についてはさまざまな記事で紹介されていたりするのですが、その内部仕様について詳しく紹介した記事をあまり見かけたことがなかったので、自分のメモがてら残しておこうと思い、ほぼ一年ぶりに筆を執りました。何かのお役に立てば幸いです。




検証環境

  • Django 3.2


 

はじめに

Django は Webアプリケーションを作成するために必要な機能が何でも揃っているフルスタックフレームワークで、ユーザー認証まわりの機能(ユーザーモデル、パーミッション、ユーザーセッションなど)などの便利な機能がデフォルトで用意されています。そしてあまりよく知られていませんが、パスワード再設定(パスワードリセット)機能(で利用できるビューやフォーム)もユーザー認証まわりの機能の一部として「django.contrib.auth(認証システム)」パッケージに含まれています。

なお、「django.contrib.admin(管理サイト)」パッケージにはパスワード再設定系のテンプレートが用意されているので、管理サイト内でパスワード再設定機能を利用するのは非常に簡単です。


 

管理サイトでのパスワード再設定機能の利用方法

管理サイトではパスワード再設定系の機能はデフォルトでオフになっているため、利用するには次のように URLconf にパスワード再設定用の4つの URLパターンを追加しなければいけません(詳しくは公式ドキュメント *1 を参照)。

config/urls.py(URLconf)

from django.contrib import admin
from django.contrib.auth import views as auth_views
from django.urls import path

urlpatterns = [
    path('admin/password_reset/', auth_views.PasswordResetView.as_view(), name='admin_password_reset'),
    path('admin/password_reset/done/', auth_views.PasswordResetDoneView.as_view(), name='password_reset_done'),
    path('reset/<uidb64>/<token>/', auth_views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
    path('reset/done/', auth_views.PasswordResetCompleteView.as_view(), name='password_reset_complete'),
    path('admin/', admin.site.urls),
]


上で示した「admin_password_reset」というURLパターンが登録されていれば、管理サイトのログイン画面に、次のように「パスワードまたはユーザー名を忘れましたか?」というリンクが表示されるようになります。


f:id:akiyoko:20211127070253p:plain:w350


この追加設定により、次のような画面遷移ができます。

f:id:akiyoko:20211128090512p:plain


それぞれの挙動は参考情報に示したビューやフォームを読めば分かるのですが、ちょっと理解しずらいのが「パスワード再設定メール」の仕組みです。


パスワード再設定メールの仕組み


f:id:akiyoko:20211128100012p:plain:w450

パスワード再設定メール送信画面で「パスワードをリセット」ボタンを押下すると、django.contrib.auth.views.PasswordResetView がリクエストを受け取り、入力したメールアドレスに紐付くアクティブ(is_active が True)なユーザーが存在する場合にのみパスワード再設定用のメールが送信されます。

例えばホストが「127.0.0.1:8000」の場合のパスワード再設定メールは次のようになります。

Subject: 127.0.0.1:8000 のパスワードリセット
From: webmaster@localhost
To: admin@example.com
Date: Fri, 26 Nov 2021 21:47:26 -0000
Message-ID: 
 <163796324660.443.3589748126282107509@1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa>
このメールは 127.0.0.1:8000 で、あなたのアカウントのパスワードリセットが要求されたため、送信されました。
次のページで新しいパスワードを選んでください:
http://127.0.0.1:8000/reset/MQ/awrev2-3a07a0471392cfbc3b885b713b2846d5/
あなたのユーザー名 (もし忘れていたら): admin
ご利用ありがとうございました!
 127.0.0.1:8000 チーム

メール本文にはパスワード再設定画面に遷移するためのリンクが含まれており、リンクの有効期限はデフォルトでは3日間となっています。*2


このリンクの URL は、「password_reset_confirm」という URL パターンで URLconf に登録されている「reset/<uidb64>/<token>/」に相当します。<uidb64>の部分、すなわち上のメールの例の「MQ」は、Base64 でエンコードされたユーザーの PK で、Base64 でデコードすると「1」になります。

<token>の「-」の左側の部分(上の例では「awrev2」)は、メール送信時のタイムスタンプ(2001/1/1 00:00:00 からの経過秒数)を Base34 でエンコードしたもので、リンクの有効期限をチェックするために使われます。残りの右側の部分(上の例では「3a07a0471392cfbc3b885b713b2846d5」)は改ざん防止のためのハッシュです。*3


django.contrib.auth.views.PasswordResetConfirmView では、パスワード再設定URL のリクエストを受け取ると、URL の妥当性(ユーザーが存在するかどうか、URL が改ざんされていないかどうか、有効期限を過ぎていないか)が検証され、「/reset/<uidb64>/set-password/」にリダイレクトされてパスワード再設定画面が表示されます。


f:id:akiyoko:20211128111830p:plain:w450

ちなみに、URL の<token>は(ハッシュ化された)パスワード文字列などから生成されているため(下記参照)、パスワード再設定によってパスワードが変更された後は同じ URL は利用できなくなります。

django.contrib.auth.tokens.PasswordResetTokenGenerator._make_hash_value

    def _make_hash_value(self, user, timestamp):
        """
        Hash the user's primary key, email (if available), and some user state
        that's sure to change after a password reset to produce a token that is
        invalidated when it's used:
        1. The password field will change upon a password reset (even if the
           same password is chosen, due to password salting).
        2. The last_login field will usually be updated very shortly after
           a password reset.
        Failing those things, settings.PASSWORD_RESET_TIMEOUT eventually
        invalidates the token.

        Running this data through salted_hmac() prevents password cracking
        attempts using the reset token, provided the secret isn't compromised.
        """
        # Truncate microseconds so that tokens are consistent even if the
        # database doesn't support microseconds.
        login_timestamp = '' if user.last_login is None else user.last_login.replace(microsecond=0, tzinfo=None)
        email_field = user.get_email_field_name()
        email = getattr(user, email_field, '') or ''
        return f'{user.pk}{user.password}{login_timestamp}{timestamp}{email}'


f:id:akiyoko:20211128111923p:plain:w450


 

まとめ

Django はパスワード再設定(パスワードリセット)機能を組み込みで提供しており、管理サイト内でパスワード再設定機能を利用するのは非常に簡単です。管理サイト外でパスワード再設定を利用する場合は、「django.contrib.auth(認証システム)」パッケージのビューやフォームを使用し、「django.contrib.admin(管理サイト)」パッケージのテンプレートを参考にすればよいでしょう。

パスワード再設定系のビューやフォーム、テンプレートについては次の参考情報をご確認ください。


 

参考情報

パスワード再設定系のビュー・フォーム

  • django.contrib.auth.views.PasswordResetView
  • django.contrib.auth.views.PasswordResetDoneView
  • django.contrib.auth.views.PasswordResetConfirmView
  • django.contrib.auth.views.PasswordResetCompleteView
  • django.contrib.auth.forms.PasswordResetForm
  • django.contrib.auth.forms.SetPasswordForm

パスワード再設定系のテンプレート

  • django/contrib/admin/templates/registration/password_reset_form.html
  • django/contrib/admin/templates/registration/password_reset_done.html
  • django/contrib/admin/templates/registration/password_reset_confirm.html
  • django/contrib/admin/templates/registration/password_reset_complete.html

(メールタイトル・本文のテンプレート)

  • django/contrib/auth/templates/registration/password_reset_subject.txt
  • django/contrib/admin/templates/registration/password_reset_email.html


 

宣伝

Django の技術同人誌をこれまでに4冊出しました。開発のお供にどうぞ。



現場で使える Django の教科書《基礎編》

「現場で使える Django の教科書」シリーズ第1弾となる Django の技術同人誌。Django を現場で使うための基礎知識やベストプラクティスについて、初心者・初級者向けに解説した本です。B5・本文192ページ。最新の Django 3.2 LTS に対応。


★ Amazon(電子版/ペーパーバック)


★ BOOTH(紙の本)



現場で使える Django の教科書《実践編》

《基礎編》の続編にあたる「現場で使える Django の教科書」シリーズの第2弾。認証まわり、ファイルアップロード、ユニットテスト、デプロイ、セキュリティ、高速化など、さらに実践的な内容に踏み込んでいます。現場で Django を本格的に活用したい、あるいはすでに活用している方にピッタリの一冊。B5・本文180ページ。


★ Amazon(電子版/ペーパーバック)


★ BOOTH(紙の本)



現場で使える Django REST Framework の教科書

Django で REST API を構築する際の鉄板ライブラリである「Django REST Framework」(通称「DRF」)にフォーカスした、「現場で使える Django の教科書」シリーズの第3弾。B5・本文220ページ。


★ Amazon(電子版/ペーパーバック)


★ BOOTH(紙の本)



現場で使える Django 管理サイトのつくり方

Django の管理サイト(Django Admin)だけに特化した、ニッチでオンリーワンな一冊。管理サイトをカスタマイズする前に絶対に読んでほしい本です。B5・本文152ページ。


★ Amazon(電子版)


★ BOOTH(紙の本)

*1:https://docs.djangoproject.com/ja/3.2/ref/contrib/admin/#adding-a-password-reset-feature

*2:有効期限をデフォルトの3日間から変更したい場合は、「PASSWORD_RESET_TIMEOUT」を設定ファイルに定義することで変更可能です。なお、Django 2.2 以前で利用されていた「PASSWORD_RESET_TIMEOUT_DAYS」は非推奨になっており、Django 4.0 で削除される予定です。https://docs.djangoproject.com/en/3.2/internals/deprecation/#deprecation-removed-in-4-0

*3:ハッシュ化アルゴリズムには Django 3.1 以降でデフォルトになった「sha256」が使われています。