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