課題
django-allauthのデフォルトでは、未ログイン状態でログイン必須な画面(=login_requiredやLoginRequiredMixinが使われている画面)にアクセスすると、以下のSign In画面にリダイレクトされる。

この画面で「Keycloak」を押すと続けて以下の画面に遷移する。

「Continue」を押すとようやくKeycloakのログイン画面に辿り着ける。
間に意味のない画面が2つ挟まるとユーザー体験が悪いので、特にSSOのIdPがKeycloakのみで選択の余地がない場合などは、未ログイン状態でログイン必須な画面にアクセスした場合に、Sign In画面を遷移せずに直接IdPのログイン画面に遷移してほしい。
実現方法を調べた。
Sign In画面の正体
まずは1つ目のSign In画面の正体を調べる。
Sign In画面のURLは/accounts/login/であるが、これはDjangoのsettings.LOGIN_URLのデフォルト値である。
https://docs.djangoproject.com/ja/5.1/ref/settings/#login-url
django-allauthもLOGIN_URLに設定されたURLにリダイレクトさせているだけである。従って、例えばsettings.pyにLOGIN_URL=http://exmple.com/と設定すると、以下のようにhttp://exmple.comにリダイレクトされる。

django-allauthではリクエストパスが/accounts/login/の場合に、allauth.account.views.LoginViewが呼ばれる。
https://github.com/pennersr/django-allauth/blob/main/allauth/account/views.py


LoginViewでは以下のテンプレートが使われている。テンプレートの中身を確認すると、確かにIf you have not created an account yet,...という文言が入っている。
https://github.com/pennersr/django-allauth/blob/main/allauth/templates/account/login.html
django-allauthのデフォルトのSign In画面の正体は分かった。
「Sign In Via Keycloak 」画面の正体
次に2つ目の「Sign In Via Keycloak 」という画面の正体を調べる。
これは正攻法で調べられなかったので、You are about to sign in using a third-party account fromという文言でソースコードをgrepしてみた。
→ allauth/templates/socialaccount/login.htmlのものであることが分かった。
https://github.com/pennersr/django-allauth/blob/main/allauth/templates/socialaccount/login.html
→ このテンプレートを使っているViewを調べたところ、直接Viewには設定されておらず、allauth.socialaccount.providers.base.utils.respond_to_login_on_getで使われていた。
https://github.com/pennersr/django-allauth/blob/main/allauth/socialaccount/providers/base/utils.py
respond_to_login_on_getを使っている箇所をgrepしたら、allauth.socialaccount.providers.base.BaseLoginViewのdispatchで使われていた。
https://github.com/pennersr/django-allauth/blob/main/allauth/socialaccount/providers/base/views.py
詳細は割愛するが、BaseLoginViewを継承しているViewを調べたところ、get_providerメソッドをオーバーライドしているだけであり、get、post、dispatchなどの処理のメインストーリームになる処理はオーバーライドされていなかった。従って、処理の本体はBaseLoginViewのdispatchである。
「Sign In Via Keycloak 」画面のHTMLを読むと、「Continue」を押した際に同一URLに対してパラメータなしでPOSTする実装になっている。

ここでBaseLoginViewのdispatchの実装を見てみると、POSTの場合は、respond_to_login_on_getがNoneを返すから、return provider.redirect_from_request(request)が実行されると考えられる。
class BaseLoginView(View): provider_id: str # Set in subclasses def dispatch(self, request, *args, **kwargs): if allauth_settings.HEADLESS_ONLY: raise Http404 provider = self.get_provider() resp = respond_to_login_on_get(request, provider) if resp: # ←POSTの場合、`resp`がNoneになるのでこのifブロックには入ってこない return resp return provider.redirect_from_request(request) # POSTの場合こっちが実行される
以上より、Viewの中でreturn provider.redirect_from_request(request)を実行すると、「Continue」ボタンを押した時と同じようにKeycloakのログイン画面にリダイレクトできそうということが分かった。
Sign In画面のスキップの実現方法
これまでの調査より、以下の項目を実装すると、未ログイン状態でログイン必須な画面にアクセスした際に、直接Keycloakのログイン画面にリダイレクトできそうである。
settings.LOGIN_URLを独自のURLにする- 1.のURLに紐付けたViewのGET処理の中で、以下の処理を行う。
- Keycloak用のProviderのインスタンスを取得する。
return provider.redirect_from_request(request)を実行する
仮説を検証してみる。
settings.LOGIN_URLの定義
settings.pyにLOGIN_URL = "app:auth_request"と定義する。app:auth_requestはこれから実装する。
settings.py
# ...省略 LOGIN_URL = "app:auth_request"
Viewの定義
以下の空のViewを定義する
views.py
class AuthRequestView(View): def get(self, request): pass
urls.pyの定義
定義したViewをリクエストパスに紐付ける
urls.py
# ...省略 app_name = "app" urlpatterns = [ # ...省略 path("auth_request/", views.AuthRequestView.as_view(), name="auth_request"), ]
Providerインスタンスの取得方法の調査
Viewの中で、Keycloak用のProviderのインスタンスを取得したいが、やり方がわからないのでdjango-allauthのソースコードから調べる。ちょうど先に調べたBaseLoginViewの中でやっている。
https://github.com/pennersr/django-allauth/blob/main/allauth/socialaccount/providers/base/views.py
# ...省略 from allauth.socialaccount.adapter import get_adapter # ...省略 class BaseLoginView(View): provider_id: str # Set in subclasses # ...省略 def get_provider(self): provider = get_adapter().get_provider(self.request, self.provider_id) return provider
provider_idが分かればproviderを取得できそうである。
→ Keycloakをdjango-allauthに設定する際にsettings.pyに以下のような定義をしている。provider_idというフィールドにkeycloakという文字列を渡しているので、これのことだと考えられる。
settings.py
# ...省略 SOCIALACCOUNT_PROVIDERS = { "openid_connect": { "APPS": [ { "provider_id": "keycloak", # これ "name": "Keycloak", "client_id": "testclient", "secret": "********", "settings": { "server_url": "http://proxy:80/realms/master/.well-known/openid-configuration", }, } ] } } # ...省略
AuthRequestViewの実装
providerインスタンスの取得方法がわかったので、AuthRequestViewを実装する。
# ...省略 from allauth.socialaccount.adapter import get_adapter # ...省略 class AuthRequestView(View): def get(self, request): provider = get_adapter().get_provider(request, "keycloak") return provider.redirect_from_request(request)
結果
Keycloakのログイン画面に遷移できた。

URLのパラメータもOIDCの仕様に準拠したものが付与されている。
http://proxy/realms/master/protocol/openid-connect/auth?client_id=testclient&redirect_uri=http%3A%2F%2Flocalhost%3A18000%2Faccounts%2Foidc%2Fkeycloak%2Flogin%2Fcallback%2F&scope=email+openid+profile&response_type=code&state=E2fs6e3NlDZop6oR
ID/パスワードを入力するとちゃんとコールバックされて、Djangoアプリ側でのログインも成功していることを確認できた。
