前回はActiveDirectoryを使ったLDAPでの認証を行いました。
他に認証方法がないかを探してみたところ、過去にWindows Serverの機能であるネットワークポリシーサーバー(以下、NPS)のRADIUSを使って、無線LANやSSTP、L2TPなどの環境構築をしたことを思い出しました。
そこで今回は、DjangoでRADIUS認証を試してみることにしました。
環境
開発環境
- Windows7 x64
- Python 3.4.3
- Django 1.8.4
- pyrad 2.0.0
- IntelliJ IDEA 14.1.5
- Python plugin 4.5.3.141.2428
- PupSQLite 1.25.4.1
ActiveDirectory環境
- Windows Server 2008 R2 (ドメインコントローラとNPSを兼任)
- NPS機能を有効化 (RADIUSサーバとして動作)
- ユーザー情報
- ログイン名: fugafuga
- パスワード: fugafuga
- 所属グループ: RadiusUserGroup
事前調査
Pythonライブラリについて
PyPiや以下を参考にPython3でも動作しそうなライブラリを探してみました。
その中でpyrad
がPython3.4.3でも動作したため、このライブラリを使うことにしました。
wichert/pyrad - GitHub
認証方式について
オライリーのRADIUS本では、RADIUSではUser-Password方式とCHAP-Password方式が挙げられていました。
pyradライブラリのコードを見たところ、User-Password方式は実装されてましたが、CHAP-Password方式は未実装でした。
そこで、MS-CHAP部分をうまいことやるライブラリがないか探してみたところ、
- bit0rez/mschap-python - GitHub
- xjaner/mschap-python - GitHub
- slashmili/freeIBS - GitHubで実装が追加された pyrad.packet.py
あたりが見つかりました。
ただ、SyntaxError: invalid syntax
エラーが出るなど、いずれもそのままではPython3の環境では使えませんでした。
また、以下のRFCなどを読んで実装することも考えましたが、今回は時間がなかったため、CHAP-Password方式を試すのは諦めました。
- MS-CHAP - Wikipedia, the free encyclopedia
- RFC 2548 - Microsoft Vendor-specific RADIUS Attributes
- RFC 2759 - Microsoft PPP CHAP Extensions, Version 2
- RFC2865 ダイヤルインユーザーサービスの遠隔認証(ラディウス)
なお、MS-CHAP v2には脆弱性も見つかっているので、使うときには注意が必要です。
MS-CHAPv2プロトコルの破綻 | セキュリティ情報 | 株式会社ラック
他に、EAP over RADIUSまわりも調べようとしましたが、こちらも時間切れで調べきれませんでした。
- authentication - how and where are RADIUS and EAP combine? - Stack Overflow
- RADIUS Protocol Security and Best Practices - MSDN
- 802.1x 認証方式と NAP 対応 - Blogs - Windows Server 使い倒し塾 - Site Home - TechNet Blogs
これらを元に、今回はUser-Password方式のRADIUS認証を試すことにしました。
NPSの設定
今回はNPSをRADIUSサーバーとして使います。
そこで、Windows Serverにネットワークポリシーサーバー(NPS)の役割を追加をしている前提で、以下を参考に作業を行います。
- Windows Server 2008ターミナル・サービス・ゲートウェイ実践構築入門(後編) - @IT
- Windows Server 2012×「ちょっとだけ連携」でネットワーク管理を便利に(3):ネットワークデバイスの管理用パスワードを集中管理しよう(前編) (1/5) - @IT
- RADIUS クライアント - TechNet
- 接続要求ポリシー - TechNet
- ネットワーク ポリシーで使用するグループを作成する - TechNet
RADIUSクライアントの追加
NPSのRADIUSクライアントとして、Djangoアプリを追加します。
ネットワークポリシーとアクセスサービス > NPS (ローカル) > RADIUS クライアントとサーバー > RADIUSクライアント
を選択、一番右のペインで新規
をクリックして、以下の設定を行います。
項目 | 値 |
---|---|
フレンドリ名 | DjangoRadiusClient (任意の値) |
アドレス | DjangoのIPアドレス |
共有シークレット | a |
接続要求ポリシーの追加
DjangoアプリからRADIUSサーバへの接続要求を許可するため、接続要求ポリシーを新規作成します。
ネットワークポリシーとアクセスサービス > NPS (ローカル) > ポリシー > 接続要求ポリシー を選択し、一番右のペインの接続要求ポリシー
で新規
をクリックします。
今回はPAPでユーザー認証をすることから、PAPを使う時のプロトコルPPP
で接続要求があった時にのみ接続を許可します。
項目 | 値 |
---|---|
ポリシー名 | DjangoConnectionRequestPolicy (任意の値) |
条件 - 値 | FramedProtocol - PPP |
認証 | このサーバーで要求を認証する |
なお、IPアドレスで絞るときには正規表現も使えそうでした。
NPS で正規表現を使用する - TechNet
ネットワークポリシーの追加
接続時の認証に関するネットワークポリシーを追加します。
今回は、ActiveDirectoryのユーザーグループRadiusUserGroup
に属していれば認証できるようにします。そのため、事前に RadiusUserGroup をグローバル
でセキュリティ
なユーザーグループとして作成しておきます。
また、RADIUSのUser-Password方式でユーザーを認証するため、認証方法はPAP
のみを指定しておきます。
項目 | 値 |
---|---|
ポリシー名 | DjangoNetWorkPolicy (任意の値) |
アクセス許可 | アクセスを許可する。... |
条件 - 値 | ユーザーグループ - RadiusUserGroup |
認証方法 | 暗号化されていない認証(PAP、SPAP) のみチェックを入れる |
RADIUS属性 - Framed-Protocol | PPP |
RADIUS属性 - Service-Type | Framed |
ログファイルの設定
上記の手順までで接続できますが、RADIUS認証でエラーが発生した場合のエラーログを見やすくするため、以下を参考にログファイルの設定を変更します。
ログ ファイルのプロパティを構成する - TechNet
ネットワークポリシーとアクセスサービス > NPS (ローカル) > アカウンティング
を選択し、ログファイルプロパティの変更
をクリックします。
ログファイル
タブで形式
を選択できますが、XML形式だと項目名も分かることから、DTS標準
を選択します。
以上でWindows Server側の設定は完了です。
Djangoアプリの実装
Django向けのライブラリとしては、django-radiusがありましたが、今回もバックエンドを自作してみることにしました。
RADIUSクライアント向けのバックエンドを実装するにあたり、以下が参考になりました。ありがとうございました。
Django の認証を RADIUS で。 - Twisted Mind
また、その他にも悩んだところがあったため、まとめておきます。
Dictionaryについて
pyradではRADIUS辞書が必要になりますが、手元には適当な辞書がありませんでした。
Clientでdict
パラメータを指定せずにCreateAuthPacket()
を実行すると、 AttributeError: 'NoneType' object has no attribute 'attributes'
のエラーが発生します。
そこで、pyradのサンプルにあるdictionary
ファイル(pyrad/dictionary at master · wichert/pyrad)をコピーし、Djangoアプリのルートディレクトリ直下へと置きます。
次に、以下を参考に$INCLUDE
行を削除します。
[ 備忘録 /python ] pyrad のサンプルファイルを使うとエラーになる: Fomalhaut of Piscis Australis
あとはdictにdictionaryファイルを指定し、動作を確認します。
secretの指定について
pyrad.client.Client
を使う際、secret
パラメータへNPSとの共有シークレットを指定する必要があります。
ただ、pyradのREADMEのように secret="s3cr3t"
と指定したところ、pyrad.packet
の84行目あたりでTypeError: secret must be a binary string
エラーが発生しました。
コードを見たところ、Clientの__init__
の引数secret
がsecret=six.b('')
となっていたため、手元の実装コードでもsix.b()
を使うようにしました。
Six: Python 2 and 3 Compatibility Library — six 1.9.0 documentation
Framed-Protocol属性について
今回はNPSのポリシーでPPP接続に限っているため、Django側でFramed-Protocol属性を指定しない場合、RADIUS認証に失敗してNPSログにReason-Code値49が記録されます。
そのため、Django側で req['Framed-Protocol'] = 1
と指定します。
なお、今回はPPPのため、Framed-Protocol属性は1
となっています。
Radius Types - IANA
User-Password方式の場合のパスワードの暗号化
ライブラリのREADMEにも記載されていますが、pyrad.packet.AuthPacket.PwCrypt()
を使います。
最終的な実装コード
DjangoのAUTHENTICATION_BACKENDS
として指定するクラスは以下の通りとなります。
from django.contrib.auth import get_user_model from django.conf import settings from pyrad.client import Client from pyrad.dictionary import Dictionary from pyrad.packet import AccessRequest, AccessAccept import six import os class RadiusPAPBackend(object): def authenticate(self, username=None, password=None): srv = Client( server=settings.AD_NPS_HOST_NAME, secret=six.b(settings.AD_NPS_SHARED_SECRETS), dict=Dictionary(os.path.join(settings.BASE_DIR, "dictionary")), ) req = srv.CreateAuthPacket( code=AccessRequest, User_Name=username, ) req['Framed-Protocol'] = 1 req['User-Password'] = req.PwCrypt(password) try: reply = srv.SendPacket(req) if reply.code == AccessAccept: user = get_user_model() result, created = user.objects.update_or_create( username = username, password = password ) return result else: return None except: return None def get_user(self, user_id): user = get_user_model() try: return user.objects.get(pk=user_id) except: return None
その他の部分の実装
- 上記のRadiusPAPBackendクラスを
settings.py
にあるAUTHENTICATION_BACKENDS
へ設定 - それ以外のコードは前回とほぼ同じ流れで実装
を実装したところ、ログイン/ログアウトが正常に動作しました。
トラブルシューティング
うまく動作しない時に自分が確認したものをまとめます。
トラブルシューティング時に確認した場所は、
- DjangoアプリでのSendPacket()の戻り値である
pyrad.packet.Packet.code
- NPSのログのReason-Code
です。
なお、NPSログで使われるタグ、およびReason-Codeは以下を参考にしています。
- Interpret NPS Database Format Log Files - TechNet
- NPS 理由コード 0 ~ 37 - TechNet
- NPS の理由コード 38 257 - TechNet
NPSとDjangoアプリでsecretが異なる場合
- Djangoアプリ
- SendPacket()後に、
pyrad.client.Timeout
のエラーが発生
- SendPacket()後に、
- NPSのログ
- Reason-Codeが
16
- Reason-Codeが
16の場合、「認証は、ユーザーの資格情報の不一致により失敗しました。 指定されたユーザー名は、既存のユーザー アカウントと一致しないか、パスワードが間違っています」のようです。
NPSの接続要求ポリシーに合致しない場合
- Djangoアプリ
- pyrad.packet.Packet.codeが
3
(AccessReject)
- pyrad.packet.Packet.codeが
- NPSのログ
Proxy-Policy-Name
タグが存在しない- Reason-Codeが
49
49の場合、「ネットワーク ポリシー サーバーで、接続要求が拒否されましたので、構成された接続要求ポリシー、接続要求が一致しません」のようです。
NPSのネットワークポリシーに合致しない場合
- Djangoアプリ
- pyrad.packet.Packet.codeが
3
(AccessReject)
- pyrad.packet.Packet.codeが
- NPSのログ
- Reason-Codeが
48
- Reason-Codeが
48の場合、「ネットワーク ポリシー サーバーで、接続要求が拒否されましたので、接続要求が構成されているネットワーク ポリシーが一致しません」のようです。
なお、ネットワークポリシーがうまく作成されていない場合、そのポリシーがスルーされ、デフォルトのConnections to other access servers
ポリシーに合致して、接続が拒否されることもありました。
その場合のReason-Codeは65
(ネットワークへのアクセス許可 ネットワークへのアクセス許可 の設定、ユーザー アカウントのダイヤルイン プロパティへのアクセスを拒否]が設定されます) でした。
Access Request Was Denied - MSDN
Djangoアプリで入力したユーザのパスワードが違う場合
- Djangoアプリ
- pyrad.packet.Packet.codeが
3
(AccessReject)
- pyrad.packet.Packet.codeが
- NPSのログ
- Reason-Codeが
16
- secretが異なる時と同じ
- Reason-Codeが
ソースコード
GitHubに上げておきました。
thinkAmi-sandbox/Django_AD_Radius_Sample
参考
RADIUSについては以下の本がとても参考になりました。
- 作者: Jonathan Hassell,アクセンス・テクノロジー
- 出版社/メーカー: オライリー・ジャパン
- 発売日: 2003/12
- メディア: 単行本
- 購入: 1人 クリック: 36回
- この商品を含むブログ (12件) を見る