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