【8日目】django-allauth 完全入門――メール認証とワンタイムコードログイン

【8日目】django-allauth 完全入門――メール認証とワンタイムコードログイン
目次

連載「10日で作る Django CMS」の8日目です。 成果物: https://github.com/kurumonn/DjangoCMS(タグ day-08


1. 今日の結論


自作の認証を捨てて、django-allauth に置き換えます。

  • メールアドレスでのログイン
  • メール確認(必須)
  • パスワード再設定
  • メールワンタイムコードでのログイン
  • Google・GitHub でのログイン
  • ログイン中の端末一覧
  • レート制限とアカウント列挙対策

今日いちばん大事なのは、ワンタイムコードの強度が「桁数」ではなく「桁数 × 有効期限 × 試行回数 × 発行制限」で決まることです。


2. 今日の完成画面

ログイン画面です。メール・ワンタイムコード・ソーシャルがそろっています。

ログイン画面

ワンタイムコードの要求画面です。

ワンタイムコードの要求

ユーザー登録画面です。

ユーザー登録

2. 今日の完成画面

ログイン方法は次の4つになります。

ログイン方法
├── メールアドレス + パスワード
├── メールワンタイムコード
├── Google
└── GitHub

メールワンタイムコードでは、次の流れになります。

メールアドレスを入力
        ↓
ログインコードを発行
        ↓
メールへコードを送信
        ↓
利用者がコードを入力
        ↓
有効期限・試行回数を確認
        ↓
一致
        ↓
ログイン成立

また、ログイン後には allauth.usersessions を使ってログイン中の端末を確認できるようにします。


3. 今日変更するファイル

config/
├── settings.py                    変更
└── urls.py                        変更

accounts/
└── tests.py                       変更

seo/
└── models.py                      変更(Site情報を同期)

templates/
├── allauth/
│   └── layouts/
│       └── base.html              新規
├── partials/
│   └── account_nav.html           新規
└── base.html                      変更

static/css/site.css                変更
requirements.txt                   変更

day-08タグの実装と今回の修正版の違い

day-08 タグを確認すると、記事と実コードにはいくつか差があります。

特に重要なのは次の点です。

  1. 記事では django.contrib.humanize を追加していますが、day-08config/settings.py には入っていません。
  2. ACCOUNT_LOGIN_BY_CODE_MAX_RESEND_COUNT は現在のallauth公式設定として確認できません。
  3. ACCOUNT_RATE_LIMITS に、公式ドキュメントで定義されていないアクション名が含まれています。
  4. 独自設定の TRUSTED_PROXY_COUNT は、allauthのクライアントIP判定には使われません。
  5. SOCIALACCOUNT_AUTO_SIGNUP の説明が、既存アカウントへのメール一致ログイン設定と混同されています。

この修正版では、公式ドキュメントで確認できる設定へ揃えます。


4. 完成コード

4.1 django-allauthをインストールする

ソーシャルログインと、9日目で使うMFA機能の依存関係も含めて導入します。

pip install "django-allauth[mfa,socialaccount]==65.18.0"

目的

django-allauth と、ソーシャルログイン・MFAで必要になる依存パッケージをインストールします。

実行場所

manage.py があるプロジェクトルートです。

正常例

Successfully installed django-allauth-65.18.0 ...

異常例

ERROR: Could not find a version that satisfies the requirement ...

判断方法

python -c "import allauth; print(allauth.__version__)"

でインストール済みバージョンを確認します。

65.19.0について

2026年8月6日に django-allauth 65.19.0 が公開されています。

65.19.0では、

IDP_OIDC_ID_TOKEN_EXPIRES_IN

を設定しても既定値が返る不具合が修正されています。

この設定は、django-allauthをOpenID Connectの**Identity Provider(トークン発行側)**として使う allauth.idp.oidc の設定です。

今回のCMSは、

Google / GitHubから認証結果を受け取る側

であり、

"allauth.idp.oidc"

INSTALLED_APPS に入れていません。

そのため、この不具合の直接の対象ではありません。

教材の day-08 を再現するときは 65.18.0 を固定して構いません。

ただし、新規プロジェクトでは、変更点を確認してテストしたうえで最新安定版を使う方が適切です。


4.2 INSTALLED_APPSとMIDDLEWARE

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "django.contrib.sitemaps",

    # usersessionsのテンプレートで使う。
    "django.contrib.humanize",

    # このCMSではSite情報をメールのサイト名・ドメインと同期する。
    #
    # settings.pyのSOCIALACCOUNT_PROVIDERS["APP"]方式だけなら、
    # django.contrib.sitesはソーシャルログインそのものの必須条件ではない。
    "django.contrib.sites",

    "allauth",
    "allauth.account",
    "allauth.socialaccount",
    "allauth.socialaccount.providers.google",
    "allauth.socialaccount.providers.github",

    # ログイン中の端末一覧。
    "allauth.usersessions",

    "core",
    "accounts",
    "blog",
    "pages",
    "media_library",
    "comments",
    "seo",
    "dashboard",
]

allauth.usersessions を使う場合、公式ドキュメントでは django.contrib.humanize も追加します。

USERSESSIONS_TRACK_ACTIVITY=True にする場合は、次のミドルウェアも追加します。

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",

    "allauth.account.middleware.AccountMiddleware",
    "allauth.usersessions.middleware.UserSessionsMiddleware",
]

バックエンドは次のようにします。

AUTHENTICATION_BACKENDS = [
    "django.contrib.auth.backends.ModelBackend",
    "allauth.account.auth_backends.AuthenticationBackend",
]

ModelBackend はDjango管理画面などの標準認証を維持するために残します。

SITE_ID = 1

このCMSでは SiteSettingdjango.contrib.sitesSite を同期して、確認メールのサイト名・ドメインにも同じ値を使います。


4.3 アカウント設定

# メールアドレスでログインする。
ACCOUNT_LOGIN_METHODS = {"email"}

ACCOUNT_SIGNUP_FIELDS = [
    "email*",
    "password1*",
    "password2*",
]

ACCOUNT_UNIQUE_EMAIL = True

# メール確認を必須化する。
ACCOUNT_EMAIL_VERIFICATION = "mandatory"

# メール確認にもコード方式を利用する。
ACCOUNT_EMAIL_VERIFICATION_BY_CODE_ENABLED = True
ACCOUNT_EMAIL_VERIFICATION_BY_CODE_MAX_ATTEMPTS = 3

# GETだけでメール確認を完了させない。
# メールセキュリティ製品のリンク先読みで確認済みになるのを避ける。
ACCOUNT_CONFIRM_EMAIL_ON_GET = False

# アカウント列挙対策。
ACCOUNT_PREVENT_ENUMERATION = True

# 強い列挙耐性を優先する場合はTrueのままにする。
# 未登録アドレスにも「アカウントが存在しない」旨のメールが送られる場合がある。
ACCOUNT_EMAIL_UNKNOWN_ACCOUNTS = True

# ログアウトはPOSTで実行する。
ACCOUNT_LOGOUT_ON_GET = False

# 重要操作前の再認証。
ACCOUNT_REAUTHENTICATION_REQUIRED = True
ACCOUNT_REAUTHENTICATION_TIMEOUT = 300

# パスワード変更後の「現在のセッション」の扱い。
#
# False:
#   Djangoのセッション認証ハッシュを更新し、現在のセッションを維持する。
#   古い認証ハッシュを持つ他セッションは無効化される。
#
# True:
#   現在のセッションもログアウトさせる。
#
# このCMSでは、利用者自身の現在セッションは維持する。
ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE = False

ACCOUNT_EMAIL_UNKNOWN_ACCOUNTSの注意

ACCOUNT_PREVENT_ENUMERATION=True でも、

ACCOUNT_EMAIL_UNKNOWN_ACCOUNTS = False

にすると、画面の応答は同じでも、

メールが届く / 届かない

という差が残ります。

つまり、メールボックスを確認できる本人から見ると「このサービスに登録されているか」を推測できる余地があります。

どちらを選ぶかは、

完全な列挙耐性
vs
未登録アドレスへのメール送信を避けたい

というトレードオフです。

この連載では列挙耐性を優先して True にします。


4.4 メールワンタイムコード

ACCOUNT_LOGIN_BY_CODE_ENABLED = True

# 180秒で失効。
ACCOUNT_LOGIN_BY_CODE_TIMEOUT = 180

# 1つのコードに対して3回まで。
ACCOUNT_LOGIN_BY_CODE_MAX_ATTEMPTS = 3

# allauth 65.15以降で追加された正式な設定。
# 同じ認証フロー内で新しいコードを要求できる機能は無効のままにする。
ACCOUNT_LOGIN_BY_CODE_SUPPORTS_RESEND = False

# 6桁数字にする。
ACCOUNT_LOGIN_BY_CODE_FORMAT = {
    "numeric": True,
    "length": 6,
    "dashed": False,
}

ACCOUNT_EMAIL_VERIFICATION_BY_CODE_FORMAT = {
    "numeric": True,
    "length": 6,
    "dashed": False,
}

削除する設定

次は使いません。

ACCOUNT_LOGIN_BY_CODE_MAX_RESEND_COUNT = 3

現在のallauth公式設定として確認できないためです。

再送機能を有効化する正式な設定は、

ACCOUNT_LOGIN_BY_CODE_SUPPORTS_RESEND

です。

なお、SUPPORTS_RESEND=False は「認証フロー内で再送ボタンを使わせない」設定です。

攻撃者が新しいログイン要求を何度も開始する攻撃まで、これだけで防げるわけではありません。

そのため、ログイン経路のレート制限も必須です。


4.5 レート制限

ACCOUNT_RATE_LIMITS には、allauthが公式に定義しているアクション名だけを使います。

ACCOUNT_RATE_LIMITS = {
    # ログイン試行。
    "login": "5/5m/ip",

    # 失敗ログイン。
    # IPとログイン対象の両方で数える。
    "login_failed": "5/5m/ip,3/5m/key",

    "signup": "5/h/ip",

    # メールアドレス管理。
    "manage_email": "10/m/user",

    # パスワード再設定要求。
    "reset_password": "5/h/ip,3/h/key",

    # 再設定リンクを開いた後の操作。
    "reset_password_from_key": "10/m/ip",

    # メール確認。
    "confirm_email": "5/m/key",

    # 重要操作前の再認証。
    "reauthenticate": "5/5m/user",

    # ログイン後のパスワード変更。
    "change_password": "3/5m/user",
}

次のようなキーは、現在のallauth公式 ACCOUNT_RATE_LIMITS のアクション一覧では確認できません。

"send_email"
"change_email"
"request_login_code"

設定辞書へ存在するだけでは、そのレート制限が実際に消費されている証明にはなりません。

ログインコード要求をさらに厳密に制限したい場合は、

  • allauthのログイン系レート制限
  • Nginx / Cloudflareなどのエッジ側レート制限
  • 必要なら独自アダプター・ミドルウェア

を組み合わせます。

スコープを必ず書く

新しいallauthでは、レート制限のスコープを明示します。

/ip    IP単位
/user  ログイン済みユーザー単位
/key   操作固有のキー単位

したがって、

"login": "5/5m"

ではなく、

"login": "5/5m/ip"

のようにします。


4.6 リバースプロキシ環境でクライアントIPを正しく扱う

allauthのレート制限は、クライアントIPの判定が正しくなければ簡単に意味を失います。

allauth 65.14.2以降は、X-Forwarded-For を既定では信用しません。

このCMS独自の、

TRUSTED_PROXY_COUNT

という設定を作っていても、allauthが自動で読むわけではありません。

allauth用に別途設定します。

ALLAUTH_TRUSTED_PROXY_COUNT = int(
    os.environ.get("DJANGO_ALLAUTH_TRUSTED_PROXY_COUNT", "0")
)

また、信頼できるプロキシが専用ヘッダーへクライアントIPを設定する構成なら、

ALLAUTH_TRUSTED_CLIENT_IP_HEADER = "CF-Connecting-IP"

のような方式もあります。

ただし、この設定を使うのは、

  • オリジンサーバーがそのプロキシ経由の通信だけを受け付ける
  • 外部利用者が同じヘッダーを自由に偽装できない
  • プロキシがヘッダーを正しく上書きする

ことを確認した場合だけです。

Redisを使えば完全になるわけではない

allauthのレート制限はDjangoキャッシュを使います。

複数Gunicornワーカーで LocMemCache を使うとカウンターがプロセスごとに分かれるため、本番ではRedisなどの共有キャッシュが適しています。

ただしallauth公式ドキュメントは、組み込みレートリミッターが非原子的な処理を含むため、並行アクセス時に少数の超過が起きる可能性も説明しています。

したがって、

Redisにしたから絶対に5回を超えない

とは考えません。

認証の高リスク経路では、WAF・リバースプロキシのレート制限も併用します。


4.7 ソーシャルログイン

認証情報はソースコードへ直接書きません。

SOCIALACCOUNT_PROVIDERS = {
    "google": {
        "APP": {
            "client_id": os.environ.get("GOOGLE_CLIENT_ID", ""),
            "secret": os.environ.get("GOOGLE_CLIENT_SECRET", ""),
            "key": "",
        },
        "SCOPE": ["profile", "email"],
    },
    "github": {
        "APP": {
            "client_id": os.environ.get("GITHUB_CLIENT_ID", ""),
            "secret": os.environ.get("GITHUB_CLIENT_SECRET", ""),
            "key": "",
        },
        "SCOPE": ["user:email"],
    },
}

新規ソーシャル登録を自動完了させない

SOCIALACCOUNT_AUTO_SIGNUP = False

この設定の意味は、

ソーシャルプロバイダーから取得した情報だけで、新規登録フォームを自動的に省略するか

です。

既存ローカルアカウントへ同じメールアドレスだけでログインさせる設定ではありません。

既存アカウントの保護に関係するのはこちらです。

# プロバイダーが「確認済み」と主張する同一メールアドレスだけを根拠に、
# 既存ローカルアカウントへログインさせない。
SOCIALACCOUNT_EMAIL_AUTHENTICATION = False

# EMAIL_AUTHENTICATIONを有効にする設計でも、
# 自動でSocialAccountを接続しない。
SOCIALACCOUNT_EMAIL_AUTHENTICATION_AUTO_CONNECT = False

また、ソーシャルログイン開始をGETで実行させないよう、明示的に次を入れておくと意図が分かりやすくなります。

SOCIALACCOUNT_LOGIN_ON_GET = False

4.8 URL

# config/urls.py
from django.urls import include, path

urlpatterns = [
    # allauth関連URLはここから一括で読み込む。
    path("accounts/", include("allauth.urls")),
]

usersessions などを個別に include() し直さないでください。

同じURL名が重複すると、reverse() の結果が意図しないパスになることがあります。

確認します。

python manage.py shell -c "from django.urls import reverse; print(reverse('usersessions_list'))"

正常例:

/accounts/sessions/

4.9 allauthの画面を自サイトへ統合する

{% extends "base.html" %}

{% comment %}
django-allauth の画面を自サイトのレイアウトへ統合する。
説明用コメントは複数行になるため、
{# ... #} ではなく comment タグを使う。
{% endcomment %}

{% block title %}
  {% block head_title %}アカウント{% endblock head_title %}
  | {{ site_setting.site_name }}
{% endblock %}

{% block head_extra %}
  <meta name="robots" content="noindex, nofollow">
  {% block extra_head %}{% endblock extra_head %}
{% endblock %}

{% block breadcrumb %}
  {% if user.is_authenticated %}
    {% include "partials/account_nav.html" %}
  {% endif %}
{% endblock %}

{% extends %} はテンプレート内の最初のテンプレートタグにします。

複数行コメントでは、

{% comment %}
...
{% endcomment %}

を使います。


4.10 django.contrib.sitesをサイト設定と同期する

このCMSでは django.contrib.sites を、Google/GitHubの設定に必須だから入れるのではありません。

サイト名とドメインを1か所へ揃える目的で利用します。

from urllib.parse import urlsplit

from django.conf import settings


def _sync_django_site(self) -> None:
    from django.contrib.sites.models import Site

    netloc = urlsplit(self.base_url).netloc
    if not netloc:
        return

    Site.objects.update_or_create(
        pk=getattr(settings, "SITE_ID", 1),
        defaults={
            "domain": netloc,
            "name": self.site_name,
        },
    )

    Site.objects.clear_cache()

これで、

example.com

の初期値がメール本文へ残る事故を防ぎます。


4.11 User Sessions

USERSESSIONS_TRACK_ACTIVITY = True

これを有効にすると、IPアドレス、User-Agent、最終アクセス時刻などのセッション情報が更新されます。

この設定を使うため、

"allauth.usersessions.middleware.UserSessionsMiddleware"

も入れます。

また、allauth.usersessions のテンプレートで使われるため、

"django.contrib.humanize"

も忘れないでください。


4.12 セッションバックエンドの注意

django-allauth公式ドキュメントでは、

SESSION_ENGINE = "django.contrib.sessions.backends.signed_cookies"

との組み合わせは推奨されていません。

signed cookieセッションは署名されますが暗号化されません

allauthは認証途中の確認コードなどをセッションへ保持するためです。

このCMSでは標準のDBセッションを使用します。


5. コードの意味

ACCOUNT_EMAIL_VERIFICATION

ACCOUNT_EMAIL_VERIFICATION = "mandatory"
動作
"none"メール確認を行わない
"optional"確認メールは送るが、確認前でも利用できる
"mandatory"確認するまで認証フローを完了させない

CMSの投稿者アカウントでは、メールアドレスの所有確認を行いたいため "mandatory" を使います。


ACCOUNT_PREVENT_ENUMERATION

アカウント列挙とは、

どのメールアドレスがこのサービスへ登録済みか

を外部から調べる攻撃です。

未対策

[email protected]
→ 「登録されていません」

[email protected]
→ 「メールを送りました」

という違いがあれば、登録者リストを推測できます。

ACCOUNT_PREVENT_ENUMERATION = True

でUI上の差を減らします。

さらに、より強い列挙耐性を重視するなら、

ACCOUNT_EMAIL_UNKNOWN_ACCOUNTS = True

も維持します。


SOCIALACCOUNT_AUTO_SIGNUPとEMAIL_AUTHENTICATIONは別物

ここは混同しやすい設定です。

SOCIALACCOUNT_AUTO_SIGNUP
        ↓
新規ソーシャルユーザーの登録フォームを省略するか

SOCIALACCOUNT_EMAIL_AUTHENTICATION
        ↓
確認済みメールが既存ローカルユーザーと一致した場合に、
それをログイン根拠として扱うか

SOCIALACCOUNT_EMAIL_AUTHENTICATION_AUTO_CONNECT
        ↓
上記のメール認証でログインしたソーシャルアカウントを、
ローカルアカウントへ永続的に接続するか

既存アカウント乗っ取り対策の説明を AUTO_SIGNUP だけに背負わせてはいけません。


ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE

ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE = False

は、

パスワードを変更した利用者の「現在のセッション」もログアウトさせるか

を制御します。

Djangoでは、認証セッションにパスワード由来のセッション認証ハッシュが含まれます。

パスワード変更後、古いハッシュを持つ他セッションは無効になります。

標準のパスワード変更処理は現在のセッションを新しいハッシュへ更新できるため、現在操作している利用者だけログイン状態を維持できます。


6. 内部で起きていること

ワンタイムコードの流れ

利用者
  │
  │ メールアドレス入力
  ▼
django-allauth
  │
  ├─ ログイン処理のレート制限
  ├─ コード生成
  ├─ 有効期限を設定
  └─ セッションへ認証途中の状態を保存
  │
  ▼
メール送信
  │
  ▼
利用者がコード入力
  │
  ├─ 期限内か
  ├─ 試行回数内か
  └─ コードが一致するか
  │
  ▼
ログイン成立

6桁コードの成功確率

6桁数字は、

000000 ~ 999999

なので100万通りです。

1つのコードに対して3回試せるなら、単純化したオンライン推測の成功確率は、

3 / 1,000,000
= 0.000003
= 0.0003%

です。

これは1つの発行済みコードに対して3回だけ試す場合の値です。

新しいコードを何度でも発行できるなら、挑戦回数は増えます。

したがって、

  • 短い有効期限
  • 少ない試行回数
  • 再送を無制限にしない
  • ログイン・発行経路のレート制限

をセットで考えます。

「桁数より試行回数が重要」は正確ではない

桁数と試行回数はどちらも効きます。

例えば、

6桁 → 8桁
100万通り → 1億通り
= 100倍

です。

一方、

3回試行 → 1回試行
= 3倍

です。

したがって、

桁数を増やすより、回数を絞る方が必ず効く

とは言えません。

このCMSでは、入力しやすさとのバランスから6桁を採用し、その代わりオンライン試行を厳しく制限します。


レート制限と複数ワーカー

LocMemCache

Worker 1 → 独自カウンター
Worker 2 → 独自カウンター
Worker 3 → 独自カウンター

では、本番の複数ワーカー環境で制限が分散します。

Redisなどの共有キャッシュなら、

Worker 1 ─┐
Worker 2 ─┼→ Redisの同じカウンター
Worker 3 ─┘

になります。

ただし、allauth組み込みレートリミッターには非原子的な処理があるため、強い並行アクセスでは少数の超過が起こる可能性があります。

認証経路では、アプリ内制限だけに依存せずWAFやリバースプロキシでも補助します。


7. コマンドの説明

python manage.py migrate

目的

allauth、socialaccount、sites、usersessionsなどのテーブルを作ります。

実行場所

manage.py があるディレクトリです。

python manage.py migrate

正常例

Applying account.... OK
Applying socialaccount.... OK
Applying sites.... OK
Applying usersessions.... OK

異常例

django.db.migrations.exceptions.InconsistentMigrationHistory

判断方法

python manage.py showmigrations

で対象アプリへ [X] が付いていることを確認します。


allauthの設定をシステムチェックする

python manage.py check

目的

INSTALLED_APPS、ミドルウェア、ログイン設定などの構成ミスを確認します。

正常例

System check identified no issues

異常例

account.W001 ...

判断方法

警告を無条件で無視せず、意図した設定か確認します。


メール本文を確認する

開発時はコンソールバックエンドを使用します。

EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"

ログインコードを要求するとターミナルへメール本文が出ます。

確認するのは、

  • 件名
  • サイト名
  • ドメイン
  • コード
  • リンク先

です。

「メール送信処理が動いた」だけで確認終了にしないでください。


8. よくあるエラー

8.1 django.contrib.humanizeを入れ忘れる

症状

端末一覧など一部画面だけテンプレートエラーになります。

原因

allauth.usersessions のテンプレートがhumanizeを利用します。

対処

INSTALLED_APPS += [
    "django.contrib.humanize",
    "allauth.usersessions",
]

8.2 ACCOUNT_LOGIN_BY_CODE_MAX_RESEND_COUNTが効かない

原因

現在のallauth公式設定として確認できません。

対処

再送可否は、

ACCOUNT_LOGIN_BY_CODE_SUPPORTS_RESEND = False

で制御します。

発行回数そのものを厳しく制御したい場合は、allauthのログイン制限とエッジ側レート制限を組み合わせます。


8.3 ACCOUNT_RATE_LIMITSへ存在しないキーを書いている

例えば、

"request_login_code": "3/5m/key"

を設定辞書へ書いたこと自体は、

このアクションが本当にallauth内部で消費されている

証明にはなりません。

公式ドキュメントのアクション一覧と突き合わせてください。


8.4 X-Forwarded-Forを信用する設定がallauthへ反映されない

独自の、

TRUSTED_PROXY_COUNT = 1

を作ってもallauthは自動では読みません。

allauth用の、

ALLAUTH_TRUSTED_PROXY_COUNT

または、

ALLAUTH_TRUSTED_CLIENT_IP_HEADER

を構成します。


8.5 SOCIALACCOUNT_AUTO_SIGNUPの意味を取り違える

SOCIALACCOUNT_AUTO_SIGNUP=False は、

新しいソーシャルユーザーの登録フォームを自動省略しない

という設定です。

既存アカウントのメール一致ログインを止める設定は、

SOCIALACCOUNT_EMAIL_AUTHENTICATION = False

です。


8.6 複数行コメントでテンプレートが壊れる

Djangoテンプレートの、

{# ... #}

は1行コメント用です。

複数行は、

{% comment %}
...
{% endcomment %}

を使います。

また、

{% extends "base.html" %}

は最初のテンプレートタグに置きます。


8.7 allauthのURLを二重にincludeする

path("accounts/", include("allauth.urls"))

だけにします。

usersessions などを個別にもう一度includeするとURL名が重複する可能性があります。


8.8 メールのサイト名がexample.comのまま

django.contrib.sites の初期値が残っています。

SiteSetting 保存時に Site を同期し、最後に、

Site.objects.clear_cache()

を実行します。


9. 動作確認

ログイン

  • /accounts/login/ が自サイトのデザインで表示される
  • メールアドレスとパスワードでログインできる
  • ユーザー名だけではログインできない
  • ログイン画面に noindex, nofollow がある
  • ログアウトはPOSTで確定する
  • ソーシャルログイン開始がGETだけで完了しない

登録・メール確認

  • 登録すると確認メールが送られる
  • 確認前のメールアドレスを確認済みとして扱わない
  • 確認メールのサイト名が example.com ではない
  • 12文字未満のパスワードが拒否される
  • 登録済みアドレスと未登録アドレスでUI応答から存在を判別しにくい

ログインコード

  • コードが6桁数字になっている
  • 正しいコードでログインできる
  • 間違ったコードを3回入力すると失効する
  • 180秒を過ぎたコードが使えない
  • 同一フローで無制限に再送できない
  • メールログインコードをMFAと誤認していない

レート制限

  • ACCOUNT_RATE_LIMITS の各キーが公式のアクション名である
  • login/ip が明記されている
  • login_failed/ip/key の両方を持つ
  • 複数ワーカーの本番では共有キャッシュを使う
  • リバースプロキシ環境ではallauth用のクライアントIP設定を行う
  • WAF / Nginx側でも認証経路を補助的に制限する

ソーシャルログイン

  • Client ID / SecretをGitへコミットしていない
  • SOCIALACCOUNT_AUTO_SIGNUP の意味を確認している
  • SOCIALACCOUNT_EMAIL_AUTHENTICATION=False
  • SOCIALACCOUNT_EMAIL_AUTHENTICATION_AUTO_CONNECT=False
  • SOCIALACCOUNT_LOGIN_ON_GET=False

端末一覧

  • /accounts/sessions/ が開く
  • django.contrib.humanize が入っている
  • USERSESSIONS_TRACK_ACTIVITY=True
  • UserSessionsMiddleware が入っている

10. セキュリティ上の注意

メールログインコードはMFAではない

メールアカウントを乗っ取られる
        ↓
ログインコードを読まれる
        ↓
CMSへログインされる

ためです。

8日目では「パスワードを使わないログイン方法」を追加します。

9日目で、

  • TOTP
  • パスキー
  • リカバリコード

を追加し、多要素認証を扱います。


認証情報をsettings.pyへ直書きしない

"secret": os.environ.get("GOOGLE_CLIENT_SECRET", "")

のように環境変数から読みます。

もし秘密情報をGitへコミットした場合は、

ファイルを消すだけでは不十分です。

認証情報を失効・再発行してください。


ソーシャルログインの3設定を混同しない

SOCIALACCOUNT_AUTO_SIGNUP = False
SOCIALACCOUNT_EMAIL_AUTHENTICATION = False
SOCIALACCOUNT_EMAIL_AUTHENTICATION_AUTO_CONNECT = False

は目的がそれぞれ違います。

「同じメールだから既存ユーザーへ自動接続しない」という説明を AUTO_SIGNUP だけで説明しないでください。


アカウント列挙対策にはトレードオフがある

ACCOUNT_PREVENT_ENUMERATION = True

は重要です。

さらに、

ACCOUNT_EMAIL_UNKNOWN_ACCOUNTS

によって、未登録メールアドレスへのメール送信をどう扱うかが変わります。

「画面の文言が同じだから完全に列挙できない」と断定しないでください。


レート制限はIP判定が正しくて初めて意味を持つ

X-Forwarded-Forは利用者が偽装できる場合があります。

リバースプロキシ構成を確認し、

ALLAUTH_TRUSTED_PROXY_COUNT

や、

ALLAUTH_TRUSTED_CLIENT_IP_HEADER

を正しく設定します。


Redisは必要だが万能ではない

複数プロセス間でカウンターを共有するため、Redisなどの共有キャッシュを使います。

ただし、allauthの組み込みレートリミッターだけを唯一の防御にはしません。

WAF・Nginx・Cloudflareなどのレート制限も補助に使います。


signed cookieセッションを使わない

allauthは確認コードなどの秘密情報をセッションへ保存します。

SESSION_ENGINE = "django.contrib.sessions.backends.signed_cookies"

では、セッション内容は署名されますが暗号化されません。

このCMSではDBセッションを使います。


11. 今日の復習問題

問1. ACCOUNT_LOGIN_BY_CODE_MAX_RESEND_COUNT を使わないのはなぜですか。再送可否はどの設定で制御しますか。

問2. SOCIALACCOUNT_AUTO_SIGNUPSOCIALACCOUNT_EMAIL_AUTHENTICATION の違いは何ですか。

問3. allauthのレート制限で、独自の TRUSTED_PROXY_COUNT だけを設定しても不十分なのはなぜですか。

問4. 6桁ログインコードを3回まで試せる場合、1つのコードに対する単純な推測成功確率はいくつですか。また、なぜ発行経路の制限も必要ですか。

問5. ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE は何を制御しますか。他端末のセッション無効化と同じ意味ですか。

解答

問1.

現在のallauth公式設定として ACCOUNT_LOGIN_BY_CODE_MAX_RESEND_COUNT を確認できないためです。

ログインコードの再送可否は、

ACCOUNT_LOGIN_BY_CODE_SUPPORTS_RESEND

で制御します。

ただし再送を無効化しても、新しいログインフローを何度も開始する攻撃まで防げるとは限らないため、ログイン経路のレート制限も必要です。


問2.

SOCIALACCOUNT_AUTO_SIGNUP は、新規ソーシャルユーザーの登録フォームを自動省略するかどうかです。

SOCIALACCOUNT_EMAIL_AUTHENTICATION は、プロバイダー側で確認済みのメールアドレスが既存ローカルアカウントと一致した場合、そのメール一致をログイン根拠として扱うかどうかです。

既存アカウントの乗っ取りリスクを議論するときは、後者を確認します。


問3.

TRUSTED_PROXY_COUNT はこのCMS独自の設定名であり、allauthが自動的に参照する設定ではないためです。

allauthのIP判定には、

ALLAUTH_TRUSTED_PROXY_COUNT

または、

ALLAUTH_TRUSTED_CLIENT_IP_HEADER

などを構成します。


問4.

3 / 1,000,000
= 0.0003%

です。

ただし、攻撃者が新しいコードを無制限に発行できれば挑戦回数自体を増やせます。

そのため、期限と試行回数だけでなく、コード発行・ログイン経路へのレート制限も必要です。


問5.

ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE は、パスワードを変更した本人の現在セッションをログアウトさせるかを制御します。

他のセッションの無効化そのものと同義ではありません。

Djangoはセッション認証ハッシュを使っており、パスワードが変わると古いハッシュを持つセッションは無効になります。標準の変更処理では、現在操作中のセッションだけ新しい認証ハッシュへ更新して維持できます。12. Git の差分

タグ    : day-08
コミット: day-08: django-allauth・メール確認・ワンタイムコードログインを導入
git diff day-07 day-08

設定だけを見る場合はこちらです。

git show day-08 -- config/settings.py

12. Gitの差分

タグ    : day-08
コミット: day-08: django-allauth・メール確認・ワンタイムコードログインを導入

前日との差分を確認します。

git diff day-07 day-08

認証設定だけを見る場合は、

git show day-08 -- config/settings.py

を使います。


13. 次回予告

9日目は、パスワードに依存しない認証を足します。

  • TOTP(認証アプリ)
  • リカバリコード
  • パスキー(WebAuthn)
  • 管理者への多要素認証の必須化
  • 本番で危険な設定を起動時に検出するシステムチェック

「テストで確かめる」だけでは足りない理由と、manage.py check --deploy に検査を載せる方法を扱います。

次回 → 【9日目】Django でパスキー認証


連載「10日で作る Django CMS」(第1部)

連載「10日で学ぶ Django 本番デプロイ」(第2部)

  • 11日目: Linuxサーバー初期設定(公開予定)(公開予定)
  • 12日目: SSHを鍵認証だけにする(公開予定)(公開予定)

残り 8 回は順次公開します。

成果物のソースコードは GitHub にあります。1日分が1つのタグ(day-01〜)に対応しているので、git diff day-02 day-03 で「その日に何が変わったか」だけを読めます。


14. 参考情報

django-allauth 公式ドキュメント

django-allauth Quickstart
https://docs.allauth.org/en/latest/installation/quickstart.html

Account Configuration
https://docs.allauth.org/en/latest/account/configuration.html

Account Rate Limits
https://docs.allauth.org/en/latest/account/rate_limits.html

Common Rate Limits
https://docs.allauth.org/en/latest/common/rate_limits.html

Social Account Configuration
https://docs.allauth.org/en/latest/socialaccount/configuration.html

Social Provider Configuration
https://docs.allauth.org/en/latest/socialaccount/provider_configuration.html

User Sessions Installation
https://docs.allauth.org/en/latest/usersessions/installation.html

OpenID Connect Identity Provider Configuration
https://docs.allauth.org/en/latest/idp/openid-connect/configuration.html

django-allauth Release Notes
https://docs.allauth.org/en/latest/release-notes/recent.html

Django公式ドキュメント

Using the Django authentication system
https://docs.djangoproject.com/en/5.2/topics/auth/default/

成果物

DjangoCMS
https://github.com/kurumonn/DjangoCMS

day-08
https://github.com/kurumonn/DjangoCMS/tree/day-08


読んだ内容を10問練習と実技で確認

記事で理解した用語を、StudyQuestの演習とクラウド実技ラボで定着させます。

10問練習 実技ラボ

コメント(0件)

まだコメントはありません。最初のコメントを投稿してください!

コメントを投稿