Azure ADを利用したDjango adminのSAML認証

SREの菅原です。

カンムのサービスのバックエンドは基本的にGoで書かれているのですが、一部の内部向け管理画面はPythonフレームワークDjangoで作成されています。

スタッフがDjango adminページにログインして各種オペレーションを行うのですが、adminページにログインするためにはDjango adminのアカウントが必要です。

社内で使う各種サービスのアカウントは基本的にはAzure Active Directoryを使ったSSOで一元管理されていますが、管理用WebアプリはSAML対応の実装をしておらず、前段のロードバランサー(ALB)でOIDC認証しているものの、adminページ自体のアカウントは管理用Webアプリで追加しなければいけない状態でした。

管理用Webアプリが独自にアカウント管理してしまうと、個別にアカウントを作成する手間が増え、Azure ADでの一元管理のメリットも薄れてしまいます。そこで管理用WebアプリでSAML認証ができるように改修をすることにしました。

django-saml2-auth → djangosaml2

DjangoSAML対応にはいくつかライブラリが存在します。

最初はgrafana/django-saml2-authを使って実装を進めていたのですが、動作確認を行ったところ以下のIssueの問題が発生しました

github.com

問題の対応にはdjango-saml2-auth自体の改修が必要そうであり、アップストリームへの修正の反映には時間がかかりそうだったため、django-saml2-authの利用は諦めIssueのコメントで触れられているdjangosaml2を使った実装に切り替えました。

djangosaml2を使った実装

djangosaml2の利用方法はドキュメントに詳しく書かれています。
※Azure AD側の設定については省略

まずは必要なライブラリを追加。

# Dockerfile
apt install libxmlsec1-dev pkg-config  xmlsec1
# requirements.txt
djangosaml2==1.5.5

settings.pyは以下のように修正。

INSTALLED_APPS = [
    # ...
    "djangosaml2",
]

MIDDLEWARE = [
    # ...
    'djangosaml2.middleware.SamlSessionMiddleware',
]

AUTHENTICATION_BACKENDS = [
    "django.contrib.auth.backends.ModelBackend",
    #"djangosaml2.backends.Saml2Backend",
    "apps.auth.saml2.ModifiedSaml2Backend",
]

SESSION_COOKIE_SECURE = True
LOGIN_URL = "/saml2/login/"
LOGIN_REDIRECT_URL = '/admin'
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
SAML_IGNORE_LOGOUT_ERRORS = True
SAML_DJANGO_USER_MAIN_ATTRIBUTE = "username"
SAML_CREATE_UNKNOWN_USER = True

SAML_ATTRIBUTE_MAPPING = {
     "name": ("username",),
     "emailAddress": ("email",),
     "givenName": ("first_name",),
     "surname": ("last_name",),
}

SAML_METADATA_URL = "https://login.microsoftonline.com/..."

SAML_CONFIG = {
    "entityid": "https://my-admin.example.com/saml2/acs/",
    "service": {
        "sp": {
            "endpoints": {
                "assertion_consumer_service": [
                    ("https://my-admin.example.com/saml2/acs/", saml2.BINDING_HTTP_POST),
                ],
                "single_logout_service": [
                    ("https://my-admin.example.com/saml2/ls/", saml2.BINDING_HTTP_REDIRECT),
                    ("https://my-admin.example.com/saml2/ls/post", saml2.BINDING_HTTP_POST),
                ],
            },
            "want_response_signed": False,
        },
    },
    "metadata": {
        "remote": [
            {"url": SAML_METADATA_URL},
        ],
    },
    # "debug": 1,
}

urls.pyには/saml2/のurlpatternsを追加。

urlpatterns = [
    # ...
    url(r'^saml2/', include("djangosaml2.urls")),
]

Saml2Backendの拡張

SAMLの認証用のバックエンドSaml2Backendをそのまま使ってログインすると、何の権限もないユーザーがDjangoに作成されるので、Saml2Backendを拡張しとりあえず必要な権限を付与したグループに新しいユーザーを所属させるようにしました。

from djangosaml2.backends import Saml2Backend

from django.contrib.auth.models import Group


class ModifiedSaml2Backend(Saml2Backend):
    def save_user(self, user, *args, **kwargs):
        user.save()
        user_group = Group.objects.get(name="default")
        user.groups.add(user_group)
        user.is_staff = True
        return super().save_user(user, *args, **kwargs)

以上の実装で https://my-admin.example.com/saml2/login からSAML認証でログインできるようになります。

ログイン画面の拡張

SAML認証でログインできるようにはなったのですが、このままだと既存のログイン画面からの導線がなく、URLを直接入力してログインしてもらう必要があります。 そこで、既存のログイン画面を拡張して「SAMLログイン」ボタンを追加しました。

Django adminのログインページをそのまま流用してテンプレートファイルを作成し、末尾に/saml2/loginに遷移するボタンのHTMLを追加します。

<!-- 
  Django adminのログインページと同じコード:
  https://github.com/django/django/blob/eafe1468d228e6f63d044f787a9ffec82ec22746/django/contrib/admin/templates/admin/login.html 
-->
<!-- (略) -->
</form>

<div class="submit-row">
  <input type="submit" value="SAML {% translate 'Log in' %}" onclick="location.href='/saml2/login'">
</div>

</div>
{% endblock %}

urlpatternsを修正し既存のログイン画面を新しいログイン画面で上書きします。

urlpatterns = [
    path(
        'admin/login/',
        auth_views.LoginView.as_view(
            template_name='login.html',
            extra_context={
                'title': _('Log in'),
                'site_header': admin.site.site_header,
            },
        ),
        name='login',
    ),
    # ...
]

若干無理矢理な実装ですが、既存のパスワードでのログイン方法からは移行しやすくなりました。

まとめ

DjangoSAML対応はなかなか情報がなく調査に苦労したのですが、現状は問題なく稼働しています。 ユーザーの属性ごとにグループや権限を分けるといった自動化はまだできていないのですが、それでもアカウント作成の手間は減らせました。

社内で利用するサービスではまだいくつかAzure ADによるSSOに対応できていない箇所があるので、同様にSSOの対応を進めていきたいところです。