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 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, associations, use_redirect) 199 self.app_url = app_url 200 self.authenticator = authenticator 201 self.urlencoding = urlencoding 202 self.encoding = encoding 203 204 def respond(self, trans): 205 206 "Respond using the transaction 'trans'." 207 208 # Check for a submitted login form. 209 210 fields = trans.get_fields(self.encoding) 211 212 if fields.has_key("login"): 213 self.check_login(trans, fields) 214 # The above method may not return. 215 216 # Check for an OpenID signature verification request. 217 218 elif fields.get("openid.mode", [None])[0] == "check_authentication": 219 self.check_authentication(trans, fields) 220 # The above method does not return. 221 222 # NOTE: Permit association requests here. 223 # Otherwise, show the login form. 224 225 self.show_login(trans, fields) 226 227 def show_login(self, trans, fields): 228 229 """ 230 Writes a login screen using the transaction 'trans' and 'fields'. 231 """ 232 233 return_to = fields.get("openid.return_to", [""])[0] 234 claimed_id = fields.get("openid.claimed_id", [""])[0] 235 local_id = fields.get("openid.identity", [""])[0] 236 237 trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding)) 238 out = trans.get_response_stream() 239 out.write(self.login_page % tuple(map(cgi.escape, (return_to, claimed_id, local_id)))) 240 241 def show_success(self, trans, fields): 242 243 """ 244 Writes a success screen using the transaction 'trans', using a 245 dictionary of 'fields' providing details of the transaction. 246 """ 247 248 trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding)) 249 out = trans.get_response_stream() 250 l = [] 251 for name, values in fields.items(): 252 l.append("""<input name="%s" type="hidden" value="%s" />""" % (name, cgi.escape(values[0]))) 253 out.write(self.success_page % (fields["openid.return_to"][0], "\n".join(l))) 254 255 login_page = """ 256 <html> 257 <head> 258 <title>Login</title> 259 </head> 260 <body> 261 <h1>Login</h1> 262 <form method="POST"> 263 <p>Username: <input name="username" type="text" size="12"/></p> 264 <p>Password: <input name="password" type="password" size="12"/></p> 265 <p><input name="login" type="submit" value="Login"/></p> 266 <input name="openid.return_to" type="hidden" value="%s"/> 267 <input name="openid.claimed_id" type="hidden" value="%s"/> 268 <input name="openid.identity" type="hidden" value="%s"/> 269 </form> 270 </body> 271 </html> 272 """ 273 274 success_page = """ 275 <html> 276 <head> 277 <title>Login Example</title> 278 </head> 279 <body> 280 <h1>Login Successful</h1> 281 <form action="%s" method="POST" name="openid_redirect"> 282 %s 283 <p>Please proceed to the application: <input name="proceed" type="submit" value="Proceed!" /></p> 284 </form> 285 </body> 286 </html> 287 """ 288 289 # vim: tabstop=4 expandtab shiftwidth=4