001: /* ====================================================================
002: * The Jcorporate Apache Style Software License, Version 1.2 05-07-2002
003: *
004: * Copyright (c) 1995-2002 Jcorporate Ltd. All rights reserved.
005: *
006: * Redistribution and use in source and binary forms, with or without
007: * modification, are permitted provided that the following conditions
008: * are met:
009: *
010: * 1. Redistributions of source code must retain the above copyright
011: * notice, this list of conditions and the following disclaimer.
012: *
013: * 2. Redistributions in binary form must reproduce the above copyright
014: * notice, this list of conditions and the following disclaimer in
015: * the documentation and/or other materials provided with the
016: * distribution.
017: *
018: * 3. The end-user documentation included with the redistribution,
019: * if any, must include the following acknowledgment:
020: * "This product includes software developed by Jcorporate Ltd.
021: * (http://www.jcorporate.com/)."
022: * Alternately, this acknowledgment may appear in the software itself,
023: * if and wherever such third-party acknowledgments normally appear.
024: *
025: * 4. "Jcorporate" and product names such as "Expresso" must
026: * not be used to endorse or promote products derived from this
027: * software without prior written permission. For written permission,
028: * please contact info@jcorporate.com.
029: *
030: * 5. Products derived from this software may not be called "Expresso",
031: * or other Jcorporate product names; nor may "Expresso" or other
032: * Jcorporate product names appear in their name, without prior
033: * written permission of Jcorporate Ltd.
034: *
035: * 6. No product derived from this software may compete in the same
036: * market space, i.e. framework, without prior written permission
037: * of Jcorporate Ltd. For written permission, please contact
038: * partners@jcorporate.com.
039: *
040: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
041: * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
042: * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
043: * DISCLAIMED. IN NO EVENT SHALL JCORPORATE LTD OR ITS CONTRIBUTORS
044: * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
045: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
046: * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
047: * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
048: * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
049: * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
050: * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
051: * SUCH DAMAGE.
052: * ====================================================================
053: *
054: * This software consists of voluntary contributions made by many
055: * individuals on behalf of the Jcorporate Ltd. Contributions back
056: * to the project(s) are encouraged when you make modifications.
057: * Please send them to support@jcorporate.com. For more information
058: * on Jcorporate Ltd. and its products, please see
059: * <http://www.jcorporate.com/>.
060: *
061: * Portions of this software are based upon other open source
062: * products and are subject to their respective licenses.
063: */
064:
065: /*
066: * Copyright 1999, 2000, 2001 Jcorporate Ltd.
067: */
068: package com.jcorporate.expresso.services.controller;
069:
070: import com.jcorporate.expresso.core.controller.Controller;
071: import com.jcorporate.expresso.core.controller.ControllerException;
072: import com.jcorporate.expresso.core.controller.ControllerRequest;
073: import com.jcorporate.expresso.core.controller.ControllerResponse;
074: import com.jcorporate.expresso.core.controller.DBController;
075: import com.jcorporate.expresso.core.controller.ErrorCollection;
076: import com.jcorporate.expresso.core.controller.NonHandleableException;
077: import com.jcorporate.expresso.core.controller.Output;
078: import com.jcorporate.expresso.core.controller.ServletControllerRequest;
079: import com.jcorporate.expresso.core.controller.session.PersistentSession;
080: import com.jcorporate.expresso.core.db.DBException;
081: import com.jcorporate.expresso.core.dbobj.ValidValue;
082: import com.jcorporate.expresso.core.misc.ConfigManager;
083: import com.jcorporate.expresso.core.misc.CookieUtil;
084: import com.jcorporate.expresso.core.misc.CurrentLogin;
085: import com.jcorporate.expresso.core.misc.StringUtil;
086: import com.jcorporate.expresso.core.security.DelayThread;
087: import com.jcorporate.expresso.core.security.User;
088: import com.jcorporate.expresso.kernel.util.FastStringBuffer;
089: import org.apache.log4j.Logger;
090:
091: import javax.servlet.http.Cookie;
092: import javax.servlet.http.HttpServletRequest;
093: import javax.servlet.http.HttpServletResponse;
094: import java.util.Enumeration;
095: import java.util.Vector;
096:
097: /**
098: * Main Login Controller - used for login/logout and basic interaction with
099: * the registration system. This class recognizes the 'registration' classHandler
100: * name in the expresso-config.xml It uses the classname in that field to
101: * construct and forward to the appropriate registration class.
102: * <p/>
103: * Creation date: (5/12/2001 6:36:41 PM)
104: *
105: * @author Shash Chatterjee
106: */
107: public abstract class LoginController extends DBController {
108:
109: public static final String LOGINNAME_COOKIE = "UserName";
110: public static final String PASSWORD_COOKIE = "Password";
111: public static final String DBNAME_COOKIE = "db";
112: public static final String CLASS_HANDLER_NAME = "login";
113: public static final String DEFAULT_CLASS_NAME = com.jcorporate.expresso.services.controller.SimpleLoginController.class
114: .getName();
115:
116: private static Logger log = Logger.getLogger(LoginController.class);
117:
118: /**
119: * LoginController constructor. Sets all the states and parameters
120: * for the system.
121: */
122: public LoginController() {
123: super ();
124: //this.setSchema(com.jcorporate.expresso.core.ExpressoSchema.class);
125: }
126:
127: /**
128: * Function called to suspend thread execution for x many seconds before
129: * offering a retry to login. Helps to slow down brute force attacks.
130: * [a 40,000 word dictionary attack prolonged by 3 seconds a piece
131: * adds potentially 33 hours to the attack time. Yes this can be partially bypassed
132: * through simultaneous requests, but it still adds significant reponse time]
133: */
134: protected void delayLogin() {
135: DelayThread.delay();
136: }
137:
138: /**
139: * Processes the login request. Sets the errors collection if there's
140: * a problem with the login. This method expects the HttpServletRequest to have
141: * two parameters, LoginName and Password
142: *
143: * @param request The ControllerRequest handed off to a controller by the
144: * framework
145: * @param response The ControllerResponse object
146: * @param errors The system fills out the errors collection if there
147: * are problems with the login itself.
148: * @param hreq The "low level" version of ControllerRequest. Allows direct
149: * access to http components.
150: * @param hres The "low level" version of ControllerResponse. Allows direct
151: * access to the http HttpServletResponse.
152: * @param session The PersistantSession object to write the CurrentLogin response to
153: * @return the uid of the user if successfully logged in
154: * @throws ControllerException upon logic error
155: * @throws NonHandleableException upon a fatal error
156: * @throws DBException if there is database lookup problems
157: */
158: protected int attemptLogin(ControllerRequest request,
159: ControllerResponse response, ErrorCollection errors,
160: HttpServletRequest hreq, HttpServletResponse hres,
161: PersistentSession session) throws ControllerException,
162: NonHandleableException, DBException {
163: try {
164: int uid = 0;
165: User myUser = new User();
166: myUser.setDataContext(request.getDataContext());
167: String loginName = StringUtil.notNull(request
168: .getParameter("LoginName"));
169:
170: if (loginName.equals("")) {
171: errors.addError("error.nologinname"); // You did not enter a login name, please go back and correct that
172: return uid;
173: }
174:
175: myUser.setLoginName(loginName);
176:
177: if ("NONE".equalsIgnoreCase(loginName) || !myUser.find()) {
178: errors.addError("error.invalidusername");
179:
180: // Brute forcing login names is often step one in the brute force attack
181: // So we counter this by giving them only a generic error.
182: // "Login '" + loginName + "' does not exist in context '"
183: // + request.getDBName() + "', please provide another value");
184:
185: //Delay threads provide ways of reducing the effectiveness
186: //of brute force attacks.
187: logInvalidLoginAttempt(response
188: .getString("error.invalidusername"), request);
189: delayLogin();
190:
191: return uid;
192: }
193: // evaluate the passwords
194: if (!myUser.passwordEquals(StringUtil.notNull(request
195: .getParameter("Password")))) {
196:
197: // Brute forcing login names is often step one in the brute force attack
198: // So we counter this by giving them only a generic error.
199: //errors.addError(errorMsg);
200: errors.addError("error.invalidusername");
201: //Delay threads provide ways of reducing the effectiveness
202: //of brute force attacks.
203: logInvalidLoginAttempt(response
204: .getString("error.invalidusername"), request);
205: delayLogin();
206:
207: return uid;
208: }
209:
210: uid = myUser.getUid();
211:
212: String currStatus = myUser.getAccountStatus();
213:
214: // test for account status
215: if (!currStatus.equals(User.ACTIVE_ACCOUNT_STATUS)) {
216: ValidValue vv = null;
217: String currDescrip = "Unknown Status '" + currStatus
218: + "'";
219: Vector v = myUser.getValidValues("AccountStatus");
220:
221: for (Enumeration ev = v.elements(); ev
222: .hasMoreElements();) {
223: vv = (ValidValue) ev.nextElement();
224:
225: if (vv.getValue().equals(currStatus)) {
226: currDescrip = vv.getDescription();
227: }
228: }
229:
230: this .logInvalidLoginAttempt(response.getString(
231: "error.login.invalidlogin", loginName,
232: currDescrip), request);
233:
234: errors.addError("error.login.invalidlogin", loginName,
235: currDescrip); // Account + loginName + is + currDescrip
236: delayLogin(); //Make 'em wait to save on server resources.
237:
238: return uid;
239: }
240:
241: ///////////////////////////////////////////////
242: // GOOD LOGIN If we get here then user is authenticated and active
243: /////////////////////////////////////////////
244:
245: // some DBs are case INsensitive, so incorrect-case login names still match,
246: // but we should correct our internal username to the
247: // "real" login name as found in the database
248: try {
249: User user = new User();
250: user.setDataContext(request.getDataContext());
251: user.setUid(uid);
252: if (!user.find()) {
253: log
254: .error("runDoLoginState unexpectedly cannot find user (after successful login!) for uid: "
255: + uid);
256: } else {
257: loginName = user.getLoginName();
258: }
259: } catch (DBException e) {
260: log.error("unexpectedly cannot find user" + e);
261: }
262:
263: if (log.isInfoEnabled()) {
264: log.info("Successful login for user: " + loginName);
265: }
266:
267: // write cookie; write password if user has chosen this option
268: /**
269: * @todo write a internally-maintained, expiring key INSTEAD of password
270: */
271: if (StringUtil.notNull(request.getParameter("Remember"))
272: .equalsIgnoreCase("Y")) {
273: setCookie(loginName, request.getParameter("Password"),
274: hres, false, request.getDataContext());
275: response.add(new Output("remembered", response
276: .getString("Login_Remembered")));
277: } else { /* if we don't remember, clear the cookie */
278: setCookie(User.UNKNOWN_USER, "NONE", hres, true,
279: request.getDataContext());
280: response.add(new Output("remembered", response
281: .getString("Login_Not_Remembered")));
282: } /* if we remember */
283:
284: response.setUser(loginName);
285: request.setUser(loginName);
286: request.setUid(uid);
287: // set params
288: setPersistentLoginAttributes(request, loginName);
289:
290: return uid;
291: } catch (Throwable t) {
292: log.error("Exception caught attempting login", t);
293: throw new ControllerException(
294: "Error while attempting login processing", t);
295: }
296: } /* attemptLogin */
297:
298: /**
299: * after successful authentication, set all the necessary parameters in session
300: * this method is useful when integrating into different authentication system.
301: * override Controller.perform, and after doing non-expresso authentication in
302: * that override, call this static method,
303: * then proceed with the standard Controller.perform, and expresso will pick
304: * up the login values specified here.
305: * <p/>
306: * this method made static 3/03 in order to allow external authentication;
307: * should be changed to plug-in model when available in v. 5.1 and thereafter
308: *
309: * @param request The ControllerRequest object for this request
310: * @param loginName the login name which has been (potentially) corrected to match case in DB, even if DB matches on any lower/upper case
311: * @throws ControllerException upon error
312: */
313: public static void setPersistentLoginAttributes(
314: ControllerRequest request, String loginName)
315: throws ControllerException {
316: PersistentSession session = request.getSession();
317: ServletControllerRequest sHreq = (ServletControllerRequest) request;
318: HttpServletRequest hreq = sHreq.getHttpServletRequest();
319:
320: session.setPersistentAttribute("UserName", loginName);
321: session.setPersistentAttribute("Password", request
322: .getParameter("Password"));
323: session.setPersistentAttribute("db", request.getDataContext());
324: session.removePersistentAttribute(CurrentLogin.LOGIN_KEY);
325:
326: CurrentLogin myLogin = CurrentLogin.newInstance(loginName, hreq
327: .getRemoteAddr(), request.getDataContext(), request
328: .getUid());
329:
330: session.setPersistentAttribute(CurrentLogin.LOGIN_KEY, myLogin);
331:
332: }
333:
334: /**
335: * Set a long-life cookie on the client side that records the login
336: * information, so the user does not have to log in again
337: * next time they begin a session. CAN BE A SERIOUS SECURITY
338: * RISK - Only used when the PC is physically secured or when the network
339: * login is required and prevents others from using the same browser cookies
340: * <p/>
341: * this method made static 3/03 in order to allow external authentication;
342: * should be changed to plug-in model when available in v. 5.4 and thereafter
343: *
344: * @param userName User name to save in cookie
345: * @param password Password to save in cookie
346: * @param res Standard response object
347: * @param clear Should the cookies be cleared instead?
348: * @param dbname The data context to set the login for.
349: * @throws ControllerException if a database error occurs
350: */
351: public static void setCookie(String userName, String password,
352: HttpServletResponse res, boolean clear, String dbname)
353: throws ControllerException {
354: try {
355: final int THIRTY_DAYS_IN_SECS = 2592000;
356: Cookie c1;
357:
358: if ((userName != null) && (userName.length() > 0)) {
359: c1 = new Cookie(LOGINNAME_COOKIE, CookieUtil
360: .cookieEncode(userName));
361: } else {
362: c1 = new Cookie(LOGINNAME_COOKIE, User.UNKNOWN_USER);
363: }
364: if (clear == true) {
365: c1.setMaxAge(10);
366: } else {
367: c1.setMaxAge(THIRTY_DAYS_IN_SECS);
368: }
369:
370: c1.setPath("/");
371: res.addCookie(c1);
372:
373: Cookie c2;
374:
375: if ((password != null) && (password.length() > 0)) {
376: c2 = new Cookie(PASSWORD_COOKIE, CookieUtil
377: .cookieEncode(password));
378: } else {
379: c2 = new Cookie(PASSWORD_COOKIE, "NONE");
380: }
381: if (clear == true) {
382: c2.setMaxAge(10); // seconds till expiry
383: } else {
384: c2.setMaxAge(THIRTY_DAYS_IN_SECS);
385: }
386:
387: c2.setPath("/"); //$NON-NLS-1$
388: res.addCookie(c2);
389:
390: Cookie c3 = null;
391:
392: if (clear == true) {
393: c3 = new Cookie(DBNAME_COOKIE, "NONE");
394: c3.setMaxAge(10);
395: } else {
396: c3 = new Cookie(DBNAME_COOKIE, CookieUtil
397: .cookieEncode(dbname));
398: c3.setMaxAge(THIRTY_DAYS_IN_SECS); /* 30 days */
399: }
400:
401: c3.setPath("/");
402: res.addCookie(c3);
403: } catch (Exception ce) {
404: throw new ControllerException(ce);
405: }
406: } /* setCookie(String, String, HttpServletRequest, HttpServletResponse, boolean) */
407:
408: /**
409: * Prefereable method to call if you already have a controller instance. Use
410: * it instead of getRegistrationController to use the Schema as your source of
411: * login controllers
412: *
413: * @return The Controller Object that is a registration controller
414: * @throws ControllerException if an error occurs instantiating the controller
415: * object
416: */
417:
418: public Controller getDefaultRegistrationController()
419: throws ControllerException {
420: return this .getSchemaInstance().getRegistrationController();
421: }
422:
423: /**
424: * Gets the Login controller based upon the classhandler or the
425: * default....
426: *
427: * @return an instantiated LoginController
428: * @throws ControllerException if there's an error instantiating the LoginController
429: */
430: public static Controller getLoginController()
431: throws ControllerException {
432: String className = ConfigManager
433: .getClassHandler(CLASS_HANDLER_NAME);
434: if (className == null || className.length() == 0) {
435: className = DEFAULT_CLASS_NAME;
436: }
437:
438: return ConfigManager.getControllerFactory().getController(
439: className);
440: }
441:
442: /**
443: * Prefereable method to call if you already have a controller instance. Use
444: * it instead of getLoginController to use the Schema as your source of
445: * login controllers
446: *
447: * @return The Controller Object that is a registration controller
448: * @throws ControllerException if an error occurs instantiating the controller
449: * object
450: */
451:
452: public Controller getDefaultLoginController()
453: throws ControllerException {
454: return this .getSchemaInstance().getLoginController();
455: }
456:
457: /**
458: * Does a warning log to log the invalid login request. Also logs the
459: * remote IP Address.
460: *
461: * @param msg The main message to log.
462: * @param request If it happens to be a ServletControllerRequest
463: */
464: public void logInvalidLoginAttempt(String msg,
465: ControllerRequest request) {
466: String remoteIP = "";
467: try {
468: remoteIP = ((ServletControllerRequest) request)
469: .getServletRequest().getRemoteAddr();
470: } catch (ClassCastException cce) {
471: //We aren't running in a servlet container, we can't get the remote
472: //IP address
473: }
474: FastStringBuffer fsb = new FastStringBuffer(msg.length() + 32);
475: fsb.append(msg);
476: if (remoteIP.length() > 0) {
477: fsb.append("\n Client IP Address: ");
478: fsb.append(remoteIP);
479: }
480:
481: log.warn(fsb.toString());
482: }
483:
484: /**
485: * Override this class to do some post processing in your derived controllers.
486: *
487: * @param request The ControllerRequest Object
488: * @param response The ControllerResponse Object
489: * @throws ControllerException upon error processing the post login information
490: */
491: public void postLoginProcessing(ControllerRequest request,
492: ControllerResponse response) throws ControllerException {
493: return;
494: }
495:
496: /**
497: * Override the normal stateAllowed method to always allow
498: * access to this controller for certain states - otherwise no-one can ever log in :-)
499: *
500: * @param newState the state to transition to.
501: * @param params The controllerRequest object
502: * @return true if the state is allowed for the currently logged in user.
503: * @throws ControllerException if there is an error while looking up the sercurity permissions
504: */
505: public boolean stateAllowed(String newState,
506: ControllerRequest params) throws ControllerException {
507: if (newState.equals("promptChangePW")
508: || newState.equals("processChangePW")
509: || newState.equals("promptLogout")) {
510: return super .stateAllowed(newState, params);
511: }
512:
513: return true;
514: } /* stateAllowed(String) */
515:
516: }
|