1 #!/usr/bin/env python 2 3 """ 4 Authentication/authorisation helper classes and functions. 5 6 Copyright (C) 2004, 2005, 2007, 2008 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 24 The authentication token produced in this module typically employs the username 25 and a secret. If other things are added in addition to the username, it should 26 not be possible to combine them in a way which causes "collisions" between 27 distinct username-plus-extra-data inputs. 28 """ 29 30 import base64 31 import md5 32 import hmac 33 try: 34 from hashlib import sha1, sha256 35 except ImportError: 36 sha256 = None 37 try: 38 from sha import new as sha1 39 except ImportError: 40 sha1 = None 41 42 class UserInfo: 43 44 """ 45 A class used to represent user information in terms of the authentication 46 scheme employed and the user details. 47 """ 48 49 def __init__(self, auth_header): 50 51 """ 52 Initialise the object with the value of the 'auth_header' - that is, the 53 HTTP Authorization header. 54 """ 55 56 self.scheme, auth_details = auth_header.split(" ") 57 if self.scheme == "Basic": 58 59 # NOTE: Assume that no username or password contains ":". 60 61 self.username, self.password = base64.decodestring(auth_details).split(":") 62 63 else: 64 65 # NOTE: Other schemes not yet supported. 66 67 self.username, self.password = None, None 68 69 # Classes providing support for authentication resources. 70 71 class Authenticator: 72 73 """ 74 A simple authenticator with no other purpose than to return the status of an 75 authentication request. 76 """ 77 78 def __init__(self, credentials): 79 80 """ 81 Initialise the authenticator with a registry of 'credentials'. 82 83 The 'credentials' must be an object which supports tests of the form 84 '(username, password) in credentials'. 85 """ 86 87 self.credentials = credentials 88 89 def authenticate(self, trans, username, password): 90 91 """ 92 Authenticate the sender of the transaction 'trans', returning a true 93 value if they are recognised, or a false value otherwise. Use the 94 'username' and 'password' supplied as credentials. 95 """ 96 97 # Check against the class's credentials. 98 99 return (username, password) in self.credentials 100 101 class Verifier: 102 103 """ 104 An authenticator which can only verify an authentication attempt with 105 existing credentials, not check new credentials. 106 """ 107 108 def __init__(self, secret_key, cookie_name=None): 109 110 """ 111 Initialise the authenticator with a 'secret_key' and an optional 112 'cookie_name'. 113 """ 114 115 self.secret_key = secret_key 116 self.cookie_name = cookie_name or "LoginAuthenticator" 117 118 def _encode(self, username): 119 return username.replace(":", "%3A") 120 121 def _decode(self, encoded_username): 122 return encoded_username.replace("%3A", ":") 123 124 def authenticate(self, trans): 125 126 """ 127 Authenticate the originator of 'trans', returning a true value if 128 successful, or a false value otherwise. 129 """ 130 131 # Test the token from the cookie against a recreated token using the 132 # given information. 133 134 details = self.get_username_and_token(trans) 135 if details is None: 136 return 0 137 else: 138 username, token = details 139 return token == get_token(self._encode(username), self.secret_key) 140 141 def get_username_and_token(self, trans): 142 143 "Return the username and token for the user." 144 145 cookie = trans.get_cookie(self.cookie_name) 146 if cookie is None or cookie.value is None: 147 return None 148 else: 149 return self._decode(cookie.value.split(":")[0]), cookie.value 150 151 def set_token(self, trans, username): 152 153 "Set an authentication token in 'trans' with the given 'username'." 154 155 trans.set_cookie_value( 156 self.cookie_name, 157 get_token(self._encode(username), self.secret_key), 158 path="/" 159 ) 160 161 def unset_token(self, trans): 162 163 "Unset the authentication token in 'trans'." 164 165 trans.delete_cookie(self.cookie_name) 166 167 class LoginAuthenticator(Authenticator, Verifier): 168 169 """ 170 An authenticator which sets authentication tokens. 171 """ 172 173 def __init__(self, secret_key, credentials, cookie_name=None): 174 175 """ 176 Initialise the authenticator with a 'secret_key', the authenticator's registry of 177 'credentials' and an optional 'cookie_name'. 178 179 The 'credentials' must be an object which supports tests of the form 180 '(username, password) in credentials'. 181 """ 182 183 Authenticator.__init__(self, credentials) 184 Verifier.__init__(self, secret_key, cookie_name) 185 186 def authenticate(self, trans, username, password): 187 188 """ 189 Authenticate the sender of the transaction 'trans', returning a true 190 value if they are recognised, or a false value otherwise. Use the 191 'username' and 'password' supplied as credentials. 192 """ 193 194 valid = Authenticator.authenticate(self, trans, username, password) 195 if valid: 196 self.set_token(trans, username) 197 return valid 198 199 def get_token(plaintext, secret_key): 200 201 """ 202 Return a string containing an authentication token made from the given 203 'plaintext' and 'secret_key'. 204 """ 205 206 # NOTE: Using "safe" encoding to deal with Unicode plaintext. 207 208 return plaintext + ":" + md5.md5(plaintext.encode("utf-8") + secret_key).hexdigest() 209 210 # OpenID token verification. 211 # NOTE: Add SHA256 usage for associations. 212 213 if sha1 is not None: 214 215 def get_openid_token(items, secret_key): 216 217 """ 218 Return a string containing the 'items' encoded using the given 219 'secret_key'. 220 """ 221 222 plaintext = "\n".join([(key + ":" + value) for (key, value) in items]) + "\n" 223 224 # NOTE: Using "safe" encoding to deal with Unicode plaintext. 225 226 hash = hmac.new(secret_key, plaintext.encode("utf-8"), sha1) 227 return base64.standard_b64encode(hash.digest()) 228 229 def check_openid_signature(fields, secret_key): 230 231 """ 232 Return whether information in the given 'fields' (a mapping from names 233 to lists of values) is signed using the 'secret_key'. 234 """ 235 236 signed_names = fields["openid.signed"][0].split(",") 237 return fields["openid.sig"][0] == make_openid_signature(signed_names, fields, secret_key) 238 239 def make_openid_signature(signed_names, fields, secret_key): 240 241 """ 242 Make and return a signature using the 'signed_names' to indicate which 243 values from the 'fields' (a mapping from names to lists of values) shall 244 be signed using the 'secret_key'. 245 """ 246 247 items = [] 248 for name in signed_names: 249 items.append((name, fields["openid." + name][0])) 250 return get_openid_token(items, secret_key) 251 252 # vim: tabstop=4 expandtab shiftwidth=4