DjangoアプリにLDAP認証を組み込む

Django+Jinja2を用いて開発しているWebアプリにLDAP認証を組み込むのに四苦八苦したのでメモ.

なお,この記事を書くにあたり作成したサンプルアプリをGitHubで公開しているので,そちらも参考にしてほしい.

ldap3のインストール

PythonでLDAPを使うために,以下のコマンドでldap3をインストールする.

$ pip install ldap3

ldapアプリケーションの作成

次に,LDAP認証を受け持つDjangoアプリケーションを以下のコマンドで作成する.

$ python manage.py startapp ldap

作成したアプリの中に,後述するLDAP認証機能を組み込んでいく.

LDAP認証バックエンドの作成

Django上でLDAP認証を行うための認証バックエンドを,Djangoのドキュメントを参考にbackend.pyという名称で作成し,settings.pyで認証バックエンドとして登録する.

LDAPで使う定数の設定

backend.pyを作成する前に,まずはLDAP接続時に使うサーバアドレスなどの情報をあらかじめsettings.pyに定数として設定しておく.ただし,プロジェクトをGitなどでバージョン管理している場合,このような繊細な情報はsettings.pyに直に書くべきではない.local_settings.pyを作成してここにLDAPサーバアドレスなどを書き,settings.pylocal_settings.pyをインポートしてデータを読み込むようにする.

まず,local_settings.pyproject_root/project/の下に作成し,以下のように記述する.

LDAP_SERVER_ADDRESS = 'YOUR LDAP SERVER ADDRESS HERE'  
LDAP_SERVER_PORT = YOUR LDAP SERVER PORT HERE  
LDAP_USER_BASEDN = 'YOUR LDAP SERVER USER BASEDN HERE'  

次に,settings.pyのimport部分に以下のものを追加する.

from project import local_settings  

そして,settings.pyの末尾に以下のものを追加する.

LDAP_SERVER_ADDRESS = local_settings.LDAP_SERVER_ADDRESS  
LDAP_SERVER_PORT = local_settings.LDAP_SERVER_PORT  
LDAP_USER_BASEDN = local_settings.LDAP_USER_BASEDN  

settings.pyにこれらの定数を設定することで,backend.pyからLDAP接続に必要な情報を簡単に読み込むことができる.

backend.pyの作成

まずは,LDAP認証のミソとなるLDAP認証バックエンドをproject_root/ldap/backend.pyで以下のように作成する.

from django.contrib.auth import get_user_model  
from django.contrib.auth.models import User  
from django.conf import settings  
from ldap3 import Server, Connection, AUTH_SIMPLE, STRATEGY_SYNC, LDAPBindError


class LDAPBackend(object):  
    def authenticate(self, username=None, password=None):
        s = Server(settings.LDAP_SERVER_ADDRESS,
                   port=settings.LDAP_SERVER_PORT)

        user_dn = 'uid='+username+',' + settings.LDAP_USER_BASEDN

        try:
            c = Connection(s,
                           user=user_dn,
                           password=password,
                           authentication=AUTH_SIMPLE,
                           check_names=True,
                           client_strategy=STRATEGY_SYNC,
                           auto_bind=True)
            user = get_user_model()

            result, created = user.objects.update_or_create(
                username=username,
                password=password
            )

            c.unbind()
            return result
        except LDAPBindError:
            return None

    def get_user(self, user_id):
        user = get_user_model()
        try:
            return user.objects.get(pk=user_id)
        except User.DoesNotExist:
            return None

認証バックエンドにLDAP認証を設定

backend.pyが作成できたら,settings.pyを編集してこのLDAP認証バックエンドをDjangoの認証バックエンドに組み込む. 具体的には,settings.pyに以下の内容を追加する.

AUTHENTICATION_BACKENDS = (  
    'ldap.backend.LDAPBackend',
)

テンプレートの作成

LDAP認証に使う認証バックエンドの作成はできた.次は認証時に表示させるWebページのテンプレートを作成し,実際に認証できるようにする.

また,ログイン完了後に自動的に別のページにリダイレクトする設定と,ログインが必要なページにログインせずにアクセスした場合に自動的にログインページにリダイレクトする設定を行う.

settings.pyでテンプレートディレクトリの追加

テンプレートファイルを作成する前に,テンプレートを保存するディレクトリを作成し,settings.pyに作成したディレクトリをテンプレートディレクトリとして設定する.

まず,project_root/直下とproject_root/ldap/の下にtemplatesディレクトリを作成する.作成できたら,settings.pyTEMPLATES内にあるDIRSに以下のものを追加する.

os.path.join(BASE_DIR, 'templates'),  
os.path.join(BASE_DIR, 'ldap/templates'),  

テンプレートファイルの作成

テンプレートディレクトリの作成と設定ができたら,テンプレートの作成を行う.まずproject_root/templates/の下にこれ以降作成するテンプレートのベースとなるbase.jinjaを以下のような内容で作成する.

<!DOCTYPE html>  
<html lang="en">  
<head>  
  <meta charset="utf-8">
  <title>django_ldap</title>
</head>  
<body>  
  {% block content %}
  {% endblock %}
</body>  
</html>  

次に,ログインページのテンプレートファイルとなるlogin.jinjaproject_root/ldap/templates/の下に以下のような内容で作成する.

{% extends "base.jinja" %}
{% block content %}

  <h1>Login page</h1>
  {% if form.errors %}
    <p>Your username and/or password didn't match.</p>
  {% endif %}

  {% if user is defined %}
    {% if user.is_authenticated %}
      Current User: {{ user.username }}
    {% endif %}
  {% endif %}

  <form method="post" action="{{ url('login') }}">
    <input type="hidden" name="csrfmiddlewaretoken" value="{{ 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="/sample">Sample page</a>
{% endblock %}

また,ログアウトページのテンプレートファイルとなるlogged_out.jinjaを先ほどと同じくproject_root/ldap/templates/の下に以下のような内容で作成する.

{% extends "base.jinja" %}

{% block content %}
  <h1>Logout page</h1>

  {{ title }}

  <p><a href="/sample">Sample page</a></p>
{% endblock %}

urlsファイルの修正

テンプレートファイルを作成できたら,project_root/project/urls.pyを修正してログインページ及びログアウトページにアクセスできるようにする.

まず,以下の内容を追加してDjango付属の認証ページ用Viewsをインポートする.

from django.contrib.auth import views as auth_views  

そして,以下の内容をurlpatternsの中に追加する.

url(r'^login/$', auth_views.login,  
    {'template_name': 'login.jinja'}, name='login'),
url(r'^logout/$', auth_views.logout,  
    {'template_name': 'logged_out.jinja'}, name='logout'),

settings.pyでリダイレクトURLの指定

ログイン完了後のリダイレクト先URLと,ログインが必要なページにログインせずにアクセスした場合のリダイレクトURLをsettings.pyに設定する.具体的には,以下の内容をsettings.pyに追加する.

LOGIN_REDIRECT_URL = '/sample/'  
LOGIN_URL = '/login/'  

特定ページへのLDAP認証によるアクセス制限

あるページへアクセスするのにログインを必須とするような設定をしたい場合があると思う.その場合はまず,そのページを設定しているviews.pyファイルに以下のモジュールをインポートする.

from django.contrib.auth.decorators import login_required  

そして,そのページのメソッドの上に@login_requiredを追加する.

from django.contrib.auth.decorators import login_required


@login_required
def index(request):  
    return render_to_response('index.jinja', ...)

このデコレータを追加することにより,そのメソッドへのアクセスにはログインが必須となり,ログインしていない場合は先ほどsettings.pyで設定したログインURLにリダイレクトされる.


Appendix

Webページ上にログイン中のユーザ名を表示したい場合は,まずviews.py内のメソッドの引数としてあるrequest変数の中にあるuserを,render_to_responsecontextとして辞書形式で渡す.

@login_required
def index(request):  
    return render_to_response(template_name='index.jinja',
        context={'user': request.user})

そして,ユーザ名を表示するテンプレートファイルで,以下の内容を追加する.

{% if user.is_authenticated %}
    <p>Current User: {{ user.username }}</p>
{% endif %}

これで,ログイン中のユーザ名が表示されるはずだ.


参考