DjangoでActiveDirectoryを使ったLDAP認証を試してみたので、その時に悩んだことや実装内容をメモしておきます。
環境
開発環境
- Windows7 x64
- Python 3.4.3
- Django 1.8.4
- LDAP3 0.9.9
- IntelliJ IDEA 14.1.4
- Python plugin 4.5 141.1624
- PupSQLite 1.25.4.1
ActiveDirectory環境
- Windows Server 2008 R2 (ドメインコントローラ)
- ドメイン名: example.local
- ユーザー情報
- ログイン名: fugafuga
- パスワード: fugafuga
事前調査
DjangoのパッケージリストのLDAP
カテゴリを見たところ、Djangoで使えそうなライブラリがいくつかあったため、それらを試してみることにしました。
Django Packages : LDAP
django-auth-ldap
公式サイト: psagers / django-auth-ldap / wiki / Home — Bitbucket
ChangeLogを見ると、django-auth-ldap
自体はPython3に対応していました。
Change Log — django-auth-ldap 1.2.6 documentation
ドキュメントのトップには、依存しているpython-ldap
がPython3に非対応とありました。ただ、併記する形でPython3対応されたforkの記載もありました。
Django Authentication Using LDAP — django-auth-ldap 1.2.6 documentation
そこで、forkをインストールしてみましたが、エラーとなりました。
# インストール pip install git+https://github.com/rbarrois/python-ldap.git@py3 # エラー ... File "path\to\virtualenv\lib\site-packages\pip\util.py", line 53, in rmtree_errorhandler (exctype is PermissionError and value.args[3] == 5) #python3.3 IndexError: tuple index out of range
そのため、django-auth-ldap
を使うのは諦めました。
django-python3-ldap
公式サイト: etianen/django-python3-ldap
Python3で動作するパッケージLDAP3
を使っていたため期待が持てそうでした。
ただ、以下のIssueにてブランチactive-directory-bind-support
がある旨が記載されていたものの、今のところ本体にはマージされていなかったため、今回は使うのを諦めました。
ldap_sync_users completes successfully, still can't login · Issue #12 · etianen/django-python3-ldap
2015/11/21 追記
現在では上記IssueはCloseされ、ActiveDirectoryでも動作するようになりました。
現時点のREADMEにもその旨と設定内容が記載されています。
2015/11/21 追記 ここまで
django-auth-ldap-ad
公式サイト: susundberg/django-auth-ldap-ad
ActiveDirectoryで動きそうでしたが、内部でpython-ldap
を使っていたため、使うのを諦めました。
自作バックエンド
上記より、現時点では既存のライブラリを使ってPython3で進めるのは厳しそうでしたので、自作バックエンドを作成する方向で作業を進めることにしました。
自分で作る方法を調べてみたところ、以下のQiitaがとても参考になりました。ありがとうございました。なお、記事にはpython3-ldap
とありますが、現在はldap3
へと改名されたようです。
python3-ldapを触ってみた - Qiita
アプリ作成
事前準備
- Project Encodingを
utf-8
にして、IntelliJ IDEAを再起動 <root>\apps\ad_ldap3
フォルダを作成startapp ad_ldap3 ./apps/ad_ldap3
INSTALLED_APPS
に'apps.ad_ldap3',
を追加- Pythonパッケージ
ldap3
をインストール
テンプレートに関するsettings.py
の修正
2015/09/26 追記
2015/9/26にIntelliJ IDEAやPython pluginをアップデートし、
の環境でプロジェクトを新規作成してみました。
新規プロジェクトのsettings.py
は
TEMPLATE_DIRS
が存在しないTEMPLATES
のDIRS
にos.path.join(BASE_DIR, 'templates')
が追加
と改善され、以下の作業が不要になりました。
2015/09/26 追記 ここまで
IntelliJ IDEA 14.1.4のPython plugin 4.5 141.1624では、settings.py
の末尾あたりに
TEMPLATE_DIRS = (
os.path.join(BASE_DIR, 'templates'),
)
の記載があります。
このままにしておくと、Django実行時に
WARNINGS: ?: (1_8.W001) The standalone TEMPLATE_* settings were deprecated in Django 1.8 and the TEMPLATES dictionary takes precedence. You must put the values of the following settings into your default TEMPLATES dict: TEMPLATE_DIRS.
というエラーメッセージが表示され、実行時にテンプレートフォルダが認識されませんでした。
ドキュメントを読むとDjango1.8ではDeprecated になっていました。
Settings - #template-dirs | Django documentation | Django
そのため、TEMPLATE_DIRS
を削除するとともに、TEMPLATES
のDIRS
にTEMPLATE_DIRS
の内容を追加します。
TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [os.path.join(BASE_DIR, 'templates'),], # ここを追加 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ]
認証バックエンドまわり
認証バックエンドの実装
以下を参考に、authenticate()
とget_user()
を持つADBackendクラスを作成します。
Customizing authentication in Django - #writing-an-authentication-backend | Django documentation | Django
ADBackendクラスでは以下を実装します。
LDAP3
を利用したActiveDIrectoryでの認証Connection
でauto_bind=True
として、接続と同時にバインドし、例外がでなければ認証成功とみなす
- Djangoの認証ユーザを登録する
User
モデルに、ActiveDirectoryより取得したデータを保存- Userモデルの取得には、
django.contrib.auth.get_user_model()
を仕様
- Userモデルの取得には、
- ドメインコントローラ名などは
settings.py
に記載- 以下を参考に
from django.conf import settings
としてsettings.py
を読み込み
- 以下を参考に
実装ソースコードは以下となります。
# <root>\apps\ad_ldap3\backend.py from django.contrib.auth import get_user_model from django.conf import settings from ldap3 import Server, Connection, NTLM class ADBackend(object): def authenticate(self, username=None, password=None): server = Server(settings.AD_DOAMIN_CONTROLLER_HOST_NAME) try: # auto_bindありのConnectionの生成で例外が発生しなければ、認証成功とみなす c = Connection(server, user="{0}\\{1}".format(settings.AD_DOMAIN_NAME, username), password=password, authentication=NTLM, auto_bind=True) user = get_user_model() result, created = user.objects.update_or_create( username = username, password = password ) c.unbind() return result except: return None def get_user(self, user_id): user = get_user_model() try: return user.objects.get(pk=user_id) except: return None
settings.pyへの追記
AUTHENTICATION_BACKENDSの設定
認証バックエンドとして自作したADBackendを使うため、追加しておきます。
AUTHENTICATION_BACKENDS = (
'apps.ad_ldap3.backend.ADBackend',
)
ドメインコントローラ・ドメイン情報の設定
自作の定数AD_DOAMIN_CONTROLLER_HOST_NAME
とAD_DOMAIN_NAME
をADBackendクラスにて参照するため、追加しておきます。
AD_DOAMIN_CONTROLLER_HOST_NAME = 'your_dc_host_name' AD_DOMAIN_NAME = 'example.local'
Viewまわりの実装
認証バックエンドまわりはできたので、今度はViewまわりを作成します。
今回必要なViewは、
- indexページ
- ログインとログアウトへのリンクあり
- 現在のログインユーザ名やセッション情報を表示
- loginページ
- ユーザ名とパスワードを入力してログインするページ
- ログインに成功したら、indexページへとリダイレクト
- logoutページ
- ログアウトするページ
の3つとなります。
loginページの作成
loginページ用にdjango.contrib.auth.views.login
という便利なものがあったため、それを利用します。
そのため、<root>\apps\ad_ldap3\views.py
への追加は不要になります。
urls.py
ルーティング設定のため、<root>\Django_AD_Ldap3_Sample\urls.py
に以下を追加します。
urlpatterns = [ url(r'^admin/', include(admin.site.urls)), url(r'^login/$', 'django.contrib.auth.views.login'), ]
login.html
django.contrib.auth.views.login
では、<root>\templates\registration\login.html
をデフォルトのファイルとして使用しますので、そのファイルを作成します。
なお、form
など、login.htmlにtemplate contextとして渡される変数は、以下にまとまっていました。
Using the Django authentication system - #django.contrib.auth.views.login | Django documentation | Django
{% extends "base.html" %} {% block content %} <h1>login page</h1> {% if form.errors %} <p>Your username and password didn't match. Please try again.</p> {% endif %} {% if user.is_authenticated %} Current User: {{ user.username }} {% endif %} <form method="post" action="{% url 'django.contrib.auth.views.login' %}"> {% csrf_token %} <table> <tr> <td>{{ form.username.label_tag }}</td> <td>{{ form.username }}</td> </tr> <tr> <td>{{ form.password.label_tag }}</td> <td>{{ form.password }}</td> </tr> </table> <input type="submit" value="login" /> <input type="hidden" name="next" value="{{ next }}" /> </form> <a href="/">index</a> {% endblock %}
base.html
login.html
はbase.html
をextendsしているため、<root>\templates\base.html
も用意します。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title></title> </head> <body> {% block content %} {% endblock %} </body> </html>
LOGIN_REDIRECT_URLの設定
ログイン成功時のリダイレクト先URLは、デフォルトでは/accounts/profile/
となります。
ただ、今回は/
を使用するため、settings.py
に設定を追加します。
LOGIN_REDIRECT_URL = '/'
logoutページの作成
こちらも、django.contrib.auth.views.logout
が用意されています。
Using the Django authentication system - #django.contrib.auth.views.logout | Django documentation | Django
loginページとほぼ同様の作りとなるため、以下に箇条書きでまとめます。
urls.py
に、url(r'^logout/$', 'django.contrib.auth.views.logout'),
を追加<root>\templates\registration\logged_out.html
がデフォルトのログアウト後ページなので、以下の内容で作成
{% extends "base.html" %} {% block content %} <h1>logout page</h1> {{ title }} <p><a href="/">index</a></p> {% endblock %}
indexページの作成
デフォルトのindexページは用意されていないため、自分で用意します。
views.pyへの追加
現在のセッション状態をindexページに表示させたいため、以下を参考にしてセッションの内容をHTMLテンプレート側に渡します。
- Djangoのセッション管理について - brainstorm
- How to use sessions - #using-sessions-out-of-views | Django documentation | Django
# <root>\apps\ad_ldap3\views.py from django.shortcuts import render_to_response from django.template.context import RequestContext from django.contrib.sessions.models import Session def index(request): session = Session.objects.all().first() return render_to_response( 'index.html', {'session_info': None if session is None else session.get_decoded()}, context_instance=RequestContext(request) )
urls.pyへの追加
indexページのルーティングとして、url(r'^$', 'apps.ad_ldap3.views.index'),
を追加します。
HTMLテンプレートの作成
<root>\templates\index.html
として、テンプレートを作成します。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title></title> </head> <body> <h1>index page</h1> {# ユーザ認証されていれば、ユーザ名を表示 #} {% if user.is_authenticated %} <p>Current User:{{ user.username }}</p> {% endif %} {# セッション情報の中身を表示 #} {% if session_info %} <p>session_info: {{ session_info }}</p> {% else %} <p>no session.</p> {% endif %} <div> <a href="/login">login</a> <a href="/logout">logout</a> </div> </body> </html>
manage.pyによる、マイグレーションの実行
念のためmakemigrations
を実行した後、migrate
を実行します。
ここまでで実装は完了です。
動作確認
以下の流れで動作を確認します。
1. Indexを表示
この時点ではCookieへの設定はありません。
また、PupSQLiteでDBを確認しますが、django_session
にはエントリがありません。
2. ログインページへ移動
Cookieに変化はありません。
3. ログイン後にIndexページへリダイレクト
Cookieにセッション情報が登録されています。また、バックエンドから取得したUserオブジェクトの内容も表示できています。よく見ると、Cookieのcsrftoken
も変更されています。
また、DBにもセッション情報が追加されています。
他に、ログインに使用したユーザも登録されています。
4. ログアウトページヘ移動
CookieやDBにセッション情報がなくなりました。
5. indexページへ移動
バックエンドからUserオブジェクトの情報は取得できなくなりました。
ソースコード
今回使用したソースコードは、GitHubに上げておきました。
thinkAmi-sandbox/Django_AD_Ldap3_Sample
2015/11/21追記:自作バックエンドをパッケージ化しました
pipでインストールできるよう、GitHubにて公開しています。
thinkAmi/django-auth-ldap3-ad-backend
上記のサンプルコードはtest_project
へと移動していますが、READMEのようにして設定すれば、同じように使えると思います。