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