1 #!/usr/bin/env python 2 3 """ 4 OpenID provider login resources which redirect clients back to the application 5 ("relying party"). 6 7 Copyright (C) 2004, 2005, 2006, 2007, 2008 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 import WebStack.Generic 25 from WebStack.Helpers.Auth import Authenticator, check_openid_signature, make_openid_signature 26 import datetime 27 import time 28 import random 29 import cgi # for escape 30 import urlparse # for urlsplit 31 32 class OpenIDLoginUtils: 33 34 "Utilities for OpenID login screens which may be inherited." 35 36 openid_ns = "http://specs.openid.net/auth/2.0" 37 signed_names = ["op_endpoint", "return_to", "response_nonce", "assoc_handle", "claimed_id", "identity"] 38 39 def __init__(self, app_url, authenticator, associations=None, use_redirect=1, urlencoding=None): 40 41 """ 42 Initialise the resource with the application URL 'app_url' and an 43 'authenticator'. The 'app_url' should be the "bare" reference using a 44 protocol, host and port, not including any path information. 45 46 The optional 'associations' is a mapping from association handles to 47 secret keys. 48 49 If the optional 'use_redirect' flag is set to a false value (which is 50 not the default), a confirmation screen is given instead of immediately 51 redirecting the user back to the original application. 52 53 The optional 'urlencoding' parameter allows a special encoding to be 54 used in producing the redirection path. 55 """ 56 57 self.app_url = app_url 58 self.authenticator = authenticator 59 self.associations = associations or {} 60 self.use_redirect = use_redirect 61 self.urlencoding = urlencoding 62 63 def get_openid_fields(self, trans, claimed_id, local_id, username, return_to, endpoint): 64 65 # Make an association that can be used in signature verification. 66 # NOTE: Encoding the handle to avoid problems with python-openid. 67 # NOTE: Probably need to consider the secret key a bit more. 68 69 handle = trans.encode_path(username, "utf-8") + str(time.time()) 70 secret_key = str(random.randint(0, 999999999)) 71 self.associations[handle] = secret_key 72 73 # Make a timestamp. 74 75 now = datetime.datetime.utcnow() 76 timestamp = now.strftime("%Y-%m-%dT%H:%M:%SZ") + str(now.microsecond) 77 78 # Make a signature. 79 80 fields = { 81 "openid.ns" : [self.openid_ns], 82 "openid.mode" : ["id_res"], 83 "openid.signed" : [",".join(self.signed_names)], 84 "openid.op_endpoint" : [endpoint], 85 "openid.return_to" : [return_to], 86 "openid.response_nonce" : [timestamp], 87 "openid.assoc_handle" : [handle], 88 "openid.claimed_id" : [claimed_id], 89 "openid.identity" : [local_id] 90 } 91 signature = make_openid_signature(self.signed_names, fields, secret_key) 92 fields["openid.sig"] = [signature] 93 94 return fields 95 96 def get_openid_url(self, trans, fields): 97 98 # Build an URL for returning to the application. 99 100 url = fields["openid.return_to"][0] 101 if "?" in url: 102 url += "&" 103 else: 104 url += "?" 105 106 first = 1 107 for name, value in fields.items(): 108 if not first: 109 url += "&" 110 url += "%s=%s" % (name, trans.encode_path(value[0])) 111 first = 0 112 113 return url 114 115 def redirect_to_application(self, trans, claimed_id, local_id, username, return_to, endpoint): 116 117 """ 118 Redirect the client using 'trans', 'claimed_id', 'local_id', 'username' 119 and the given 'return_to' and 'endpoint' details. 120 """ 121 122 fields = self.get_openid_fields(trans, claimed_id, local_id, username, return_to, endpoint) 123 url = self.get_openid_url(trans, fields) 124 125 # Show the success page anyway. 126 # Offer a POST-based form for redirection. 127 128 self.show_success(trans, fields) 129 if self.use_redirect: 130 trans.redirect(url) 131 else: 132 raise WebStack.Generic.EndOfResponse 133 134 def show_verification(self, trans, status): 135 136 """ 137 Writes a signature verification response using the transaction 'trans' 138 and the 'status' of the verification. 139 """ 140 141 trans.set_content_type(WebStack.Generic.ContentType("text/plain")) 142 out = trans.get_response_stream() 143 144 # NOTE: Need to use invalidate_handle, too. 145 146 if status: 147 status_str = "true" 148 else: 149 status_str = "false" 150 out.write("ns:%s\nis_valid:%s\n" % (self.openid_ns, status_str)) 151 raise WebStack.Generic.EndOfResponse 152 153 def check_authentication(self, trans, fields): 154 155 "Check the authentication details supplied in 'trans' and 'fields'." 156 157 # Obtain the secret key from recorded associations. 158 159 handle = fields.get("openid.assoc_handle", [None])[0] 160 if handle is not None and self.associations.has_key(handle): 161 valid = check_openid_signature(fields, self.associations[handle]) 162 del self.associations[handle] 163 else: 164 valid = 0 165 166 # Produce a response for this request. 167 168 self.show_verification(trans, valid) 169 170 class OpenIDLoginResource(OpenIDLoginUtils): 171 172 "A resource providing a login screen." 173 174 def __init__(self, app_url, authenticator, associations=None, use_redirect=1, urlencoding=None, encoding=None): 175 176 """ 177 Initialise the resource with the application URL 'app_url' and an 178 'authenticator'. 179 180 The optional 'associations' is a mapping from association handles to 181 secret keys. 182 183 If the optional 'use_redirect' flag is set to a false value (which is 184 not the default), a confirmation screen is given instead of immediately 185 redirecting the user back to the original application. 186 187 The optional 'urlencoding' parameter allows a special encoding to be 188 used in producing the redirection path. 189 190 The optional 'encoding' parameter allows a special encoding to be used 191 in producing the login pages. 192 193 To change the pages employed by this resource, either redefine the 194 'login_page' and 'success_page' attributes in instances of this class or 195 a subclass, or override the 'show_login' and 'show_success' methods. 196 """ 197 198 OpenIDLoginUtils.__init__(self, app_url, authenticator, associations, use_redirect, urlencoding) 199 self.encoding = encoding 200 201 def respond(self, trans): 202 203 "Respond using the transaction 'trans'." 204 205 # Check for a submitted login form. 206 207 fields = trans.get_fields(self.encoding) 208 209 if fields.has_key("login"): 210 self.check_login(trans, fields) 211 # The above method may not return. 212 213 # Check for an OpenID signature verification request. 214 215 elif fields.get("openid.mode", [None])[0] == "check_authentication": 216 self.check_authentication(trans, fields) 217 # The above method does not return. 218 219 # NOTE: Permit association requests here. 220 # Otherwise, show the login form. 221 222 self.show_login(trans, fields) 223 224 def check_login(self, trans, fields): 225 226 "Check the login details supplied in 'trans' and 'fields'." 227 228 return_to = fields.get("openid.return_to", [""])[0] 229 claimed_id = fields.get("openid.claimed_id", [""])[0] 230 local_id = fields.get("openid.identity", [""])[0] 231 232 # Check a combination of local identifier and username together with 233 # the password. 234 235 username = fields.get("username", [""])[0] 236 password = fields.get("password", [""])[0] 237 238 # NOTE: Permit flexibility in the credentials. 239 240 if self.authenticator.authenticate(trans, (local_id, username), password): 241 endpoint = self.app_url + trans.get_path_without_query(self.urlencoding) 242 self.redirect_to_application(trans, claimed_id, local_id, username, return_to, endpoint) 243 244 def show_login(self, trans, fields): 245 246 """ 247 Writes a login screen using the transaction 'trans' and 'fields'. 248 """ 249 250 return_to = fields.get("openid.return_to", [""])[0] 251 claimed_id = fields.get("openid.claimed_id", [""])[0] 252 local_id = fields.get("openid.identity", [""])[0] 253 254 trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding or trans.default_charset)) 255 out = trans.get_response_stream() 256 out.write(self.login_page % tuple(map(cgi.escape, (return_to, claimed_id, local_id)))) 257 258 def show_success(self, trans, fields): 259 260 """ 261 Writes a success screen using the transaction 'trans', using a 262 dictionary of 'fields' providing details of the transaction. 263 """ 264 265 trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding or trans.default_charset)) 266 out = trans.get_response_stream() 267 l = [] 268 for name, values in fields.items(): 269 l.append("""<input name="%s" type="hidden" value="%s" />""" % (name, cgi.escape(values[0]))) 270 out.write(self.success_page % (fields["openid.return_to"][0], "\n".join(l))) 271 272 login_page = """ 273 <html> 274 <head> 275 <title>Login</title> 276 </head> 277 <body> 278 <h1>Login</h1> 279 <form method="POST"> 280 <p>Username: <input name="username" type="text" size="12"/></p> 281 <p>Password: <input name="password" type="password" size="12"/></p> 282 <p><input name="login" type="submit" value="Login"/></p> 283 <input name="openid.return_to" type="hidden" value="%s"/> 284 <input name="openid.claimed_id" type="hidden" value="%s"/> 285 <input name="openid.identity" type="hidden" value="%s"/> 286 </form> 287 </body> 288 </html> 289 """ 290 291 success_page = """ 292 <html> 293 <head> 294 <title>Login Example</title> 295 </head> 296 <body> 297 <h1>Login Successful</h1> 298 <form action="%s" method="POST" name="openid_redirect"> 299 %s 300 <p>Please proceed to the application: <input name="proceed" type="submit" value="Proceed!" /></p> 301 </form> 302 </body> 303 </html> 304 """ 305 306 # vim: tabstop=4 expandtab shiftwidth=4