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