WSGIでダイジェスト認証

DigestAuth Middlewareの紹介

tags:python, web, tips
created:2008-04-11T04:18:44

ダイジェスト認証のほうが安全に認証できるらしいので、 WSGIでダイジェスト認証をするミドルウェアを作成してみました。

ダイジェスト認証のほうが安全に認証できるらしいので、 WSGIでダイジェスト認証をするミドルウェアを作成してみました。

概要

ダイジェスト認証というものがほとんどのブラウザで利用可能らしいので、 ガンガン使えるようにミドルウェアにしてみました。

ダイジェスト認証の特徴

ベーシック認証に比べると、

  • パスワードをハッシュキー化して送る。
  • ハッシュキーにはランダムキーが含まれる。
  • トライ毎にインクリメントするコードを含む。

といった点からより安全に認証できるという利点があります。

ただ、クライアント側から見ると

  • 必ず一度認証接続失敗を受け取らなければならないので2度手間がかかる。

というデメリットもあります。

ただ、多くのブラウザはサポートしているのでちゃんと ユーザーからIDとパスワードのダイアログを表示して、 その2度手間を実行してくれます。

ソースコード

wsgiauth.py
#!/usr/local/bin/python
# -*- encoding: utf-8 -*-

import sys
import os
import traceback
import webob

import md5
import random
from ConfigParser import ConfigParser, NoOptionError

class DigestAuthMiddleware:
  def __init__(self, app, filename=u'users.conf', realm='admin-page'):
    self.app = app
    self.filename = filename
    self.realm = realm
    self.users = ConfigParser()
    try:
      assert self.users.read(filename)
    except:
      print 'read error :', filename
      self.add_user('admin', 'admin')

  def add_user(self, username, password, realm=None):
    if realm==None:
      realm = self.realm
    a1 = md5.new('%(username)s:%(realm)s:%(password)s' % locals()).hexdigest()
    if not self.users.has_section(realm):
      self.users.add_section(realm)
    self.users.set(realm, username, a1)
    self.users.write(open(self.filename, 'w'))

  def del_user(self, username, realm=None):
    if realm==None:
      realm = self.realm
    self.users.remove_option(realm, username)
    self.users.write(open(self.filename, 'w'))

  def make_auth_header(self):
    values = []
    values.append('realm="%s"' % self.realm)
    chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
    salt = ''.join([random.choice(chars) for i in range(11)])
    nonce = '%s=%s' % (salt, md5.new(salt).hexdigest())
    values.append('nonce="%s"' % nonce)
    values.append('algorithm=MD5')
    values.append('qop="auth"')
    return ('WWW-Authenticate', 'Digest ' + ', '.join(values))

  def check(self, environ):
    req = webob.Request(environ)
    authorization = req.headers.get('Authorization', '')
    if authorization:
      try:
        authtype, valstr = authorization.strip().split(' ', 1)
        assert authtype.strip().lower()=='digest'
        values = dict([
          [j.strip().strip('"') for j in i.split('=', 1)]
          for i in valstr.split(',')])
        values['method'] = req.method
        values['uri'] = req.path_info
        values['a1'] = self.users.get(values['realm'], values['username'])
        values['a2'] = md5.new('%(method)s:%(uri)s' % values).hexdigest()
        response = md5.new('%(a1)s:%(nonce)s:%(nc)s:%(cnonce)s:%(qop)s:%(a2)s' % values).hexdigest()
        return values['response']==response
      except NoOptionError:
        pass
    return False

  def __call__(self, environ, start_response):
    try:
      assert self.check(environ)
      return self.app(environ, start_response)
    except AssertionError:
      headers = [self.make_auth_header()]
      headers.append(('Content-Type', 'text/html; charset=utf-8'))
      start_response('401 Authorization Required', headers)
      return 'Authorization Required.'
    except:
      headers = []
      headers.append(('Content-Type', 'text/html; charset=utf-8'))
      start_response('500 Internal Server Error', headers)
      return ''.join([
        '<pre>',
        ''.join(traceback.format_exception(*sys.exc_info())),
        '</pre>'
      ])

def __main():
  def application(environ, start_response):
    headers = []
    headers.append(('Content-Type', 'text/html; charset=utf-8'))
    start_response('200 OK', headers)
    return 'hello!'
  from wsgiref.simple_server import make_server
  app = DigestAuthMiddleware(application)
  app.add_user('admin', 'hogefoo')
  httpd = make_server('', 8000, app)
  httpd.serve_forever()

if __name__=='__main__':
  __main()

使い方

import wsgiauth
from wsgiref.simple_server import make_server

application = あなたのアプリ

authapp = DigestAuthMiddleware(application)
authapp.add_user('admin1', 'hogefoo')
authapp.add_user('admin2', 'hogege')
httpd = make_server('', 8000, authapp)
httpd.serve_forever()

add_userを呼び出すごとに 「users.conf」というファイルにユーザーアカウント情報が保存されます。

users.confの中身はiniファイル形式になっていますが、 パスワードは認証ハッシュキー化されていますので手書きではアカウントは作れません。

まとめ

最近WSGIが面白くていろいろ実験してます。

このサイトの動的な部分はほとんどWSGIを利用して構築しています。 簡易データストレージや、XML-RPC&ダイジェスト認証などをつかった管理、 ローカルでテストでき、そのままレンタルサーバにデプロイすることも出来ます。 ローカルでテストできるようにするため、CGIラッパーミドルウェアも作りました・・・。 これらの仕組みと今話題の Google App Engine がそっくりすぎてびっくり!! なんてこったい! 当然スケーラビリティは Google App Engine の方が圧倒的に高いじゃん!

でも、Mercurialをつかったデプロイが出来るので乗り換えるほどのメリットはないですね。 (このサイトでそんなにトラフィックがあるわけでもないので)

このダイジェスト認証、WSGIミドルウェアになっていますので、 Google App Engine などでも使えるはずですよ~