paulb@733 | 1 | #!/usr/bin/env python |
paulb@733 | 2 | |
paulb@733 | 3 | """ |
paulb@733 | 4 | OpenID redirection classes, sending unauthenticated users to the OpenID |
paulb@733 | 5 | initiation page. |
paulb@733 | 6 | |
paulb@733 | 7 | Copyright (C) 2004, 2005, 2006, 2007 Paul Boddie <paul@boddie.org.uk> |
paulb@733 | 8 | |
paulb@733 | 9 | This library is free software; you can redistribute it and/or |
paulb@733 | 10 | modify it under the terms of the GNU Lesser General Public |
paulb@733 | 11 | License as published by the Free Software Foundation; either |
paulb@733 | 12 | version 2.1 of the License, or (at your option) any later version. |
paulb@733 | 13 | |
paulb@733 | 14 | This library is distributed in the hope that it will be useful, |
paulb@733 | 15 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
paulb@733 | 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
paulb@733 | 17 | Lesser General Public License for more details. |
paulb@733 | 18 | |
paulb@733 | 19 | You should have received a copy of the GNU Lesser General Public |
paulb@733 | 20 | License along with this library; if not, write to the Free Software |
paulb@733 | 21 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA |
paulb@733 | 22 | """ |
paulb@733 | 23 | |
paulb@736 | 24 | from WebStack.Resources.LoginRedirect import LoginRedirectResource |
paulb@733 | 25 | from WebStack.Helpers.Auth import Verifier, check_openid_signature |
paulb@733 | 26 | import WebStack.Generic |
paulb@733 | 27 | import datetime |
paulb@733 | 28 | import urllib |
paulb@733 | 29 | |
paulb@736 | 30 | class OpenIDRedirectResource(LoginRedirectResource): |
paulb@736 | 31 | |
paulb@736 | 32 | "A resource redirecting to an OpenID initiation page." |
paulb@736 | 33 | |
paulb@736 | 34 | openid_ns = "http://specs.openid.net/auth/2.0" |
paulb@736 | 35 | |
paulb@736 | 36 | def respond(self, trans): |
paulb@736 | 37 | |
paulb@736 | 38 | "Respond using the given transaction 'trans'." |
paulb@736 | 39 | |
paulb@738 | 40 | fields = trans.get_fields(self.path_encoding) |
paulb@736 | 41 | |
paulb@736 | 42 | # If requested, attempt to verify OpenID assertions. |
paulb@736 | 43 | # http://openid.net/specs/openid-authentication-2_0-12.html#rfc.section.11 |
paulb@736 | 44 | |
paulb@739 | 45 | if fields.get("openid.mode", [None])[0] == "id_res": |
paulb@739 | 46 | |
paulb@739 | 47 | # The additional condition could be used to insist on OpenID 2.0 |
paulb@739 | 48 | # conformance: |
paulb@739 | 49 | # fields.get("openid.ns", [None])[0] == self.openid_ns |
paulb@736 | 50 | |
paulb@736 | 51 | if self.authenticator.authenticate(trans, verify=1): |
paulb@756 | 52 | trans.redirect(trans.encode_url_without_query(fields["openid.return_to"][0])) |
paulb@736 | 53 | |
paulb@736 | 54 | # Otherwise, handle the usual parameters and request details. |
paulb@736 | 55 | |
paulb@736 | 56 | LoginRedirectResource.respond(self, trans) |
paulb@736 | 57 | |
paulb@733 | 58 | class OpenIDRedirectAuthenticator(Verifier): |
paulb@733 | 59 | |
paulb@733 | 60 | """ |
paulb@733 | 61 | An authenticator which verifies the credentials provided in a special login |
paulb@733 | 62 | cookie, accepting OpenID assertions if necessary. |
paulb@733 | 63 | """ |
paulb@733 | 64 | |
paulb@733 | 65 | openid_ns = "http://specs.openid.net/auth/2.0" |
paulb@733 | 66 | replay_limit = datetime.timedelta(0, 10) # 10s |
paulb@733 | 67 | |
paulb@733 | 68 | def __init__(self, secret_key, app_url, associations=None, replay_limit=None, |
paulb@733 | 69 | cookie_name=None, urlencoding=None): |
paulb@733 | 70 | |
paulb@733 | 71 | """ |
paulb@733 | 72 | Initialise the authenticator with a 'secret_key', 'app_url' and optional |
paulb@759 | 73 | 'associations', 'replay_limit', 'cookie_name' and 'urlencoding'. The |
paulb@759 | 74 | 'app_url' should be the "bare" reference using a protocol, host and |
paulb@759 | 75 | port, not including any path information. |
paulb@733 | 76 | """ |
paulb@733 | 77 | |
paulb@733 | 78 | Verifier.__init__(self, secret_key, cookie_name) |
paulb@733 | 79 | |
paulb@733 | 80 | self.app_url = app_url |
paulb@733 | 81 | self.associations = associations or {} |
paulb@733 | 82 | self.replay_limit = replay_limit or self.replay_limit |
paulb@752 | 83 | self.urlencoding = urlencoding |
paulb@733 | 84 | |
paulb@736 | 85 | def authenticate(self, trans, verify=0): |
paulb@733 | 86 | |
paulb@733 | 87 | """ |
paulb@733 | 88 | Authenticate the originator of 'trans', updating the object if |
paulb@733 | 89 | successful and returning a true value if successful, or a false value |
paulb@733 | 90 | otherwise. |
paulb@736 | 91 | |
paulb@736 | 92 | If the optional 'verify' parameter is specified as a true value, perform |
paul@775 | 93 | verification on any incoming OpenID credentials. |
paulb@733 | 94 | """ |
paulb@733 | 95 | |
paulb@736 | 96 | # If requested, attempt to verify OpenID assertions. |
paulb@733 | 97 | |
paulb@736 | 98 | if verify: |
paulb@738 | 99 | fields = trans.get_fields(self.urlencoding) |
paulb@733 | 100 | |
paulb@736 | 101 | # NOTE: Could expose all errors. |
paulb@733 | 102 | |
paulb@736 | 103 | try: |
paulb@736 | 104 | # Test the details of the assertion. |
paulb@733 | 105 | |
paulb@738 | 106 | if self.test_url(fields) and \ |
paulb@756 | 107 | self.test_signature(trans, fields) and \ |
paulb@738 | 108 | self.test_replay(fields): |
paulb@733 | 109 | |
paul@775 | 110 | self.set_token(trans, fields["openid.claimed_id"][0]) |
paulb@736 | 111 | return 1 |
paulb@733 | 112 | |
paulb@736 | 113 | # Incomplete assertion. |
paulb@733 | 114 | |
paulb@736 | 115 | except (KeyError, ValueError): |
paulb@736 | 116 | raise |
paulb@733 | 117 | |
paulb@733 | 118 | # Assertion failed or was incomplete. |
paulb@733 | 119 | |
paulb@733 | 120 | return 0 |
paulb@733 | 121 | |
paulb@736 | 122 | # Otherwise, try to authenticate with an application cookie. |
paulb@736 | 123 | |
paulb@736 | 124 | else: |
paulb@736 | 125 | valid = Verifier.authenticate(self, trans) |
paulb@733 | 126 | |
paulb@736 | 127 | # Update the transaction with the user details. |
paulb@736 | 128 | |
paulb@736 | 129 | if valid: |
paulb@736 | 130 | username, token = self.get_username_and_token(trans) |
paulb@736 | 131 | trans.set_user(username) |
paulb@736 | 132 | return valid |
paulb@733 | 133 | |
paulb@738 | 134 | def test_url(self, fields): |
paulb@733 | 135 | |
paulb@733 | 136 | """ |
paulb@733 | 137 | See: |
paulb@733 | 138 | http://openid.net/specs/openid-authentication-2_0-12.html#rfc.section.11.1 |
paulb@733 | 139 | """ |
paulb@733 | 140 | |
paulb@733 | 141 | # NOTE: Currently, this is not strict enough. |
paulb@733 | 142 | |
paul@775 | 143 | return fields.has_key("openid.return_to") and \ |
paul@775 | 144 | fields["openid.return_to"][0].startswith(self.app_url) |
paulb@733 | 145 | |
paulb@756 | 146 | def test_signature(self, trans, fields): |
paulb@733 | 147 | |
paulb@733 | 148 | """ |
paulb@733 | 149 | See: |
paulb@733 | 150 | http://openid.net/specs/openid-authentication-2_0-12.html#rfc.section.11.4 |
paulb@733 | 151 | http://openid.net/specs/openid-authentication-2_0-12.html#rfc.section.6 |
paulb@733 | 152 | """ |
paulb@733 | 153 | |
paulb@738 | 154 | handle = fields.get("openid.assoc_handle", [None])[0] |
paulb@733 | 155 | |
paulb@733 | 156 | # With an association handle, look up the appropriate secret key and |
paulb@733 | 157 | # verify the signed items. |
paulb@733 | 158 | |
paulb@733 | 159 | if handle is not None: |
paulb@733 | 160 | |
paulb@733 | 161 | # Where an association exists, use the known secret key. |
paulb@733 | 162 | |
paulb@733 | 163 | if self.associations.has_key(handle): |
paulb@738 | 164 | return check_openid_signature(fields, self.associations[handle]) |
paulb@733 | 165 | |
paulb@733 | 166 | # Without an association, request verification of the signed items |
paulb@733 | 167 | # from the OpenID provider. |
paulb@733 | 168 | |
paulb@733 | 169 | else: |
paulb@756 | 170 | return self.test_signature_direct(trans, fields) |
paulb@733 | 171 | |
paulb@733 | 172 | # Without a handle, no signature verification can occur. |
paulb@733 | 173 | |
paulb@733 | 174 | return 0 |
paulb@733 | 175 | |
paulb@756 | 176 | def test_signature_direct(self, trans, fields): |
paulb@733 | 177 | |
paulb@733 | 178 | """ |
paulb@733 | 179 | See: |
paulb@733 | 180 | http://openid.net/specs/openid-authentication-2_0-12.html#rfc.section.11.4.2 |
paulb@733 | 181 | """ |
paulb@733 | 182 | |
paulb@733 | 183 | # Make a POST request using the "openid." fields. |
paulb@733 | 184 | |
paulb@756 | 185 | d = [] |
paulb@738 | 186 | for name, values in fields.items(): |
paulb@733 | 187 | if name.startswith("openid.") and name != "openid.mode": |
paulb@756 | 188 | d.append("%s=%s" % (name, trans.encode_path(values[0]))) |
paulb@756 | 189 | d.append("%s=%s" % ("openid.mode", "check_authentication")) |
paulb@756 | 190 | data = "&".join(d) |
paulb@733 | 191 | |
paulb@733 | 192 | # Send a POST request to the OpenID provider, reading the response and |
paulb@733 | 193 | # testing for certain fields and values. |
paulb@733 | 194 | |
paulb@738 | 195 | f = urllib.urlopen(fields["openid.op_endpoint"][0], data) |
paulb@733 | 196 | try: |
paulb@733 | 197 | items = [] |
paulb@733 | 198 | for line in f.readlines(): |
paulb@733 | 199 | if line[-1] == "\n": |
paulb@733 | 200 | line = line[:-1] |
paulb@733 | 201 | parts = line.split(":") |
paulb@733 | 202 | items.append((parts[0], ":".join(parts[1:]))) |
paulb@733 | 203 | fields = dict(items) |
paulb@733 | 204 | return fields["ns"] == self.openid_ns and fields["is_valid"] == "true" |
paulb@733 | 205 | finally: |
paulb@733 | 206 | f.close() |
paulb@733 | 207 | |
paulb@738 | 208 | def test_replay(self, fields): |
paulb@733 | 209 | |
paulb@733 | 210 | """ |
paulb@733 | 211 | See: |
paulb@733 | 212 | http://openid.net/specs/openid-authentication-2_0-12.html#rfc.section.11.3 |
paulb@733 | 213 | """ |
paulb@733 | 214 | |
paulb@738 | 215 | timestamp = fields["openid.response_nonce"][0] |
paulb@733 | 216 | # YYYY-MM-DDTHH:MM:SSZ... |
paulb@733 | 217 | year, month, day, hour, minute, second, unique = \ |
paulb@733 | 218 | int(timestamp[0:4]), int(timestamp[5:7]), int(timestamp[8:10]), \ |
paulb@733 | 219 | int(timestamp[11:13]), int(timestamp[14:16]), int(timestamp[17:19]), \ |
paulb@733 | 220 | timestamp[20:] |
paulb@733 | 221 | dt = datetime.datetime(year, month, day, hour, minute, second) |
paulb@733 | 222 | return -self.replay_limit < (datetime.datetime.utcnow() - dt) < self.replay_limit |
paulb@733 | 223 | |
paulb@733 | 224 | def set_token(self, trans, username): |
paulb@733 | 225 | |
paulb@733 | 226 | "Set an authentication token in 'trans' with the given 'username'." |
paulb@733 | 227 | |
paulb@733 | 228 | Verifier.set_token(self, trans, username) |
paulb@733 | 229 | |
paulb@733 | 230 | # Update the transaction with the user details. |
paulb@733 | 231 | |
paulb@733 | 232 | trans.set_user(username) |
paulb@733 | 233 | |
paulb@733 | 234 | # vim: tabstop=4 expandtab shiftwidth=4 |