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