以下の内容はhttps://kamatimaru.hatenablog.com/entry/2025/02/14/013345より取得しました。


django-allauthのSign In画面をスキップする方法

課題

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

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

「Continue」を押すとようやくKeycloakのログイン画面に辿り着ける。

間に意味のない画面が2つ挟まるとユーザー体験が悪いので、特にSSOのIdPがKeycloakのみで選択の余地がない場合などは、未ログイン状態でログイン必須な画面にアクセスした場合に、Sign In画面を遷移せずに直接IdPのログイン画面に遷移してほしい。

実現方法を調べた。

Sign In画面の正体

まずは1つ目のSign In画面の正体を調べる。

Sign In画面のURLは/accounts/login/であるが、これはDjangosettings.LOGIN_URLのデフォルト値である。

https://docs.djangoproject.com/ja/5.1/ref/settings/#login-url

django-allauthもLOGIN_URLに設定されたURLにリダイレクトさせているだけである。従って、例えばsettings.pyLOGIN_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.BaseLoginViewdispatchで使われていた。

https://github.com/pennersr/django-allauth/blob/main/allauth/socialaccount/providers/base/views.py

詳細は割愛するが、BaseLoginViewを継承しているViewを調べたところ、get_providerメソッドをオーバーライドしているだけであり、getpostdispatchなどの処理のメインストーリームになる処理はオーバーライドされていなかった。従って、処理の本体はBaseLoginViewdispatchである。

Sign In Via Keycloak 」画面のHTMLを読むと、「Continue」を押した際に同一URLに対してパラメータなしでPOSTする実装になっている。

ここでBaseLoginViewdispatchの実装を見てみると、POSTの場合は、respond_to_login_on_getNoneを返すから、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のログイン画面にリダイレクトできそうである。

  1. settings.LOGIN_URLを独自のURLにする
  2. 1.のURLに紐付けたViewのGET処理の中で、以下の処理を行う。
    • Keycloak用のProviderのインスタンスを取得する。
    • return provider.redirect_from_request(request)を実行する

仮説を検証してみる。

settings.LOGIN_URLの定義

settings.pyLOGIN_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アプリ側でのログインも成功していることを確認できた。




以上の内容はhttps://kamatimaru.hatenablog.com/entry/2025/02/14/013345より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14