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