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