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