# HG changeset patch # User paulb # Date 1194828603 0 # Node ID 7f1f02b485f8eb33dc41c070922619abb79e14d9 # Parent 867372001a0cb2d642d8e83c2b6b52ecb598017b [project @ 2007-11-12 00:50:03 by paulb] Introduced base classes for common authentication activities. Made cookie usage "safe" for usernames containing ":" characters. Added support for OpenID signatures. diff -r 867372001a0c -r 7f1f02b485f8 WebStack/Helpers/Auth.py --- a/WebStack/Helpers/Auth.py Mon Nov 12 00:49:00 2007 +0000 +++ b/WebStack/Helpers/Auth.py Mon Nov 12 00:50:03 2007 +0000 @@ -22,6 +22,10 @@ import base64 import md5 +try: + import hmac +except ImportError: + hmac = None class UserInfo: @@ -50,6 +54,136 @@ self.username, self.password = None, None +# Classes providing support for authentication resources. + +class Authenticator: + + """ + A simple authenticator with no other purpose than to return the status of an + authentication request. + """ + + def __init__(self, credentials): + + """ + Initialise the authenticator with a registry of 'credentials'. + + The 'credentials' must be an object which supports tests of the form + '(username, password) in credentials'. + """ + + self.credentials = credentials + + def authenticate(self, trans, username, password): + + """ + Authenticate the sender of the transaction 'trans', returning a true + value if they are recognised, or a false value otherwise. Use the + 'username' and 'password' supplied as credentials. + """ + + # Check against the class's credentials. + + return (username, password) in self.credentials + +class Verifier: + + """ + An authenticator which can only verify an authentication attempt with + existing credentials, not check new credentials. + """ + + def __init__(self, secret_key, cookie_name=None): + + """ + Initialise the authenticator with a 'secret_key' and an optional + 'cookie_name'. + """ + + self.secret_key = secret_key + self.cookie_name = cookie_name or "LoginAuthenticator" + + def _encode(self, username): + return username.replace(":", "%3A") + + def _decode(self, encoded_username): + return encoded_username.replace("%3A", ":") + + def authenticate(self, trans): + + """ + Authenticate the originator of 'trans', returning a true value if + successful, or a false value otherwise. + """ + + # Test the token from the cookie against a recreated token using the + # given information. + + details = self.get_username_and_token(trans) + if details is None: + return 0 + else: + username, token = details + return token == get_token(self._encode(username), self.secret_key) + + def get_username_and_token(self, trans): + + "Return the username and token for the user." + + cookie = trans.get_cookie(self.cookie_name) + if cookie is None or cookie.value is None: + return None + else: + return self._decode(cookie.value.split(":")[0]), cookie.value + + def set_token(self, trans, username): + + "Set an authentication token in 'trans' with the given 'username'." + + trans.set_cookie_value( + self.cookie_name, + get_token(self._encode(username), self.secret_key), + path="/" + ) + + def unset_token(self, trans): + + "Unset the authentication token in 'trans'." + + trans.delete_cookie(self.cookie_name) + +class LoginAuthenticator(Authenticator, Verifier): + + """ + An authenticator which sets authentication tokens. + """ + + def __init__(self, secret_key, credentials, cookie_name=None): + + """ + Initialise the authenticator with a 'secret_key', the authenticator's registry of + 'credentials' and an optional 'cookie_name'. + + The 'credentials' must be an object which supports tests of the form + '(username, password) in credentials'. + """ + + Authenticator.__init__(self, credentials) + Verifier.__init__(self, secret_key, cookie_name) + + def authenticate(self, trans, username, password): + + """ + Authenticate the sender of the transaction 'trans', returning a true + value if they are recognised, or a false value otherwise. Use the + 'username' and 'password' supplied as credentials. + """ + + valid = Authenticator.authenticate(self, trans, username, password) + if valid: + self.set_token(trans, username) + return valid + def get_token(plaintext, secret_key): """ @@ -59,4 +193,40 @@ return plaintext + ":" + md5.md5(plaintext + secret_key).hexdigest() +if hmac is not None: + + def get_openid_token(items, secret_key): + + """ + Return a string containing the 'items' encoded using the given + 'secret_key'. + """ + + plaintext = "\n".join([(key + ":" + value) for (key, value) in items]) + "\n" + hash = hmac.new(secret_key, plaintext) + return base64.standard_b64encode(hash.digest()) + + def check_openid_signature(fields, secret_key): + + """ + Return whether information in the given 'fields' (a mapping from names + to lists of values) is signed using the 'secret_key'. + """ + + signed_names = fields["openid.signed"][0].split(",") + return fields["openid.sig"][0] == make_openid_signature(signed_names, fields, secret_key) + + def make_openid_signature(signed_names, fields, secret_key): + + """ + Make and return a signature using the 'signed_names' to indicate which + values from the 'fields' (a mapping from names to lists of values) shall + be signed using the 'secret_key'. + """ + + items = [] + for name in signed_names: + items.append((name, fields["openid." + name][0])) + return get_openid_token(items, secret_key) + # vim: tabstop=4 expandtab shiftwidth=4