Skip to content
Browse files

added HTTP digest authorization filter

  • Loading branch information...
1 parent a767138 commit a23a81d6fad0461384d1f7d2758e56c9741a84a6 @rsms committed Jul 25, 2009
Showing with 183 additions and 41 deletions.
  1. +65 −41 docs/source/library/smisk.mvc.filters.rst
  2. +118 −0 lib/smisk/mvc/filters.py
View
106 docs/source/library/smisk.mvc.filters.rst
@@ -6,44 +6,68 @@ filters
.. function:: confirm(leaf, *va, **params)
-
- Requires the client to resend the request, passing a one-time valid token
- as confirmation.
-
- Used like this::
-
- @confirm
- def delete(self, id, confirmed, *args, **kwargs):
- item = Item.get_by(id=id)
- if confirmed:
- item.delete()
- return {'msg': 'Item was successfully deleted'}
- else:
- return {'msg': 'To confirm deletion, make a new request and '\
- 'include the attached confirm_token'}
-
- Generates a random string which is stored in session with the key
- ``confirm_token`` and adds the same string to the response, keyed by
- ``confirm_token``. The client needs to send the same request again
- with the addition of passing "confirm_token", as a confirmation. This
- token will only be valid for one confirmation, thus providing a good
- protection against accidents.
-
- The leaf being filtered by these filters receives a boolean keyword
- argument named ``confirmed``:
-
- * When the value of this argument is True, the client did confirm (client
- sent a request containing a valid token). In this case, you should perform
- whatever leaf needed to be confirmed.
-
- * When the value of ``confirmed`` is false, the client has not confirmed or
- tried to confirm with an invalid token. In this case, you should respond
- with some kind of information, telling the client to send a new request
- with the attached token.
-
- **Note:** This filter will force the session to be a dictionary. If session is
- something else, this filter will replace session::
-
- if not isinstance(req.session, dict):
- req.session = {}
-
+
+ Requires the client to resend the request, passing a one-time valid token
+ as confirmation.
+
+ Used like this::
+
+ @confirm
+ def delete(self, id, confirmed, *args, **kwargs):
+ item = Item.get_by(id=id)
+ if confirmed:
+ item.delete()
+ return {'msg': 'Item was successfully deleted'}
+ else:
+ return {'msg': 'To confirm deletion, make a new request and '\
+ 'include the attached confirm_token'}
+
+ Generates a random string which is stored in session with the key
+ ``confirm_token`` and adds the same string to the response, keyed by
+ ``confirm_token``. The client needs to send the same request again
+ with the addition of passing "confirm_token", as a confirmation. This
+ token will only be valid for one confirmation, thus providing a good
+ protection against accidents.
+
+ The leaf being filtered by these filters receives a boolean keyword
+ argument named ``confirmed``:
+
+ * When the value of this argument is True, the client did confirm (client
+ sent a request containing a valid token). In this case, you should perform
+ whatever leaf needed to be confirmed.
+
+ * When the value of ``confirmed`` is false, the client has not confirmed or
+ tried to confirm with an invalid token. In this case, you should respond
+ with some kind of information, telling the client to send a new request
+ with the attached token.
+
+ **Note:** This filter will force the session to be a dictionary. If session is
+ something else, this filter will replace session::
+
+ if not isinstance(req.session, dict):
+ req.session = {}
+
+
+
+.. class:: DigestAuthFilter(object)
+
+ HTTP Digest authorization filter.
+
+ Used like this::
+
+ authenticate = DigestAuthFilter('Protected', {'username': 'password'})
+ check_authenticated = DigestAuthFilter('Protected', {'username': 'password'}, False)
+
+ class root(Controller):
+ @authenticate
+ def only_for_users(self, authorized_user):
+ # do something...
+
+ @check_authenticated
+ def for_everyone(self, authorized_user):
+ if authorized_user:
+ # do something only authorized users can do
+ else:
+ # do something unauthorized users can do
+
+ .. versionadded:: 1.1.7
View
118 lib/smisk/mvc/filters.py
@@ -2,7 +2,13 @@
'''Leaf filters
'''
import smisk.core
+import smisk.mvc.http as http
from smisk.mvc.decorators import leaf_filter
+from time import time
+try:
+ from hashlib import md5
+except ImportError:
+ from md5 import md5
__all__ = ['confirm']
@@ -48,3 +54,115 @@ def confirm(leaf, *va, **params):
# Return response
return rsp
+
+class DigestAuthFilter(object):
+ '''HTTP Digest authorization filter.
+ '''
+ required = ['username', 'realm', 'nonce', 'uri', 'response']
+ users = {}
+
+ def __init__(self, realm, users=None, require_authentication=True):
+ self.realm = realm
+ if users is not None:
+ self.users = users
+ self.require_authentication = require_authentication
+ self.leaf = None
+ self.app = smisk.core.Application.current
+
+ def respond_unauthorized(self, send401=True, *va, **kw):
+ if not send401:
+ kw['authorized_user'] = None
+ return self.leaf(*va, **kw)
+ # send response
+ self.app.response.headers.append(
+ 'WWW-Authenticate: Digest realm="%s", nonce="%s", algorithm="MD5", qop="auth"'
+ % (self.realm, self.create_nonce())
+ )
+ raise http.Unauthorized()
+
+ def respond_authorized(self, user, *va, **kw):
+ kw['authorized_user'] = user
+ return self.leaf(*va, **kw)
+
+ def get_authorized(self, username):
+ # subclasses can return an alternative object which will be propagated
+ return username
+
+ def create_nonce(self):
+ return md5('%d:%s' % (time(), self.realm)).hexdigest()
+
+ def H(self, data):
+ return md5(data).hexdigest()
+
+ def KD(self, secret, data):
+ return self.H(secret + ':' + data)
+
+ def filter(self, *va, **kw):
+ # did the client even try to authenticate?
+ if 'HTTP_AUTHORIZATION' not in self.app.request.env:
+ return self.respond_unauthorized(self.require_authentication, *va, **kw)
+
+ # not digest auth?
+ if not self.app.request.env['HTTP_AUTHORIZATION'].startswith('Digest '):
+ raise http.BadRequest('only Digest authorization is allowed')
+
+ # parse
+ params = {}
+ required = len(self.required)
+ for k, v in [i.split("=", 1) for i in self.app.request.env['HTTP_AUTHORIZATION'][7:].strip().split(',')]:
+ k = k.strip()
+ params[k] = v.strip().replace('"', '')
+ if k in self.required:
+ required -= 1
+
+ # missing required parameters?
+ if required > 0:
+ raise http.BadRequest('insufficient authorization parameters')
+
+ # user exists?
+ if params['username'] not in self.users:
+ return self.respond_unauthorized(True, *va, **kw)
+
+ # build A1 and A2
+ A1 = '%s:%s:%s' % (params['username'], self.realm, self.users[params['username']])
+ A2 = self.app.request.method + ':' + self.app.request.url.uri
+
+ # build expected response
+ expected_response = None
+ if 'qop' in params:
+ # if qop is sent then cnonce and nc MUST be present
+ if not 'cnonce' in params or not 'nc' in params:
+ raise http.BadRequest('cnonce and/or nc authorization parameters missing')
+
+ # only auth type is supported
+ if params['qop'] != 'auth':
+ raise http.BadRequest('unsupported qop ' + params['qop'])
+
+ # build
+ expected_response = self.KD(self.H(A1), '%s:%s:%s:%s:%s' % (
+ params['nonce'], params['nc'], params['cnonce'], params['qop'], self.H(A2)))
+ else:
+ # qop not present (compatibility with RFC 2069)
+ expected_response = self.KD(self.H(A1), params['nonce'] + ':' + self.H(A2))
+
+ # 401 on realm mismatch
+ if params['realm'] != self.realm:
+ log.debug('auth failure: unexpected realm')
+ return self.respond_unauthorized(True, *va, **kw)
+
+ # 401 on unexpected response
+ if params['response'] != expected_response:
+ log.debug('auth failure: unexpected digest response')
+ return self.respond_unauthorized(True, *va, **kw)
+
+ # authorized -- delegate further down the filter chain
+ return self.respond_authorized(params['username'], *va, **kw)
+
+ def __call__(self, leaf):
+ self.leaf = leaf
+ def f(*va, **kw):
+ return self.filter(*va, **kw)
+ f.parent_leaf = leaf
+ f.__name__ = leaf.__name__+'_with_DigestAuthFilter'
+ return f
+

0 comments on commit a23a81d

Please sign in to comment.
Something went wrong with that request. Please try again.