DjangoでDigest認証をかける
django-digestというパッケージを使うとdjangoでdigest認証をかけることができます。
python-digestに依存してるので一緒にinstallします。
pip install django-digest python-digest
akoha / django-digest / wiki / Home ― Bitbucket
dimagi/python-digest · GitHub
設定
MIDDLEWARE_CLASSESにHttpDigestMiddlewareを追加
MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', 'django_digest.middleware.HttpDigestMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', )
INSTALLED_APPSにdjango-digestを追加
INSTALLED_APPS = ( 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.messages', 'django.contrib.staticfiles', 'django_digest', 'apps', )
digest認証を有効にする
DIGEST_REQUIRE_AUTHENTICATION = True
syncdbしてユーザーも作っておきます。
このユーザーでダイジェスト認証します。
Creating tables ... Creating table auth_permission Creating table auth_group_permissions Creating table auth_group Creating table auth_user_groups Creating table auth_user_user_permissions Creating table auth_user Creating table django_content_type Creating table django_session Creating table django_site Creating table django_digest_usernonce Creating table django_digest_partialdigest You just installed Django's auth system, which means you don't have any superusers defined. Would you like to create one now? (yes/no):
digest認証の流れ
1. クライアントが認証が必要なページにアクセスする。
DIGEST_REQUIRE_AUTHENTICATION =Trueを使う場合は全部のページが認証の対象になります。
他にデコレータでview関数ごとに設定する方法がありますが、追加でライブラリのインストールが必要な模様。
2. サーバは401レスポンスコードを返し、認証領域 (realm) や認証方式(Digest)に関する情報をクライアントに返す。このとき、ランダムな文字列(nonce)も返される。
この辺が該当のコードですね。
def build_challenge_response(self, stale=False): response = HttpResponse('Authorization Required', content_type='text/plain', status=401) opaque = ''.join([random.choice('0123456789ABCDEF') for x in range(32)]) response["WWW-Authenticate"] = python_digest.build_digest_challenge( time.time(), self.secret_key, self.realm, opaque, stale) return response
def build_digest_challenge(timestamp, secret, realm, opaque, stale): ''' Builds a Digest challenge that may be sent as the value of the 'WWW-Authenticate' header in a 401 or 403 response. 'opaque' may be any value - it will be returned by the client. 'timestamp' will be incorporated and signed in the nonce - it may be retrieved from the client's authentication request using get_nonce_timestamp() ''' nonce = calculate_nonce(timestamp, secret) return 'Digest %s' % format_parts(realm=realm, qop='auth', nonce=nonce, opaque=opaque, algorithm='MD5', stale=stale and 'true' or 'false')
def calculate_nonce(timestamp, secret, salt=None): ''' Generate a nonce using the provided timestamp, secret, and salt. If the salt is not provided, (and one should only be provided when validating a nonce) one will be generated randomly in order to ensure that two simultaneous requests do not generate identical nonces. ''' if not salt: salt = ''.join([random.choice('0123456789ABCDEF') for x in range(4)]) return "%s:%s:%s" % (timestamp, salt, md5.md5("%s:%s:%s" % (timestamp, salt, secret)).hexdigest())
レスポンスのWWW-Authenticateヘッダはこんな感じになります。
WWW-Authenticate: Digest nonce="1364103971.44:B096:ac48ab9b1a210f433ea749432c7177a8", realm="DJANGO", algorithm="MD5", opaque="24B604C6BC4B38EA54C8E3E974B1E028", qop="auth", stale="false"
構成要素を見やすくするとこうなります。
nonce="1364103971.44:B096:ac48ab9b1a210f433ea749432c7177a8" realm="DJANGO" algorithm="MD5" opaque="24B604C6BC4B38EA54C8E3E974B1E028" qop="auth" stale="false"
nonceは「number used once」の略で、リクエストごとに異なる値を返します。
opaqueは「不透明」という意味で、クライアントからは推測できない文字列ということです。
qopは「quality of protection」の略で、authかauth-initが入ります。
authの場合、メソッッドとURIからダイジェストを作成するのに対し、auth-initの場合はメッセージボディも利用します。
python-digestではauth固定になってます。
staleは「新鮮でない」とか、そういう意味ですね。
前回のリクエストでnonceが「新鮮でない」ために拒否された場合、trueになるようです。
参考 : RFC2617
Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESS plus)
- 作者: 山本陽平
- 出版社/メーカー: 技術評論社
- 発売日: 2010/04/08
- メディア: 単行本(ソフトカバー)
- 購入: 143人 クリック: 4,320回
- この商品を含むブログ (181件) を見る
3. 401を受けたクライアントは、ユーザ名とパスワードの入力を求めます。
4. ユーザによりユーザ名とパスワードが入力されると、クライアントはnonceとは別のランダムな文字列(cnonce)を生成する。そして、ユーザ名とパスワードとこれら2つのランダムな文字列などを使ってハッシュ文字列(response)を生成する。
5. クライアントはサーバから送られた認証に関する情報とともに、ユーザ名とresponseをサーバに送信する。
以下のような感じのAuthorizationヘッダが送信されます。
responseというのが、サーバーから送られたnonceとクライアントで生成されたcnonce、そしてuser/passなどから生成されたハッシュ値です。
Authorization: Digest username="user", realm="DJANGO", nonce="1364103998.63:0FA7:6414a5481e6a50ff169d59ef76601d2d", uri="/", algorithm=MD5, response="2a8e8b1423011f2b4317ab565d13c928", opaque="D2F4DE11BA31AC3BEAA9806C53D0F9F7", qop=auth, nc=00000001, cnonce="37db66bd20c031f3"
構成要素
username="user" realm="DJANGO" nonce="1364103998.63:0FA7:6414a5481e6a50ff169d59ef76601d2d" uri="/" algorithm=MD5 response="2a8e8b1423011f2b4317ab565d13c928" opaque="D2F4DE11BA31AC3BEAA9806C53D0F9F7" qop=auth nc=00000001 cnonce="37db66bd20c031f3"
6. サーバ側では、クライアントから送られてきたランダムな文字列(nonce、cnonce)などとサーバに格納されているハッシュ化されたパスワードから、正解のハッシュを計算する。
「ハッシュ可されたパスワード」はdjango-digestでは、Userのpost_saveで作成されるようになります。
django_digest_partialdigestテーブルのpartial_digestカラムに保存されます。
以下のロジックで生成されます。
md5.md5("%s:%s:%s" % (username.encode('utf-8'), realm, password.encode('utf-8'))).hexdigest()
7. この計算値とクライアントから送られてきたresponseとが一致する場合は、認証が成功し、サーバはコンテンツを返す。不一致の場合は再び401レスポンスコードが返され、それによりクライアントは再びユーザにユーザ名とパスワードの入力を求める。
5で送信されたパラメータとpartial_digestから、response(2a8e8b1423011f2b4317ab565d13c928)の値が生成できれば認証OKってことですね。
python_digestのコードから、その辺だけ抜粋して試してみます。
# coding=utf-8 import hashlib as md5 # request.methodで取得 method = "GET" # HTTPヘッダで渡される値 username = "user" realm = "DJANGO" nonce = "1364103998.63:0FA7:6414a5481e6a50ff169d59ef76601d2d" uri = "/" algorithm = "MD5" opaque = "D2F4DE11BA31AC3BEAA9806C53D0F9F7" qop = "auth" nc = 00000001 cnonce = "37db66bd20c031f3" response = "2a8e8b1423011f2b4317ab565d13c928" # dbから取得 partial_digest = "5acaf5417a098526606b13519e21d775" # 計算 ha2 = md5.md5("%s:%s" % (method, uri)).hexdigest() data = "%s:%s:%s:%s:%s" % (nonce, "%08x" % nc, cnonce, 'auth', ha2) kd = md5.md5("%s:%s" % (partial_digest, data)).hexdigest() print kd # => 2a8e8b1423011f2b4317ab565d13c928 print kd == response # => True
一致しました。
一致すれば認証OKになります。