検索しても「認証機能自体を『追加』する」記事しかヒットしなかったり、ChatGPT 4 君に聞いても上手く動かなかったりして、けっこう試行錯誤したので、誰かの助けになるかなと思って記事を書きます。
背景
イーロンが Twitter の方針をアレコレ変えるせいで、半永続的なインフラと思われていた Twitter の OAuth 認証がマジで使えなくなる危険が出てきました。万が一の事態に備えて、別の認証手段を追加し、既存ユーザーにそれを統合させる手段を用意しておいた方が安全です。
本記事では Discord 認証を想定しています。
Discord 認証を追加
この記事が必要になる方は、既に他のサービスでの認証は実装していると思われるので、細かい説明は省きますが、Discord でも同様にプロバイダーの追加や、コールバック URL の設定などをしていきます。
この時点で認証はできますが、認証済のユーザーが Discord ログインを行った場合、新規ユーザーとして登録されてしまう、という問題が起きます。
理想は、
・認証済み→統合される
・認証済みでない→新規ユーザーとして登録
なので、これはちょっと期待した動きではありません。
ChatGPT 君に聞こう
こういう時は ChatGPT に聞くに限ります。
ふむふむ、アダプターというのを設定すればいいのね。
settings.py
に以下を追記した上で、
AUTHENTICATION_BACKENDS = (
...
'myapp.adapters.CustomAccountAdapter',
'myapp.adapters.CustomSocialAccountAdapter',
...
)
adapters.py
を作って以下をコピペ。
from allauth.account.adapter import DefaultAccountAdapter
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
class CustomAccountAdapter(DefaultAccountAdapter):
pass
class CustomSocialAccountAdapter(DefaultSocialAccountAdapter):
def is_auto_signup_allowed(self, request, sociallogin):
return False
def save_user(self, request, sociallogin, form=None):
if request.user.is_authenticated:
sociallogin.connect(request, request.user)
return request.user
return super().save_user(request, sociallogin, form)
結果:解決せず
おいおい、AI さんよお……。ちゃんと仕事してくれなきゃ困るぜ。
解決手段
コードをよくみると、ChatGPT 君は save_user
の段階で認証済みかどうかをチェックしているのですが、これは認証済みであっても匿名ユーザー扱いになってしまうようです。いろいろ調べた結果(海外のどっかの掲示板だっけ?)、pre_social_login
というメソッドをオーバーロードすると、既存ユーザーの認証具合を調べられるようです。
結果的に、以下のようにコードを改造するとうまくいきました。
from django.http import HttpResponseRedirect
from django.urls import reverse
from allauth.account.adapter import DefaultAccountAdapter
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
class CustomAccountAdapter(DefaultAccountAdapter):
pass
class CustomSocialAccountAdapter(DefaultSocialAccountAdapter):
def pre_social_login(self, request, sociallogin):
# print(request.user)
if request.user.is_authenticated:
sociallogin.connect(request, request.user)
return
def is_auto_signup_allowed(self, request, sociallogin):
return True
def save_user(self, request, sociallogin, form=None):
if request.user.is_authenticated:
sociallogin.connect(request, request.user)
return request.user
return super().save_user(request, sociallogin, form)
さらっと、is_auto_signup_allowed
が True
を返すようにしていますが、これにしないとメールアドレスを記入する画面とかが挟まれるようになります。後半の save_user
の if
文は無意味になってる気がしますが、まあとりあえずこれで動いている以上、余計な改変や解説はせずにそのまま載せています。詳しい人から見たら無駄な実装や間違いがあるかもしれませんが、とりあえず解決事例を報告した方がいいと思ったので。
注意事項
これをやる場合、既に連携しているソーシャルプロバイダを更に追加しようとするとアサーションエラーが起こります。例えば、Discord に認証していない時のみ、ブラウザ上で「Discord 認証」ボタンを押せるようにするとかすれば、普通のユーザーがトラブルになることはないと思います。