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 0, a confirmation screen 47 is given instead of redirecting the user back to the original 48 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_body = trans.get_fields_from_body(self.encoding) 75 76 if fields_body.has_key("login"): 77 78 # Check a combination of local identifier and username together with 79 # the password. 80 81 claimed_id = fields_body.get("claimed_id", [""])[0] 82 local_id = fields_body.get("local_id", [""])[0] 83 username = fields_body.get("username", [""])[0] 84 password = fields_body.get("password", [""])[0] 85 app = fields_body.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_body.get("openid.mode", [None])[0] == "check_authentication": 96 97 # Obtain the secret key from recorded associations. 98 99 handle = fields_body.get("openid.assoc_handle", [None])[0] 100 if handle is not None and self.associations.has_key(handle): 101 valid = check_openid_signature(fields_body, 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 fields_path = trans.get_fields_from_path(self.urlencoding) 116 app = fields_path.get("openid.return_to", [""])[0] 117 claimed_id = fields_path.get("openid.claimed_id", [""])[0] 118 local_id = fields_path.get("openid.identity", [""])[0] 119 120 self.show_login(trans, app, claimed_id, local_id) 121 122 def _redirect(self, trans, claimed_id, local_id, username, app): 123 124 """ 125 Redirect the client using 'trans', 'claimed_id', 'local_id', 'username' 126 and the given 'app' details. 127 """ 128 129 app_url = self.app_url + trans.get_path_without_query(self.urlencoding) 130 131 # Make an association that can be used in signature verification. 132 # NOTE: Probably need to consider the secret key a bit more. 133 134 handle = username + str(time.time()) 135 secret_key = str(random.randint(0, 999999999)) 136 self.associations[handle] = secret_key 137 138 # Make a timestamp. 139 140 now = datetime.datetime.utcnow() 141 timestamp = now.strftime("%Y-%m-%dT%H:%M:%SZ") + str(now.microsecond) 142 143 # Make a signature. 144 145 signed_names = ["op_endpoint", "return_to", "response_nonce", "assoc_handle", "claimed_id", "identity"] 146 fields = { 147 "openid.op_endpoint" : [app_url], 148 "openid.return_to" : [app], 149 "openid.response_nonce" : [timestamp], 150 "openid.assoc_handle" : [handle], 151 "openid.claimed_id" : [claimed_id], 152 "openid.identity" : [local_id] 153 } 154 155 signature = make_openid_signature(signed_names, fields, secret_key) 156 157 # Build an URL for returning to the application. 158 159 url = "%s?openid.ns=%s&openid.mode=%s&openid.signed=%s&openid.sig=%s" % ( 160 app, 161 trans.encode_path(self.openid_ns, self.urlencoding), 162 trans.encode_path("id_res", self.urlencoding), 163 trans.encode_path(",".join(signed_names), self.urlencoding), 164 trans.encode_path(signature, self.urlencoding) 165 ) 166 167 for name, value in fields.items(): 168 url += "&%s=%s" % (name, trans.encode_path(value[0], self.urlencoding)) 169 170 # Show the success page anyway. 171 # NOTE: Offer a POST-based form for redirection. 172 173 self.show_success(trans, url) 174 if self.use_redirect: 175 trans.redirect(url) 176 else: 177 raise WebStack.Generic.EndOfResponse 178 179 def show_verification(self, trans, status): 180 181 """ 182 Writes a signature verification response using the transaction 'trans' 183 and the 'status' of the verification. 184 """ 185 186 trans.set_content_type(WebStack.Generic.ContentType("text/plain")) 187 out = trans.get_response_stream() 188 189 # NOTE: Need to use invalidate_handle, too. 190 191 if status: 192 status_str = "true" 193 else: 194 status_str = "false" 195 out.write("ns:%s\nis_valid:%s\n" % (self.openid_ns, status_str)) 196 raise WebStack.Generic.EndOfResponse 197 198 def show_login(self, trans, app, claimed_id, local_id): 199 200 """ 201 Writes a login screen using the transaction 'trans', including details 202 of the 'app' which the client was attempting to access, along with the 203 'claimed_id' and 'local_id'. 204 """ 205 206 trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding)) 207 out = trans.get_response_stream() 208 out.write(self.login_page % (app, claimed_id, local_id)) 209 210 def show_success(self, trans, app): 211 212 """ 213 Writes a success screen using the transaction 'trans', including details 214 of the 'app' which the client was attempting to access. 215 """ 216 217 trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding)) 218 out = trans.get_response_stream() 219 out.write(self.success_page % (app, app)) 220 221 login_page = """ 222 <html> 223 <head> 224 <title>Login</title> 225 </head> 226 <body> 227 <h1>Login</h1> 228 <form method="POST"> 229 <p>Username: <input name="username" type="text" size="12"/></p> 230 <p>Password: <input name="password" type="password" size="12"/></p> 231 <p><input name="login" type="submit" value="Login"/></p> 232 <input name="app" type="hidden" value="%s"/> 233 <input name="claimed_id" type="hidden" value="%s"/> 234 <input name="local_id" type="hidden" value="%s"/> 235 </form> 236 </body> 237 </html> 238 """ 239 240 success_page = """ 241 <html> 242 <head> 243 <title>Login Example</title> 244 </head> 245 <body> 246 <h1>Login Successful</h1> 247 <p>Please proceed to the application: <a href="%s">%s</a></p> 248 </body> 249 </html> 250 """ 251 252 # vim: tabstop=4 expandtab shiftwidth=4