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