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 LOG.debug( "Logged in cookie authenticated name={}", username ); 126 127 // If login succeeds, commit these principals/roles 128 m_principals.add( new WikiPrincipal( username, WikiPrincipal.LOGIN_NAME ) ); 129 130 // Tag the file so that we know that it has been accessed recently. 131 return cookieFile.setLastModified( System.currentTimeMillis() ); 132 133 } catch( final IOException e ) { 134 return false; 135 } 136 } 137 } 138 } catch( final IOException e ) { 139 final String message = "IO exception; disallowing login."; 140 LOG.error( message, e ); 141 throw new LoginException( message ); 142 } catch( final UnsupportedCallbackException e ) { 143 final String message = "Unable to handle callback; disallowing login."; 144 LOG.error( message, e ); 145 throw new LoginException( message ); 146 } 147 return false; 148 } 149 150 /** 151 * Attempts to locate the cookie file. 152 * 153 * @param engine Engine 154 * @param uid An unique ID fetched from the user cookie 155 * @return A File handle, or null, if there was a problem. 156 */ 157 private static File getCookieFile( final Engine engine, final String uid ) { 158 final File cookieDir = new File( engine.getWorkDir(), COOKIE_DIR ); 159 if( !cookieDir.exists() ) { 160 cookieDir.mkdirs(); 161 } 162 if( !cookieDir.canRead() ) { 163 LOG.error( "Cannot read from cookie directory! {}", cookieDir.getAbsolutePath() ); 164 return null; 165 } 166 if( !cookieDir.canWrite() ) { 167 LOG.error( "Cannot write to cookie directory! {}", cookieDir.getAbsolutePath() ); 168 return null; 169 } 170 171 // Scrub away old files 172 final long now = System.currentTimeMillis(); 173 if( now > ( c_lastScrubTime + SCRUB_PERIOD ) ) { 174 scrub( TextUtil.getIntegerProperty( engine.getWikiProperties(), PROP_LOGIN_EXPIRY_DAYS, DEFAULT_EXPIRY_DAYS ), cookieDir ); 175 c_lastScrubTime = now; 176 } 177 178 // Find the cookie file 179 final File file = new File( cookieDir, uid ); 180 try { 181 if( file.getCanonicalPath().startsWith( cookieDir.getCanonicalPath() ) ) { 182 return file; 183 } 184 } catch( final IOException e ) { 185 LOG.error( "Problem retrieving login cookie, returning null: {}", e.getMessage() ); 186 return null; 187 } 188 return null; 189 } 190 191 /** 192 * Extracts the login cookie UID from the servlet request. 193 * 194 * @param request The HttpServletRequest 195 * @return The UID value from the cookie, or null, if no such cookie exists. 196 */ 197 private static String getLoginCookie( final HttpServletRequest request ) { 198 return HttpUtil.retrieveCookieValue( request, LOGIN_COOKIE_NAME ); 199 } 200 201 /** 202 * Sets a login cookie based on properties set by the user. This method also 203 * creates the cookie uid-username mapping in the work directory. 204 * 205 * @param engine The Engine 206 * @param response The HttpServletResponse 207 * @param username The username for whom to create the cookie. 208 */ 209 public static void setLoginCookie( final Engine engine, final HttpServletResponse response, final String username ) { 210 final UUID uid = UUID.randomUUID(); 211 final int days = TextUtil.getIntegerProperty( engine.getWikiProperties(), PROP_LOGIN_EXPIRY_DAYS, DEFAULT_EXPIRY_DAYS ); 212 final Cookie userId = getLoginCookie( uid.toString() ); 213 userId.setMaxAge( days * 24 * 60 * 60 ); 214 response.addCookie( userId ); 215 216 final File cf = getCookieFile( engine, uid.toString() ); 217 if( cf != null ) { 218 // Write the cookie content to the cookie store file. 219 try( final Writer out = new BufferedWriter( new OutputStreamWriter( Files.newOutputStream( cf.toPath() ), StandardCharsets.UTF_8 ) ) ) { 220 FileUtil.copyContents( new StringReader( username ), out ); 221 LOG.debug( "Created login cookie for user {} for {} days", username, days ); 222 } catch( final IOException ex ) { 223 LOG.error( "Unable to create cookie file to store user id: {}", uid ); 224 } 225 } 226 } 227 228 /** 229 * Clears away the login cookie, and removes the uid-username mapping file as well. 230 * 231 * @param engine Engine 232 * @param request Servlet request 233 * @param response Servlet response 234 */ 235 public static void clearLoginCookie( final Engine engine, final HttpServletRequest request, final HttpServletResponse response ) { 236 final Cookie userId = getLoginCookie( "" ); 237 userId.setMaxAge( 0 ); 238 response.addCookie( userId ); 239 final String uid = getLoginCookie( request ); 240 if( uid != null ) { 241 final File cf = getCookieFile( engine, uid ); 242 if( cf != null ) { 243 if( !cf.delete() ) { 244 LOG.debug( "Error deleting cookie login {}", uid ); 245 } 246 } 247 } 248 } 249 250 /** 251 * Helper function to get secure LOGIN cookie 252 * 253 * @param value of the cookie 254 */ 255 private static Cookie getLoginCookie( final String value ) { 256 final Cookie c = new Cookie( LOGIN_COOKIE_NAME, value ); 257 c.setHttpOnly( true ); // no browser access 258 c.setSecure( true ); // only access via encrypted https allowed 259 return c; 260 } 261 262 /** 263 * Goes through the cookie directory and removes any obsolete files. 264 * The scrubbing takes place one day after the cookie was supposed to expire. 265 * However, if the user has logged in during the expiry period, the expiry is 266 * reset, and the cookie file left here. 267 * 268 * @param days number of days that the cookie will survive 269 * @param cookieDir cookie directory 270 */ 271 private static synchronized void scrub( final int days, final File cookieDir ) { 272 LOG.debug( "Scrubbing cookieDir..." ); 273 final File[] files = cookieDir.listFiles(); 274 final long obsoleteDateLimit = System.currentTimeMillis() - ( ( long )days + 1 ) * 24 * 60 * 60 * 1000L; 275 int deleteCount = 0; 276 277 for( int i = 0; i < files.length; i++ ) { 278 final File f = files[ i ]; 279 final long lastModified = f.lastModified(); 280 if( lastModified < obsoleteDateLimit ) { 281 if( f.delete() ) { 282 deleteCount++; 283 } else { 284 LOG.debug( "Error deleting cookie login with index {}", i ); 285 } 286 } 287 } 288 289 LOG.debug( "Removed {} obsolete cookie logins", deleteCount ); 290 } 291 292}