Django allauthを使ってログイン機能を実装する

Django
DjangoGoogle Cloud Platform

Djangoでログイン機能を実装する場合はallauthを使うのが便利である。非常に簡単に一連のログイン・サインアップに関わる機能を実装することができる。しかし、細かい仕様を実装するには工夫が必要であり、ややカスタマイズの難易度が高い。

この記事ではallauthを使ってログイン機能を実装するところから、メールアドレスの変更やソーシャルアカウントとメールアドレスの紐付けなど細かい仕様をSignalという機能を使ってカスタマイズする例を紹介する。

スポンサーリンク

環境

  • Django==3.1.3
  • django-allauth==0.44.0

allauthの公式ドキュメントはこちら

スポンサーリンク

allauthを使う理由

Djangoにはallauth以外にもpython-social-authなどのログイン機能を実装するためのパッケージがあるし、そもそもパッケージを使わなくても割と簡単にログイン機能を実装することができる。
その中でallauthを使用する理由は以下のようなものが挙げられる。

  • ログインに必要な一連の機能をとても簡単に実装できる。
    • 単にログインだけではなく、メールアドレス確認の手続きやパスワードの変更、パスワードを忘れた場合の手続きがデフォルトで使える。(これらのログインシステムを1から作るのはかなり複雑で大変。)
    • ソーシャルログインにも対応していてGoogle、Apple、Facebook、Github、LINEなど連携できるアカウントがとてもたくさんある。
  • セキュリティもある程度は高くなる。
    • 例えばデフォルトでログイン試行回数を制限してブルートフォース攻撃を対策している。
  • ユーザー数が多く、ちゃんとメンテナンスされている。
  • 公式のドキュメントの内容が充実している。
    • python-social-authはドキュメントが弱すぎる。

allauthの対象外の領域

allauthはログインに関わる一連の機能を実装しているが、以下に示す対象外の領域がある。

  • ユーザ情報の更新
    • メールアドレスの更新とパスワードの変更には対応しているが、usernameの変更には対応していない。

これは自前のViewやFormを用意する必要があるので注意が必要である。

セットアップの手順

Djangoアプリ自体のセットアップ手順は省略し、アプリの作成が完了していることを前提とする。カスタムユーザは最初に作成したほうがが良いので予め設定しておくと良い。デフォルトのユーザー設定については以下の記事に書いた。

allauthのセットアップの方法はここに簡単に書いてある。
注意点を書いておくと、

  • 使えるソーシャルアカウントが非常に多いので好きなものを選ぶ。
  • SOCIAL_ACCOUNT_PROVIDERSkeyは無記入でOK。

例えば「Googleでログイン」を実装したい場合はGoogle Cloud PlatformのOAuth設定からclient_idsecretを取得する必要がある。OAuthについての説明はここにあるが分かりにくいかも。

migrateまで完了すると、urls.pyに設定したようにhttp://127.0.0.1:8000/accounts/loginのようにaccounts/以下にログイン関連の画面が表示される。この画面の表示を変更する方法は次の章で説明する。

ログインやサインアップ時の設定を変更するには、settings.pyを編集すれば良い。例えば、次のように設定する。

# 必ずカスタムユーザを作る
AUTH_USER_MODEL = 'myapp.User'

# ログインURLやリダイレクト先の設定
LOGIN_URL = 'account_login'
LOGIN_REDIRECT_URL = 'hoge'
LOGOUT_REDIRECT_URL = 'account_login'
ACCOUNT_LOGOUT_REDIRECT_URL = 'account_login'
ACCOUNT_SIGNUP_REDIRECT_URL = 'hoge'

# ログイン・サインアップ時の設定
ACCOUNT_AUTHENTICATION_METHOD = 'email' # メールアドレスでログイン
ACCOUNT_EMAIL_REQUIRED = True # メールアドレスでログインする場合は必要
ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_EMAIL_VERIFICATION = 'mandatory' # メールアドレスを認証するか(none=しない, mandatory=必須)
ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = 1 # 確認メールの有効期限(日)

# ソーシャルアカウントでログイン・サインアップ時の設定
SOCIALACCOUNT_EMAIL_VERIFICATION = 'none' 
SOCIALACCOUNT_EMAIL_REQUIRED = False
SOCIALACCOUNT_USERNAME_REQUIRED = False

# その他の設定
ACCOUNT_EMAIL_SUBJECT_PREFIX = '' # メールの件名のプレフィックス
ACCOUNT_MAX_EMAIL_ADDRESSES = 2 # 登録できるメールアドレスの上限。1だと変更できない。 
ACCOUNT_USERNAME_BLACKLIST = [] # usernameとして使えない文字

# メール送信の設定 Gmailを使う。
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_PORT = 587
EMAIL_HOST_USER = '[email protected]'
EMAIL_HOST_PASSWORD = 'gmailのログインパスワード'
EMAIL_USE_TLS = True

設定の詳細はここに書かれている。細かいが重要な設定がたくさんあるので全ての設定に目を通した方が良い。
例えば、ログインの試行回数がデフォルトで5回に設定されていたり、5回間違えるとデフォルトで5分間ログイン試行が禁止されたりと、セキュリティに関わる設定も数多く考慮されている。

「メールアドレスを認証する」というのはメールアドレスが実際に使われており、本人が所有するものか確かめるために、そのメールアドレス宛にメールを送り、メールにあるリンクをクリックしてもらうことで確認する仕組みである。
メールアドレスが認証できれば、「パスワードを忘れた場合」にそのメールアドレス宛に再設定用のリンクを送ることができる。

メールを送信する方法はいくつかあるので要件に合うものを設定すると良い。上の例はGmailを使っているが、Gmail側で認証が必要となる。他にも例えばSendGridは非常に幅広く高機能なサービスを提供しているが審査があるのでとりあえずメールを送りたい人には適していないかもしれない。

画面を編集する方法

デフォルトで表示されるログイン関連の画面は、allauthのパッケージの中(allauth/templates/)に入っている。そのまま編集するとallauthをupdateをした際に問題になるので自分のアプリのtemplates内のフォルダ・ファイルを全てコピーして自分のアプリのtemplateにペーストする。

myapp/templates/accountとmyapp/templates/socialaccountの2つのディレクトリができていればallauthモジュールのHTMLではなくこちらが優先される。(他のブログ記事に書かれているようにsettings.pyをいじる必要はない。)

メールアドレスの変更

allauthはメールアドレスの変更に対応していない。代わりにメールアドレスは複数登録できるようになっている。
変更に対応したい場合、登録数の上限はsettings.pyACCOUNT_MAX_EMAIL_ADDRESSESで定義したように2としておくのが良い。(デフォルトで5になっている。)1とすると変更できなくなる。
また、allauthではデフォルトでメールアドレスは一意(ACCOUNT_UNIQUE_EMAIL=True)となっているため既に登録されているメールアドレスに変更することはできない。

allauthでメールアドレスを変更しようと思ったとき、ユーザが行うのは

  1. 新しいメールアドレスを登録する。
  2. 新しいメールアドレスを認証する。
  3. 古いメールアドレスを削除する。

という手順を踏む必要がある。このままでは手順3を行わないユーザが出てくるので、手順2が完了したら自動的に手順3が実行されるようにしたい。
これを実装するには少しの工夫が必要がある。

着目するのはSignalsである。簡単に説明すると、何かしらのアクションが完了したことを他の場所に通知する仕組みである。
詳しくはDjangoのドキュメントを見てほしい。

allauthではメールアドレスの認証が終わるとemail_confirmedというSignalが出る。これを利用すれば自動的に手順3を完了することができる。
そこで、myapp/signals.pyを新しく追加し、以下のような内容にする。

from allauth.account.signals import email_confirmed
from django.dispatch import receiver

from allauth.account.models import EmailAddress

@receiver(email_confirmed)
def email_confirmed_(request, email_address, **kwargs):
    user = email_address.user
    old_email = EmailAddress.objects.filter(user=user).exclude(email=email_address.email)
    if old_email.exists():
        user.email = email_address.email
        user.save()
        email_address.primary = True
        email_address.save()
        old_email.delete()
    else:
        pass

流れをざっくり説明すると、email_confirmedというSignalを受け取る度にemail_confirmed_という関数が実行される。
allauthではUserモデルの他にEmailAddressというモデルをメールアドレスのテーブルとして使っている。そこに古いメールアドレスがあれば、Userモデルのemailを新しいメールアドレスに更新し、allauthのモデルEmailAddressprimary=Trueとしておく。最後に古いメールアドレスを消去する。(上記のようにUserモデルのメールアドレスを新しいものに更新しないと削除されないので注意。)

またreceiver()デコレータを使っているのでこのままだとsignals.pyを読み込んでくれないので、myapp/app.pyを次のように書き換える。

from django.apps import AppConfig

class MyappConfig(AppConfig):
    name = 'myapp'

    def ready(self):
        import myapp.signals

ちなみに、このSignal処理に関するコードはどこに書いても良いらしいが、実際にはアプリの中のsignalsサブモジュール(つまり上述したようなmyapp/signals.py)の中で書かれることが慣例のようである。
Signalのドキュメントにしれっと書いてある。

以上でaccount/email.htmlを例えば次のように書き換えればメールアドレスの変更に対応できたことになる。(見た目は整えていない。)

{% extends "base.html" %}

{% block title %}メールアドレス{% endblock %}

{% block content %}

<div>
    <main>
        <div>
            {% for emailaddress in user.emailaddress_set.all %}
            {% if emailaddress.verified %}
            <p>登録されているメールアドレス</p>
            <p>{{ emailaddress.email }}</p>
            {% else %}
            <p>未登録のメールアドレス</p>
            <form action="{% url 'account_email' %}" method="post">
                {% csrf_token %}
                <input type="text" readonly name="email" value="{{emailaddress.email}}"/>
                <p>
                    メールアドレスを変更する場合は、上記メールアドレス宛に送信されたメールをご確認ください。
                    メールが届いていない場合は、「確認メールを再送する」ボタンをクリックしてください。			
                </p>
                <button type="submit" name="action_send" >確認メールを送信する</button>
                <button type="submit" name="action_remove" >削除</button>
            </form>
            {% endif %}
            {% endfor %}
        </div>
        {% if can_add_email %}
        <form method="post" action="{% url 'account_email' %}" class="add_email">
            {% csrf_token %}
            <p>
                <label for="id_email">新しいメールアドレス</label>
                <input type="email" name="email" placeholder="メールアドレス" required="" id="id_email">
            </p>
            <p>メールアドレスを所有していることを確認するためにメールが送信されます。確認が完了するとメールアドレスが更新されます。</p>
            {% for error in form.errors.email %}
            <p>{{ error }}</p>
            {% endfor %}
            <button name="action_add" type="submit">確認メールを送信</button>
        </form>
        {% else %}
        <p>変更する場合は、未確認となっているメールアドレスを削除してください。</p>
        {% endif %}
    </main>
</div>

<script type="text/javascript">
    (function() {
        var message = "選択されたメールアドレスを削除しても良いですか?";
        var actions = document.getElementsByName('action_remove');
        if (actions.length) {
            actions[0].addEventListener("click", function(e) {
                if (! confirm(message)) {
                    e.preventDefault();
                }
            });
        }
    })();
</script>

{% endblock %}

確認ができていない状態では登録されているメールアドレスをそのまま表示し、確認を促す。確認ができた時点で登録されているメールアドレスが変更され、また新しいメールアドレスを登録できるようになっている。また、未確認のメールアドレスの削除にも対応している。

ソーシャルアカウントに関しても1プロバイダーにつき複数登録できるようになっている。変更に対応したい場合はこの章を参考にすれば対応できるはずである。

ソーシャルアカウントとメールアドレスを紐付ける

あるユーザがGmailで「メールアドレスでアカウント登録」している状態でログアウトした後、同じGmailで「Googleでログイン」したときどうなるだろうか?
同じメールアドレスを使っているため同じアカウントと認識してくれるだろうか?
allauthのデフォルトの設定では答えは「NO」である。つまり、新しいアカウントでアカウント登録されてしまう。

なぜこのような仕様になっているかはセキュリティの観点から説明される。ソーシャルアカウントによってはメールアドレスの認証をしていないため、悪意のある人が他人のメールアドレスでアカウントを作成し、あなたのサービスに不正ログインする可能性がある。
最近はメールアドレスの認証を行うサービスがほとんどであるが、ソーシャルアカウントと連携する場合は確認した方が良い。例えば、2015年時点ではFacebookはメールアドレスの認証をしていなかったらしい。2021年時点ではメールアドレスの認証をしている。

以上のセキュリティ上考慮すべき点を理解した上で、メールアドレスの検証をしているソーシャルアカウントとメールアドレスを紐付けるためには、メールアドレスの変更と同様にSignalを利用する。
myapp/signals.pyというファイルに次のように記述する。

from allauth.socialaccount.signals import pre_social_login
from django.dispatch import receiver

from allauth.account.models import EmailAddress

@receiver(pre_social_login)
def pre_social_login_(request, sociallogin, **kwargs):

    if sociallogin.is_existing:
        return

    if not sociallogin.email_addresses:
        return

    verified_email = None
    for email in sociallogin.email_addresses:
        if email.verified:
            verified_email = email
            break

    if not verified_email:
        return

    try:
        existing_email = EmailAddress.objects.get(email__iexact=email.email, verified=True)
    except EmailAddress.DoesNotExist:
        return

    sociallogin.connect(request, existing_email.user)

pre_social_loginというSignalを受け取るとpre_social_login_という関数が実行される。pre_social_loginというSignalはユーザがソーシャルプロバイダーから認証された時点、つまりログインが完全に処理する直前に送信される。
「login」とついているが、サインアップやソーシャルアカウントを追加するときも送信されるため、ユーザが既にソーシャルアカウントでログインしているか(socialloginがすでに存在するのか)確かめている。

おわりに

この記事ではallauthを使ってログインを実装する方法と、Signalを使ったカスタマイズの例を2つ紹介した。簡単に実装できるとはいえ、カスタマイズの難易度がやや高いのがallauthの特徴だが慣れるととても使いやすいので、おすすめできるモジュールである。

誤植や質問がありましたらコメントしていただければ返答致します。

スポンサーリンク
H-MEMO

コメント