以下の内容はhttps://kamatimaru.hatenablog.com/entry/2025/01/19/192430より取得しました。


django-allauthでOIDC認証した際のDjango側のデータの持ち方について調べた

概要

以下の記事を参考にさせて頂きながら、Keycloak x django-allauthでのOIDC認証を検証していた。

ryu22e.org

OIDC認証で認証成功した際に、django-allauthDjangoの組み込みのユーザーモデルなどにデータをどのように保存するのか気になったので、実際に動かしながら調べてみた。

Keycloak側の事前準備

Keycloak上にユーザーを作成する

パスワードを設定する

ユーザー作成完了

DjangoからKeycloakでログインする

/accounts/login/(=django-allauthのデフォルトのログイン画面のURL)にアクセスする。

Keycloakに遷移する。

Keycloakのユーザー/パスワードを入力する

先ほど作ったKeycloakのユーザー/パスワードを入力する。

OIDC認証完了

Sign In」を押すとリダイレクトされてDjangoに返ってくる。以下のようなエラー画面になるが、認証は完了している。

※ エラーになるのはdjango-allauthのデフォルト設定で本人確認メールが有効だが、SMTPサーバーを用意していないからコネクションエラーになるのが原因なので今回調べたいことの本質とは関係ない。

Django側のDBの状態を確認

Django側のDBにユーザー情報がどのように保存されているのかをDjango Adminで確認する。

ユーザーモデル

まずはDjangoの組み込みのユーザーモデルを確認すると、以下のように保存されている。

これより以下のことが分かる。

  • django-allauthDjangoの組み込みのユーザーモデルを使用する
  • 基本的にKeycloak上の情報をDjangoのユーザーモデルにマッピングして保存している
  • OIDC認証の場合、ユーザー名やメールアドレスなどはRP側のDBには保持せずにJWTから読み込んだり都度取得したりする設計も可能だが、そうではなくユーザー名やメールアドレスもDBに保存している

SocialAccountモデル

django-allauthを導入してマイグレーションを実行すると、ユーザーモデルを外部参照するSocialAccountというモデルが作成される。

以下のようなデータが保存されている。

  • Uid: Keycloak上のユーザーID(=OIDCのクレームのsub)
  • Extra data: 恐らくIDトークンの中身をJSON形式でごそっと保存したもの

(参考)Keycloak側のユーザーIDは以下のように確認できる & 一致していることが分かる。

ユーザーモデルのパスワードについて

ここでユーザーモデルのパスワードをどうしているのかが気になった。

  • 気になった理由
    • OIDC認証ではパスワードをID Providerにしか保持しないはずである
    • 他方でDjangoの組み込みのユーザーモデルにおいて、パスワードは必須フィールドである

Django Admin上ではパスワードがどのように保存されているかは確認できないので、Django Shellで確認してみた。すると以下のような先頭が!から始まる文字列が保存されていた。

>>> from django.contib.auth.models import User
>>> user = User.objects.get(username="test")
>>> user.password
'!XeAXbVE84yge5YVPrextythu2u7RwgYItHfgb1kq'
>>> user = User.objects.get(username="delhi09")
>>> user.password
'!8wjVPvaa2sEPmGzCKGcUXgEQ5IpwxMnpniJJQjsE'
>>>

これは何だろうと思い、django-allauthソースコードを調べていたら、Provider#sociallogin_from_responseというメソッド内でuser.set_unusable_passwordというものを呼んでいた。

django-allauth/allauth/socialaccount/providers/base/provider.py

# 省略
class Provider:
    # 省略
    def sociallogin_from_response(self, request, response):
        # 省略
        user.set_unusable_password()
        adapter.populate_user(request, sociallogin, common_fields)
        return sociallogin

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

set_unusable_passworddjango-allauthではなくDjangoが提供しているAPIであり、照合に失敗するパスワードを保存するメソッドらしい。 https://docs.djangoproject.com/ja/4.2/ref/contrib/auth/#django.contrib.auth.models.User.set_unusable_password

Djangoソースコードを読むと、内部処理的にはmake_passwordを引数Noneで呼んでいるだけである。

base_user.py

class AbstractBaseUser(models.Model):
    # 省略
    def set_unusable_password(self):
        # Set a value that will never be a valid hash
        self.password = make_password(None)
    # 省略

https://github.com/django/django/blob/main/django/contrib/auth/base_user.py

make_passwordは引数のpasswordNoneの場合UNUSABLE_PASSWORD_PREFIXとランダム文字列を連結させて返している。

hashers.py

def make_password(password, salt=None, hasher="default"):
    # 省略 
    if password is None:
        return UNUSABLE_PASSWORD_PREFIX + get_random_string(
            UNUSABLE_PASSWORD_SUFFIX_LENGTH
        )
    # 省略
    return hasher.encode(password, salt)

https://github.com/django/django/blob/main/django/contrib/auth/hashers.py

UNUSABLE_PASSWORD_PREFIX!だった。

これでパスワードに!から始まる文字列が保存されている理由が分かった。




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

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