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