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