001 /* 002 Licensed to the Apache Software Foundation (ASF) under one 003 or more contributor license agreements. See the NOTICE file 004 distributed with this work for additional information 005 regarding copyright ownership. The ASF licenses this file 006 to you under the Apache License, Version 2.0 (the 007 "License"); you may not use this file except in compliance 008 with the License. You may obtain a copy of the License at 009 010 http://www.apache.org/licenses/LICENSE-2.0 011 012 Unless required by applicable law or agreed to in writing, 013 software distributed under the License is distributed on an 014 "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 KIND, either express or implied. See the License for the 016 specific language governing permissions and limitations 017 under the License. 018 */ 019 package org.apache.wiki.auth.login; 020 021 import java.io.*; 022 import java.util.UUID; 023 024 import javax.security.auth.callback.Callback; 025 import javax.security.auth.callback.UnsupportedCallbackException; 026 import javax.security.auth.login.LoginException; 027 import javax.servlet.http.Cookie; 028 import javax.servlet.http.HttpServletRequest; 029 import javax.servlet.http.HttpServletResponse; 030 031 import org.apache.log4j.Logger; 032 import org.apache.wiki.WikiEngine; 033 import org.apache.wiki.auth.WikiPrincipal; 034 import org.apache.wiki.util.FileUtil; 035 import org.apache.wiki.util.HttpUtil; 036 import org.apache.wiki.util.TextUtil; 037 038 /** 039 * Logs in an user based on a cookie stored in the user's computer. The cookie 040 * information is stored in the <code>jspwiki.workDir</code>, under the directory 041 * {@value #COOKIE_DIR}. For security purposes it is a very, very good idea 042 * to prevent access to this directory by everyone except the web server process; 043 * otherwise people having read access to this directory may be able to spoof 044 * other users. 045 * <p> 046 * The cookie directory is scrubbed of old entries at regular intervals. 047 * <p> 048 * This module must be used with a CallbackHandler (such as 049 * {@link WebContainerCallbackHandler}) that supports the following Callback 050 * types: 051 * </p> 052 * <ol> 053 * <li>{@link HttpRequestCallback}- supplies the cookie, which should contain 054 * an unique id for fetching the UID.</li> 055 * <li>{@link WikiEngineCallback} - allows access to the WikiEngine itself. 056 * </ol> 057 * <p> 058 * After authentication, a generic WikiPrincipal based on the username will be 059 * created and associated with the Subject. 060 * </p> 061 * @see javax.security.auth.spi.LoginModule#commit() 062 * @see CookieAssertionLoginModule 063 * @since 2.5.62 064 */ 065 public class CookieAuthenticationLoginModule extends AbstractLoginModule 066 { 067 068 private static final Logger log = Logger.getLogger( CookieAuthenticationLoginModule.class ); 069 private static final String LOGIN_COOKIE_NAME = "JSPWikiUID"; 070 071 /** The directory name under which the cookies are stored. The value is {@value}. */ 072 protected static final String COOKIE_DIR = "logincookies"; 073 074 /** 075 * User property for setting how long the cookie is stored on the user's computer. 076 * The value is {@value}. The default expiry time is 14 days. 077 */ 078 public static final String PROP_LOGIN_EXPIRY_DAYS = "jspwiki.cookieAuthentication.expiry"; 079 080 /** 081 * Built-in value for storing the cookie. 082 */ 083 private static final int DEFAULT_EXPIRY_DAYS = 14; 084 085 private static long c_lastScrubTime = 0L; 086 087 /** Describes how often we scrub the cookieDir directory. 088 */ 089 private static final long SCRUB_PERIOD = 60*60*1000L; // In milliseconds 090 091 /** 092 * @see javax.security.auth.spi.LoginModule#login() 093 * 094 * {@inheritDoc} 095 */ 096 public boolean login() throws LoginException 097 { 098 // Otherwise, let's go and look for the cookie! 099 HttpRequestCallback hcb = new HttpRequestCallback(); 100 //UserDatabaseCallback ucb = new UserDatabaseCallback(); 101 WikiEngineCallback wcb = new WikiEngineCallback(); 102 103 Callback[] callbacks = new Callback[] 104 { hcb, wcb }; 105 106 try 107 { 108 m_handler.handle( callbacks ); 109 110 HttpServletRequest request = hcb.getRequest(); 111 String uid = getLoginCookie( request ); 112 113 if( uid != null ) 114 { 115 WikiEngine engine = wcb.getEngine(); 116 File cookieFile = getCookieFile(engine, uid); 117 118 if( cookieFile != null && cookieFile.exists() && cookieFile.canRead() ) 119 { 120 Reader in = null; 121 122 try 123 { 124 in = new BufferedReader( new InputStreamReader( new FileInputStream( cookieFile ), "UTF-8" ) ); 125 String username = FileUtil.readContents( in ); 126 127 if ( log.isDebugEnabled() ) 128 { 129 log.debug( "Logged in cookie authenticated name=" + username ); 130 } 131 132 // If login succeeds, commit these principals/roles 133 m_principals.add( new WikiPrincipal( username, WikiPrincipal.LOGIN_NAME ) ); 134 135 // 136 // Tag the file so that we know that it has been accessed recently. 137 // 138 cookieFile.setLastModified( System.currentTimeMillis() ); 139 140 return true; 141 } 142 catch( IOException e ) 143 { 144 return false; 145 } 146 finally 147 { 148 if( in != null ) in.close(); 149 } 150 } 151 } 152 } 153 catch( IOException e ) 154 { 155 String message = "IO exception; disallowing login."; 156 log.error( message, e ); 157 throw new LoginException( message ); 158 } 159 catch( UnsupportedCallbackException e ) 160 { 161 String message = "Unable to handle callback; disallowing login."; 162 log.error( message, e ); 163 throw new LoginException( message ); 164 } 165 166 return false; 167 } 168 169 /** 170 * Attempts to locate the cookie file. 171 * @param engine WikiEngine 172 * @param uid An unique ID fetched from the user cookie 173 * @return A File handle, or null, if there was a problem. 174 */ 175 private static File getCookieFile(WikiEngine engine, String uid) 176 { 177 File cookieDir = new File( engine.getWorkDir(), COOKIE_DIR ); 178 179 if( !cookieDir.exists() ) 180 { 181 cookieDir.mkdirs(); 182 } 183 184 if( !cookieDir.canRead() ) 185 { 186 log.error("Cannot read from cookie directory!"+cookieDir.getAbsolutePath()); 187 return null; 188 } 189 190 if( !cookieDir.canWrite() ) 191 { 192 log.error("Cannot write to cookie directory!"+cookieDir.getAbsolutePath()); 193 return null; 194 } 195 196 // 197 // Scrub away old files 198 // 199 long now = System.currentTimeMillis(); 200 201 if( now > (c_lastScrubTime+SCRUB_PERIOD ) ) 202 { 203 scrub( TextUtil.getIntegerProperty( engine.getWikiProperties(), 204 PROP_LOGIN_EXPIRY_DAYS, 205 DEFAULT_EXPIRY_DAYS ), 206 cookieDir ); 207 c_lastScrubTime = now; 208 } 209 210 // 211 // Find the cookie file 212 // 213 File cookieFile = new File( cookieDir, uid ); 214 return cookieFile; 215 } 216 217 /** 218 * Extracts the login cookie UID from the servlet request. 219 * 220 * @param request The HttpServletRequest 221 * @return The UID value from the cookie, or null, if no such cookie exists. 222 */ 223 private static String getLoginCookie(HttpServletRequest request) 224 { 225 String cookie = HttpUtil.retrieveCookieValue( request, LOGIN_COOKIE_NAME ); 226 227 return cookie; 228 } 229 230 /** 231 * Sets a login cookie based on properties set by the user. This method also 232 * creates the cookie uid-username mapping in the work directory. 233 * 234 * @param engine The WikiEngine 235 * @param response The HttpServletResponse 236 * @param username The username for whom to create the cookie. 237 */ 238 // FIXME3.0: For 3.0, switch to using java.util.UUID so that we can 239 // get rid of jug.jar 240 public static void setLoginCookie( WikiEngine engine, HttpServletResponse response, String username ) 241 { 242 UUID uid = UUID.randomUUID(); 243 244 int days = TextUtil.getIntegerProperty( engine.getWikiProperties(), 245 PROP_LOGIN_EXPIRY_DAYS, 246 DEFAULT_EXPIRY_DAYS ); 247 248 Cookie userId = new Cookie( LOGIN_COOKIE_NAME, uid.toString() ); 249 userId.setMaxAge( days * 24 * 60 * 60 ); 250 response.addCookie( userId ); 251 252 File cf = getCookieFile( engine, uid.toString() ); 253 Writer out = null; 254 255 try 256 { 257 // 258 // Write the cookie content to the cookie store file. 259 // 260 out = new BufferedWriter( new OutputStreamWriter( new FileOutputStream(cf), "UTF-8" ) ); 261 FileUtil.copyContents( new StringReader(username), out ); 262 263 if( log.isDebugEnabled() ) 264 { 265 log.debug( "Created login cookie for user "+username+" for "+days+" days" ); 266 } 267 } 268 catch( IOException ex ) 269 { 270 log.error("Unable to create cookie file to store user id: "+uid); 271 } 272 finally 273 { 274 if( out != null ) 275 { 276 try 277 { 278 out.close(); 279 } 280 catch( IOException ex ) 281 { 282 log.error("Unable to close stream"); 283 } 284 } 285 } 286 } 287 288 /** 289 * Clears away the login cookie, and removes the uid-username mapping file as well. 290 * 291 * @param engine WikiEngine 292 * @param request Servlet request 293 * @param response Servlet response 294 */ 295 public static void clearLoginCookie( WikiEngine engine, HttpServletRequest request, HttpServletResponse response ) 296 { 297 Cookie userId = new Cookie( LOGIN_COOKIE_NAME, "" ); 298 userId.setMaxAge( 0 ); 299 response.addCookie( userId ); 300 301 String uid = getLoginCookie( request ); 302 303 if( uid != null ) 304 { 305 File cf = getCookieFile( engine, uid ); 306 307 if( cf != null ) 308 { 309 cf.delete(); 310 } 311 } 312 } 313 314 /** 315 * Goes through the cookie directory and removes any obsolete files. 316 * The scrubbing takes place one day after the cookie was supposed to expire. 317 * However, if the user has logged in during the expiry period, the expiry is 318 * reset, and the cookie file left here. 319 * 320 * @param days 321 * @param cookieDir 322 */ 323 private static synchronized void scrub( int days, File cookieDir ) 324 { 325 log.debug("Scrubbing cookieDir..."); 326 327 File[] files = cookieDir.listFiles(); 328 329 long obsoleteDateLimit = System.currentTimeMillis() - ((long)days+1) * 24 * 60 * 60 * 1000L; 330 331 int deleteCount = 0; 332 333 for( int i = 0; i < files.length; i++ ) 334 { 335 File f = files[i]; 336 337 long lastModified = f.lastModified(); 338 339 if( lastModified < obsoleteDateLimit ) 340 { 341 f.delete(); 342 deleteCount++; 343 } 344 } 345 346 log.debug("Removed "+deleteCount+" obsolete cookie logins"); 347 } 348 }