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