1 #!/usr/bin/env python 2 3 """ 4 Authentication/authorisation helper classes and functions. 5 6 Copyright (C) 2004, 2005 Paul Boddie <paul@boddie.org.uk> 7 8 This library is free software; you can redistribute it and/or 9 modify it under the terms of the GNU Lesser General Public 10 License as published by the Free Software Foundation; either 11 version 2.1 of the License, or (at your option) any later version. 12 13 This library is distributed in the hope that it will be useful, 14 but WITHOUT ANY WARRANTY; without even the implied warranty of 15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 16 Lesser General Public License for more details. 17 18 You should have received a copy of the GNU Lesser General Public 19 License along with this library; if not, write to the Free Software 20 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 21 """ 22 23 import base64 24 import md5 25 try: 26 import hmac 27 except ImportError: 28 hmac = None 29 30 class UserInfo: 31 32 """ 33 A class used to represent user information in terms of the authentication 34 scheme employed and the user details. 35 """ 36 37 def __init__(self, auth_header): 38 39 """ 40 Initialise the object with the value of the 'auth_header' - that is, the 41 HTTP Authorization header. 42 """ 43 44 self.scheme, auth_details = auth_header.split(" ") 45 if self.scheme == "Basic": 46 47 # NOTE: Assume that no username or password contains ":". 48 49 self.username, self.password = base64.decodestring(auth_details).split(":") 50 51 else: 52 53 # NOTE: Other schemes not yet supported. 54 55 self.username, self.password = None, None 56 57 # Classes providing support for authentication resources. 58 59 class Authenticator: 60 61 """ 62 A simple authenticator with no other purpose than to return the status of an 63 authentication request. 64 """ 65 66 def __init__(self, credentials): 67 68 """ 69 Initialise the authenticator with a registry of 'credentials'. 70 71 The 'credentials' must be an object which supports tests of the form 72 '(username, password) in credentials'. 73 """ 74 75 self.credentials = credentials 76 77 def authenticate(self, trans, username, password): 78 79 """ 80 Authenticate the sender of the transaction 'trans', returning a true 81 value if they are recognised, or a false value otherwise. Use the 82 'username' and 'password' supplied as credentials. 83 """ 84 85 # Check against the class's credentials. 86 87 return (username, password) in self.credentials 88 89 class Verifier: 90 91 """ 92 An authenticator which can only verify an authentication attempt with 93 existing credentials, not check new credentials. 94 """ 95 96 def __init__(self, secret_key, cookie_name=None): 97 98 """ 99 Initialise the authenticator with a 'secret_key' and an optional 100 'cookie_name'. 101 """ 102 103 self.secret_key = secret_key 104 self.cookie_name = cookie_name or "LoginAuthenticator" 105 106 def _encode(self, username): 107 return username.replace(":", "%3A") 108 109 def _decode(self, encoded_username): 110 return encoded_username.replace("%3A", ":") 111 112 def authenticate(self, trans): 113 114 """ 115 Authenticate the originator of 'trans', returning a true value if 116 successful, or a false value otherwise. 117 """ 118 119 # Test the token from the cookie against a recreated token using the 120 # given information. 121 122 details = self.get_username_and_token(trans) 123 if details is None: 124 return 0 125 else: 126 username, token = details 127 return token == get_token(self._encode(username), self.secret_key) 128 129 def get_username_and_token(self, trans): 130 131 "Return the username and token for the user." 132 133 cookie = trans.get_cookie(self.cookie_name) 134 if cookie is None or cookie.value is None: 135 return None 136 else: 137 return self._decode(cookie.value.split(":")[0]), cookie.value 138 139 def set_token(self, trans, username): 140 141 "Set an authentication token in 'trans' with the given 'username'." 142 143 trans.set_cookie_value( 144 self.cookie_name, 145 get_token(self._encode(username), self.secret_key), 146 path="/" 147 ) 148 149 def unset_token(self, trans): 150 151 "Unset the authentication token in 'trans'." 152 153 trans.delete_cookie(self.cookie_name) 154 155 class LoginAuthenticator(Authenticator, Verifier): 156 157 """ 158 An authenticator which sets authentication tokens. 159 """ 160 161 def __init__(self, secret_key, credentials, cookie_name=None): 162 163 """ 164 Initialise the authenticator with a 'secret_key', the authenticator's registry of 165 'credentials' and an optional 'cookie_name'. 166 167 The 'credentials' must be an object which supports tests of the form 168 '(username, password) in credentials'. 169 """ 170 171 Authenticator.__init__(self, credentials) 172 Verifier.__init__(self, secret_key, cookie_name) 173 174 def authenticate(self, trans, username, password): 175 176 """ 177 Authenticate the sender of the transaction 'trans', returning a true 178 value if they are recognised, or a false value otherwise. Use the 179 'username' and 'password' supplied as credentials. 180 """ 181 182 valid = Authenticator.authenticate(self, trans, username, password) 183 if valid: 184 self.set_token(trans, username) 185 return valid 186 187 def get_token(plaintext, secret_key): 188 189 """ 190 Return a string containing an authentication token made from the given 191 'plaintext' and 'secret_key'. 192 """ 193 194 return plaintext + ":" + md5.md5(plaintext + secret_key).hexdigest() 195 196 if hmac is not None: 197 198 def get_openid_token(items, secret_key): 199 200 """ 201 Return a string containing the 'items' encoded using the given 202 'secret_key'. 203 """ 204 205 plaintext = "\n".join([(key + ":" + value) for (key, value) in items]) + "\n" 206 hash = hmac.new(secret_key, plaintext) 207 return base64.standard_b64encode(hash.digest()) 208 209 def check_openid_signature(fields, secret_key): 210 211 """ 212 Return whether information in the given 'fields' (a mapping from names 213 to lists of values) is signed using the 'secret_key'. 214 """ 215 216 signed_names = fields["openid.signed"][0].split(",") 217 return fields["openid.sig"][0] == make_openid_signature(signed_names, fields, secret_key) 218 219 def make_openid_signature(signed_names, fields, secret_key): 220 221 """ 222 Make and return a signature using the 'signed_names' to indicate which 223 values from the 'fields' (a mapping from names to lists of values) shall 224 be signed using the 'secret_key'. 225 """ 226 227 items = [] 228 for name in signed_names: 229 items.append((name, fields["openid." + name][0])) 230 return get_openid_token(items, secret_key) 231 232 # vim: tabstop=4 expandtab shiftwidth=4