1 #!/usr/bin/env python 2 3 """ 4 Login resources which redirect clients back to an application after a successful 5 login. 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 get_token 26 27 class LoginResource: 28 29 "A resource providing a login screen." 30 31 encoding = "utf-8" 32 33 def __init__(self, authenticator, use_redirect=1, urlencoding=None): 34 35 """ 36 Initialise the resource with an 'authenticator'. 37 38 If the optional 'use_redirect' flag is set to 0, a confirmation screen 39 is given instead of redirecting the user back to the original 40 application. 41 42 The optional 'urlencoding' parameter allows a special encoding to be 43 used in producing the redirection path. 44 45 To change the pages employed by this resource, either redefine the 46 'login_page' and 'success_page' attributes in instances of this class or 47 a subclass, or override the 'show_login' and 'show_success' methods. 48 """ 49 50 self.authenticator = authenticator 51 self.use_redirect = use_redirect 52 self.urlencoding = urlencoding or self.encoding 53 54 def respond(self, trans): 55 56 "Respond using the transaction 'trans'." 57 58 app, path, qs = get_target(trans, self.urlencoding, self.encoding) 59 60 # Check for a submitted login form. 61 62 fields_body = trans.get_fields_from_body(self.encoding) 63 64 if fields_body.has_key("login"): 65 if self.authenticator.authenticate(trans, fields_body.get("username", [None])[0], fields_body.get("password", [None])[0]): 66 self._redirect(trans, app, path, qs) 67 # The above method does not return. 68 69 # Otherwise, show the login form. 70 71 self.show_login(trans, app, path, qs) 72 73 def _redirect(self, trans, app, path, qs): 74 75 """ 76 Redirect the client using 'trans' and the given 'app', 'path' and 'qs' 77 details. 78 """ 79 80 # Show the success page anyway. 81 82 self.show_success(trans, app, path, qs) 83 if self.use_redirect: 84 trans.redirect(app + trans.encode_path(path, self.urlencoding) + qs) 85 else: 86 raise WebStack.Generic.EndOfResponse 87 88 def show_login(self, trans, app, path, qs): 89 90 """ 91 Writes a login screen using the transaction 'trans', including details 92 of the 'app', 'path' and 'qs' which the client was attempting to access. 93 """ 94 95 trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding)) 96 out = trans.get_response_stream() 97 out.write(self.login_page % (app, path, qs)) 98 99 def show_success(self, trans, app, path, qs): 100 101 """ 102 Writes a success screen using the transaction 'trans', including details 103 of the 'app', 'path' and 'qs' which the client was attempting to access. 104 """ 105 106 # When authentication fails or is yet to take place, show the login 107 # screen. 108 109 trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding)) 110 out = trans.get_response_stream() 111 out.write(self.success_page % (app, trans.encode_path(path, self.urlencoding), qs)) 112 113 login_page = """ 114 <html> 115 <head> 116 <title>Login</title> 117 </head> 118 <body> 119 <h1>Login</h1> 120 <form method="POST"> 121 <p>Username: <input name="username" type="text" size="12"/></p> 122 <p>Password: <input name="password" type="password" size="12"/></p> 123 <p><input name="login" type="submit" value="Login"/></p> 124 <input name="app" type="hidden" value="%s"/> 125 <input name="path" type="hidden" value="%s"/> 126 <input name="qs" type="hidden" value="%s"/> 127 </form> 128 </body> 129 </html> 130 """ 131 132 success_page = """ 133 <html> 134 <head> 135 <title>Login Example</title> 136 </head> 137 <body> 138 <h1>Login Successful</h1> 139 <p>Please proceed <a href="%s%s%s">to the application</a>.</p> 140 </body> 141 </html> 142 """ 143 144 class LoginAuthenticator: 145 146 def __init__(self, secret_key, credentials, cookie_name=None): 147 148 """ 149 Initialise the authenticator with a 'secret_key', the authenticator's registry of 150 'credentials' and an optional 'cookie_name'. 151 152 The 'credentials' must be an object which supports tests of the form 153 '(username, password) in credentials'. 154 """ 155 156 self.secret_key = secret_key 157 self.credentials = credentials 158 self.cookie_name = cookie_name or "LoginAuthenticator" 159 160 def authenticate(self, trans, username, password): 161 162 """ 163 Authenticate the sender of the transaction 'trans', returning 1 (true) if they are 164 recognised, 0 (false) otherwise. Use the 'username' and 'password' supplied as 165 credentials. 166 """ 167 168 # Process any supplied parameters. 169 170 fields = trans.get_fields_from_body() 171 172 # Check against the class's credentials. 173 174 if (username, password) in self.credentials: 175 176 # Make a special cookie token. 177 178 self.set_token(trans, username) 179 return 1 180 181 return 0 182 183 def set_token(self, trans, username): 184 185 "Set an authentication token in 'trans' with the given 'username'." 186 187 trans.set_cookie_value( 188 self.cookie_name, 189 get_token(username, self.secret_key), 190 path="/" 191 ) 192 193 # General functions. 194 195 def get_target(trans, urlencoding, encoding): 196 197 """ 198 Return the application, path and query string for 'trans' using the given 199 'urlencoding' (or path encoding) and request body 'encoding'. 200 """ 201 202 fields_path = trans.get_fields_from_path(urlencoding) 203 fields_body = trans.get_fields_from_body(encoding) 204 205 # NOTE: Handle missing redirects better. 206 207 if fields_body.has_key("app"): 208 apps = fields_body["app"] 209 app = apps[0] 210 elif fields_path.has_key("app"): 211 apps = fields_path["app"] 212 app = apps[0] 213 else: 214 app = u"" 215 216 if fields_body.has_key("path"): 217 paths = fields_body["path"] 218 path = paths[0] 219 elif fields_path.has_key("path"): 220 paths = fields_path["path"] 221 path = paths[0] 222 else: 223 path = u"" 224 225 if fields_body.has_key("qs"): 226 qss = fields_body["qs"] 227 qs = qss[0] 228 elif fields_path.has_key("qs"): 229 qss = fields_path["qs"] 230 qs = qss[0] 231 else: 232 qs = u"" 233 234 return app, path, qs 235 236 # vim: tabstop=4 expandtab shiftwidth=4