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