1 #!/usr/bin/env python 2 3 """ 4 Login redirection resources, sending unauthenticated users to a login screen 5 URL. 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 from WebStack.Helpers.Auth import get_token 25 import WebStack.Generic 26 27 class LoginRedirectResource: 28 29 "A resource redirecting to a login URL." 30 31 encoding = "utf-8" 32 33 def __init__(self, resource, authenticator, login_url=None, app_url=None, 34 anonymous_parameter_name=None, anonymous_username="anonymous", 35 logout_parameter_name=None, logout_url="/", use_logout_redirect=1, 36 urlencoding=None, path_encoding=None): 37 38 """ 39 Initialise the resource with a 'resource' for the application being 40 protected and an 'authenticator' protecting the resource. 41 42 If the optional 'login_url' and 'app_url' are provided, these values 43 will be used to locate the login application and protected application 44 respectively. Such values, if not provided, must be otherwise set at a 45 later time or provided by 'get_login_url' and 'get_app_url' methods in 46 a subclass of this class. 47 48 If the optional 'anonymous_parameter_name' is set, clients providing a 49 parameter of that name in the URL will not be authenticated, but then 50 such clients will get a predefined user identity associated with them, 51 configurable using the optional 'anonymous_username'. 52 53 If the optional 'logout_parameter_name' is set, clients providing a 54 parameter of that name in the URL will become logged out. After logging 55 out, clients are redirected to a location which can be configured by the 56 optional 'logout_url'. 57 58 If the optional 'use_logout_redirect' flag is set to 0, a confirmation 59 screen is given instead of redirecting the user to the 'logout_url'. 60 61 The optional 'path_encoding' parameter (previously 'urlencoding', which 62 is still supported) allows a special encoding to be used in producing 63 the redirection path. 64 65 To change the page used by this resource, either redefine the 66 'logout_page' attribute in instances of this class or a subclass, or 67 override the 'show_logout' method. 68 """ 69 70 self.login_url = login_url 71 self.app_url = app_url 72 self.resource = resource 73 self.authenticator = authenticator 74 self.anonymous_parameter_name = anonymous_parameter_name 75 self.anonymous_username = anonymous_username 76 self.logout_parameter_name = logout_parameter_name 77 self.logout_url = logout_url 78 self.use_logout_redirect = use_logout_redirect 79 self.path_encoding = path_encoding or urlencoding or self.encoding 80 81 def respond(self, trans): 82 83 "Respond using the given transaction 'trans'." 84 85 fields_path = trans.get_fields_from_path(self.path_encoding) 86 87 # Check for the logout parameter, if appropriate. 88 89 if self.logout_parameter_name is not None and fields_path.has_key(self.logout_parameter_name): 90 91 # Remove the special cookie token, then pass on the transaction. 92 93 self.authenticator.unset_token(trans) 94 95 # Redirect to the logout URL. 96 97 if self.use_logout_redirect: 98 trans.set_header_value("Location", self.logout_url) 99 trans.set_response_code(302) # was 307 100 101 # Show the logout confirmation anyway. 102 103 self.show_logout(trans, self.logout_url) 104 105 # Check the authentication details with the specified authenticator. 106 107 elif self.authenticator.authenticate(trans): 108 109 # If successful, pass on the transaction. 110 111 self.resource.respond(trans) 112 113 # Check for the anonymous parameter, if appropriate. 114 115 elif self.anonymous_parameter_name is not None and fields_path.has_key(self.anonymous_parameter_name): 116 117 # Make a special cookie token, then pass on the transaction. 118 119 self.authenticator.set_token(trans, self.anonymous_username) 120 self.resource.respond(trans) 121 122 else: 123 124 # Redirect to the login URL. 125 126 path = trans.get_path_without_query(self.path_encoding) 127 qs = trans.get_query_string() 128 if qs: 129 qs = "?" + qs 130 trans.redirect("%s?app=%s&path=%s&qs=%s" % ( 131 self.get_login_url(trans), 132 trans.encode_path(self.get_app_url(trans), self.path_encoding), 133 trans.encode_path(path, self.path_encoding), 134 trans.encode_path(qs, self.path_encoding)) 135 ) 136 137 def get_app_url(self, trans): 138 139 """ 140 Return the application URL, using 'trans' if necessary, in order to 141 provide a complete URL to redirect an authenticated user to their 142 originally requested page. If the application URL is empty, any 143 redirects will be within the same application, rather than to 144 potentially completely different applications residing at arbitrary 145 locations. 146 """ 147 148 return self.app_url 149 150 def get_login_url(self, trans): 151 152 """ 153 Return the login URL, using 'trans' if necessary, in order to 154 provide a complete URL to redirect an authenticated user to their 155 originally requested page. 156 """ 157 158 return self.login_url 159 160 def show_logout(self, trans, redirect): 161 162 """ 163 Write a confirmation page to 'trans' containing the 'redirect' URL which the 164 client should be sent to upon logout. 165 """ 166 167 # When logout takes place, show the logout screen. 168 169 trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding)) 170 out = trans.get_response_stream() 171 out.write(self.logout_page % redirect) 172 173 logout_page = """ 174 <html> 175 <head> 176 <title>Logout</title> 177 </head> 178 <body> 179 <h1>Logout Successful</h1> 180 <p>Please proceed <a href="%s">to the application</a>.</p> 181 </body> 182 </html> 183 """ 184 185 class LoginRedirectAuthenticator: 186 187 """ 188 An authenticator which verifies the credentials provided in a special login cookie. 189 """ 190 191 def __init__(self, secret_key, cookie_name=None): 192 193 "Initialise the authenticator with a 'secret_key' and an optional 'cookie_name'." 194 195 self.secret_key = secret_key 196 self.cookie_name = cookie_name or "LoginAuthenticator" 197 198 def authenticate(self, trans): 199 200 """ 201 Authenticate the originator of 'trans', updating the object if successful and 202 returning 1 (true) if successful, 0 (false) otherwise. 203 """ 204 205 cookie = trans.get_cookie(self.cookie_name) 206 if cookie is None or cookie.value is None: 207 return 0 208 209 # Test the token from the cookie against a recreated token using the 210 # given information. 211 212 username = cookie.value.split(":")[0] 213 if cookie.value == get_token(username, self.secret_key): 214 215 # Update the transaction with the user details. 216 217 trans.set_user(username) 218 return 1 219 else: 220 return 0 221 222 def set_token(self, trans, username): 223 224 "Set an authentication token in 'trans' with the given 'username'." 225 226 trans.set_cookie_value( 227 self.cookie_name, 228 get_token(username, self.secret_key), 229 path="/" 230 ) 231 232 # Update the transaction with the user details. 233 234 trans.set_user(username) 235 236 def unset_token(self, trans): 237 238 "Unset the authentication token in 'trans'." 239 240 trans.delete_cookie(self.cookie_name) 241 242 # vim: tabstop=4 expandtab shiftwidth=4