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