概要
以下の記事を参考にさせて頂きながら、Keycloak x django-allauthでのOIDC認証を検証していた。
OIDC認証で認証成功した際に、django-allauthがDjangoの組み込みのユーザーモデルなどにデータをどのように保存するのか気になったので、実際に動かしながら調べてみた。
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-allauthはDjangoの組み込みのユーザーモデルを使用する- 基本的にKeycloak上の情報をDjangoのユーザーモデルにマッピングして保存している
- OIDC認証の場合、ユーザー名やメールアドレスなどはRP側のDBには保持せずにJWTから読み込んだり都度取得したりする設計も可能だが、そうではなくユーザー名やメールアドレスもDBに保存している
SocialAccountモデル
django-allauthを導入してマイグレーションを実行すると、ユーザーモデルを外部参照するSocialAccountというモデルが作成される。
以下のようなデータが保存されている。

(参考)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
set_unusable_passwordはdjango-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は引数のpasswordがNoneの場合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が!だった。

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