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.logging.log4j.LogManager; 022import org.apache.logging.log4j.Logger; 023import org.apache.wiki.api.core.Engine; 024import org.apache.wiki.auth.WikiPrincipal; 025import org.apache.wiki.util.FileUtil; 026import org.apache.wiki.util.HttpUtil; 027import org.apache.wiki.util.TextUtil; 028 029import javax.security.auth.callback.Callback; 030import javax.security.auth.callback.UnsupportedCallbackException; 031import javax.security.auth.login.LoginException; 032import javax.servlet.http.Cookie; 033import javax.servlet.http.HttpServletRequest; 034import javax.servlet.http.HttpServletResponse; 035import java.io.BufferedReader; 036import java.io.BufferedWriter; 037import java.io.File; 038import java.io.IOException; 039import java.io.InputStreamReader; 040import java.io.OutputStreamWriter; 041import java.io.Reader; 042import java.io.StringReader; 043import java.io.Writer; 044import java.nio.charset.StandardCharsets; 045import java.nio.file.Files; 046import java.util.UUID; 047 048 049/** 050 * Logs in a 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 = LogManager.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; 096 097 /** 098 * Describes how often we scrub the cookieDir directory. 099 */ 100 private static final long SCRUB_PERIOD = 60 * 60 * 1_000L; // In milliseconds 101 102 /** 103 * {@inheritDoc} 104 * 105 * @see javax.security.auth.spi.LoginModule#login() 106 */ 107 @Override 108 public boolean login() throws LoginException { 109 // Otherwise, let's go and look for the cookie! 110 final HttpRequestCallback hcb = new HttpRequestCallback(); 111 final WikiEngineCallback wcb = new WikiEngineCallback(); 112 final Callback[] callbacks = new Callback[] { hcb, wcb }; 113 114 try { 115 m_handler.handle( callbacks ); 116 final HttpServletRequest request = hcb.getRequest(); 117 final String uid = getLoginCookie( request ); 118 119 if( uid != null ) { 120 final Engine engine = wcb.getEngine(); 121 final File cookieFile = getCookieFile( engine, uid ); 122 if( cookieFile != null && cookieFile.exists() && cookieFile.canRead() ) { 123 try( final Reader in = new BufferedReader( new InputStreamReader( Files.newInputStream( cookieFile.toPath() ), StandardCharsets.UTF_8 ) ) ) { 124 final String username = FileUtil.readContents( in ); 125 if( log.isDebugEnabled() ) { 126 log.debug( "Logged in cookie authenticated name={}", username ); 127 } 128 129 // If login succeeds, commit these principals/roles 130 m_principals.add( new WikiPrincipal( username, WikiPrincipal.LOGIN_NAME ) ); 131 132 // Tag the file so that we know that it has been accessed recently. 133 return cookieFile.setLastModified( System.currentTimeMillis() ); 134 135 } catch( final IOException e ) { 136 return false; 137 } 138 } 139 } 140 } catch( final IOException e ) { 141 final String message = "IO exception; disallowing login."; 142 log.error( message, e ); 143 throw new LoginException( message ); 144 } catch( final UnsupportedCallbackException e ) { 145 final String message = "Unable to handle callback; disallowing login."; 146 log.error( message, e ); 147 throw new LoginException( message ); 148 } 149 return false; 150 } 151 152 /** 153 * Attempts to locate the cookie file. 154 * 155 * @param engine Engine 156 * @param uid An unique ID fetched from the user cookie 157 * @return A File handle, or null, if there was a problem. 158 */ 159 private static File getCookieFile( final Engine engine, final String uid ) { 160 final File cookieDir = new File( engine.getWorkDir(), COOKIE_DIR ); 161 if( !cookieDir.exists() ) { 162 cookieDir.mkdirs(); 163 } 164 if( !cookieDir.canRead() ) { 165 log.error( "Cannot read from cookie directory! {}", cookieDir.getAbsolutePath() ); 166 return null; 167 } 168 if( !cookieDir.canWrite() ) { 169 log.error( "Cannot write to cookie directory! {}", cookieDir.getAbsolutePath() ); 170 return null; 171 } 172 173 // Scrub away old files 174 final long now = System.currentTimeMillis(); 175 if( now > ( c_lastScrubTime + SCRUB_PERIOD ) ) { 176 scrub( TextUtil.getIntegerProperty( engine.getWikiProperties(), PROP_LOGIN_EXPIRY_DAYS, DEFAULT_EXPIRY_DAYS ), cookieDir ); 177 c_lastScrubTime = now; 178 } 179 180 // Find the cookie file 181 final File file = new File( cookieDir, uid ); 182 try { 183 if( file.getCanonicalPath().startsWith( cookieDir.getCanonicalPath() ) ) { 184 return file; 185 } 186 } catch( final IOException e ) { 187 log.error( "Problem retrieving login cookie, returning null: {}", e.getMessage() ); 188 return null; 189 } 190 return null; 191 } 192 193 /** 194 * Extracts the login cookie UID from the servlet request. 195 * 196 * @param request The HttpServletRequest 197 * @return The UID value from the cookie, or null, if no such cookie exists. 198 */ 199 private static String getLoginCookie( final HttpServletRequest request ) { 200 return HttpUtil.retrieveCookieValue( request, LOGIN_COOKIE_NAME ); 201 } 202 203 /** 204 * Sets a login cookie based on properties set by the user. This method also 205 * creates the cookie uid-username mapping in the work directory. 206 * 207 * @param engine The Engine 208 * @param response The HttpServletResponse 209 * @param username The username for whom to create the cookie. 210 */ 211 public static void setLoginCookie( final Engine engine, final HttpServletResponse response, final String username ) { 212 final UUID uid = UUID.randomUUID(); 213 final int days = TextUtil.getIntegerProperty( engine.getWikiProperties(), PROP_LOGIN_EXPIRY_DAYS, DEFAULT_EXPIRY_DAYS ); 214 final Cookie userId = getLoginCookie( uid.toString() ); 215 userId.setMaxAge( days * 24 * 60 * 60 ); 216 response.addCookie( userId ); 217 218 final File cf = getCookieFile( engine, uid.toString() ); 219 if( cf != null ) { 220 // Write the cookie content to the cookie store file. 221 try( final Writer out = new BufferedWriter( new OutputStreamWriter( Files.newOutputStream( cf.toPath() ), StandardCharsets.UTF_8 ) ) ) { 222 FileUtil.copyContents( new StringReader( username ), out ); 223 if( log.isDebugEnabled() ) { 224 log.debug( "Created login cookie for user {} for {} days", username, days ); 225 } 226 } catch( final IOException ex ) { 227 log.error( "Unable to create cookie file to store user id: {}", uid ); 228 } 229 } 230 } 231 232 /** 233 * Clears away the login cookie, and removes the uid-username mapping file as well. 234 * 235 * @param engine Engine 236 * @param request Servlet request 237 * @param response Servlet response 238 */ 239 public static void clearLoginCookie( final Engine engine, final HttpServletRequest request, final HttpServletResponse response ) { 240 final Cookie userId = getLoginCookie( "" ); 241 userId.setMaxAge( 0 ); 242 response.addCookie( userId ); 243 final String uid = getLoginCookie( request ); 244 if( uid != null ) { 245 final File cf = getCookieFile( engine, uid ); 246 if( cf != null ) { 247 if( !cf.delete() ) { 248 log.debug( "Error deleting cookie login {}", uid ); 249 } 250 } 251 } 252 } 253 254 /** 255 * Helper function to get secure LOGIN cookie 256 * 257 * @param value of the cookie 258 */ 259 private static Cookie getLoginCookie( final String value ) { 260 final Cookie c = new Cookie( LOGIN_COOKIE_NAME, value ); 261 c.setHttpOnly( true ); // no browser access 262 c.setSecure( true ); // only access via encrypted https allowed 263 return c; 264 } 265 266 /** 267 * Goes through the cookie directory and removes any obsolete files. 268 * The scrubbing takes place one day after the cookie was supposed to expire. 269 * However, if the user has logged in during the expiry period, the expiry is 270 * reset, and the cookie file left here. 271 * 272 * @param days number of days that the cookie will survive 273 * @param cookieDir cookie directory 274 */ 275 private static synchronized void scrub( final int days, final File cookieDir ) { 276 log.debug( "Scrubbing cookieDir..." ); 277 final File[] files = cookieDir.listFiles(); 278 final long obsoleteDateLimit = System.currentTimeMillis() - ( ( long )days + 1 ) * 24 * 60 * 60 * 1000L; 279 int deleteCount = 0; 280 281 for( int i = 0; i < files.length; i++ ) { 282 final File f = files[ i ]; 283 final long lastModified = f.lastModified(); 284 if( lastModified < obsoleteDateLimit ) { 285 if( f.delete() ) { 286 deleteCount++; 287 } else { 288 log.debug( "Error deleting cookie login with index {}", i ); 289 } 290 } 291 } 292 293 log.debug( "Removed {} obsolete cookie logins", deleteCount ); 294 } 295 296}