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