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 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 return plaintext + ":" + md5.md5(plaintext + secret_key).hexdigest() 200 201 # OpenID token verification. 202 # NOTE: Add SHA256 usage for associations. 203 204 if sha1 is not None: 205 206 def get_openid_token(items, secret_key): 207 208 """ 209 Return a string containing the 'items' encoded using the given 210 'secret_key'. 211 """ 212 213 plaintext = "\n".join([(key + ":" + value) for (key, value) in items]) + "\n" 214 hash = hmac.new(secret_key, plaintext, sha1) 215 return base64.standard_b64encode(hash.digest()) 216 217 def check_openid_signature(fields, secret_key): 218 219 """ 220 Return whether information in the given 'fields' (a mapping from names 221 to lists of values) is signed using the 'secret_key'. 222 """ 223 224 signed_names = fields["openid.signed"][0].split(",") 225 return fields["openid.sig"][0] == make_openid_signature(signed_names, fields, secret_key) 226 227 def make_openid_signature(signed_names, fields, secret_key): 228 229 """ 230 Make and return a signature using the 'signed_names' to indicate which 231 values from the 'fields' (a mapping from names to lists of values) shall 232 be signed using the 'secret_key'. 233 """ 234 235 items = [] 236 for name in signed_names: 237 items.append((name, fields["openid." + name][0])) 238 return get_openid_token(items, secret_key) 239 240 # vim: tabstop=4 expandtab shiftwidth=4