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