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