001 // Copyright (c) 2001 Hursh Jain (http://www.mollypages.org)
002 // The Molly framework is freely distributable under the terms of an
003 // MIT-style license. For details, see the molly pages web site at:
004 // http://www.mollypages.org/. Use, modify, have fun !
005
006 package fc.web.servlet;
007
008 import java.io.*;
009 import java.net.*;
010 import java.sql.*;
011 import java.util.*;
012 import java.security.*;
013 import javax.servlet.*;
014 import javax.servlet.http.*;
015
016 import fc.jdbc.*;
017 import fc.io.*;
018 import fc.web.*;
019 import fc.util.*;
020
021 /**
022 Logs in/logs out the user. Works in conjunction with {@link JDBCAuthFilter} and
023 the login page of the application. (handles the submit from the HTML login
024 form). Moreover, the application can logout the user by invoking this servlet
025 with an additional <tt><b>act=logout</b></tt> query string. Uses {@link
026 JDBCSession} to create/delete session id's automatically on successful
027 logins/logouts.
028 <p>
029 Requires the following servlet initialization parameters
030 <ul>
031 <li><tt>welcome_page</tt> the <i>webapp relative</i> path to the
032 welcome page to be shown after a successful login.
033 <li><tt>login_page</tt> the <i>webapp relative</i> path to the
034 login page.
035 <li><tt>logout_welcome_page</tt> the <i>webapp relative</i> path
036 to the welcome page to be shown after a successful logout. (this
037 parameter is <b>optional</b> and if not specified, the welcome_page
038 will be used).
039 </ul>
040 <u>On login failure</u>, the following attributes are set in the request
041 before control is transferred to the login page via a server side
042 redirect.
043 <ol>
044 <li><tt>retrycount</tt>, value is a Integer object representing the
045 number of times login has been unsuccessfuly tried. Note: <u>the login
046 page should read this attribute if present and store it in the form
047 as a hidden parameter. When the login form is submitted, this variable
048 will be sent back to this servlet in the request as a parameter and
049 upon login failure, be appropriately incremented.</u>
050 </ol>
051 <u>On login success</u>, the following attributes are set as a cookie on
052 the client. This cookie is removed on logout.
053 <ol>
054 <li><tt>{@link #SID_COOKIE_NAME}</tt> the session ID assigned to the
055 user. After logging in, a session will exist in the database. {@link
056 JDBCSession} can thereafter be used to store any information for that
057 session in the database via the session ID.
058 <li><tt>user.name</tt>, the name of the user that was succefully
059 used to login to the system. (this is useful for displaying the
060 username in the front end page without hitting the database everytime).
061 </ol>
062 In addition, upon successful login, the {@link #onLogin} method is invoked.
063 This method can be overriden as necessary by subclasses. Similarly, In addition, upon
064 successful logout, the {@link #onLogout} method is invoked.
065 <p>
066 The servlet requires the following request parameters from the
067 login form:
068 <ul>
069 <li><tt>username</tt>
070 <li><tt>password</tt>
071 </ul>
072 The following request parameters are optional.
073 <ul>
074 <li><tt>target</tt> if present (either as a cookie or in the URL),
075 the client is redirected to the URL specified by the target
076 (otherwise the client is redirected to the welcome_page after
077 login/logout). The {@link AuthFilter} automatically stores the
078 original target page as a parameter (URLEncoded) so that users are
079 seamlessly redirected to their original target after a successful
080 login or logout.
081 </ul>
082 <p>
083 Requires the following database schema:
084 <blockquote>
085 A <b>Users</b> table must exist. (note: "user" is a reserved word in many
086 databases, the table must be called User<b>s</b>.
087 <ol>
088 The following columns must exist in the <tt>Users</tt> table.
089 <li> <b>user_id</b> the user id
090 <li> <b>username</b> the name of the user (corresponds to
091 the username parameter in the login form)
092 <li> <b>password</b> the password for the user (corresponds
093 to the password parameter in the login form).
094 </ol>
095 The class will use the {@link fc.jdbc.dbo.DBOMgr} framework so a
096 <b>UserMgr</b> class corresponding to the aforementioned <b>User</b> table
097 must exist in the classpath of this servlet.
098 </blockquote>
099 Since this class uses {@link JDBCSession}, the default database
100 tables required by {@link JDBCSession} also must exist.
101 <p>
102 For security reasons, for logging in, the username/password form must be
103 submitted via a POST (GET is fine when logging out).
104
105 @author hursh jain
106 **/
107 public class LoginServlet extends FCBaseServlet
108 {
109 /** value = "sid" **/
110 public static final String SID_COOKIE_NAME = "sid";
111
112 protected String login_query;
113 protected String loginPage;
114 protected String welcomePage;
115 protected String logoutWelcomePage;
116 protected JDBCSession session;
117 protected MessageDigest hash;
118
119 public void init(ServletConfig conf) throws ServletException
120 {
121 super.init(conf);
122
123 try {
124 loginPage = WebUtil.getRequiredParam(this, "login_page");
125 welcomePage = WebUtil.getRequiredParam(this,"welcome_page");
126 logoutWelcomePage = WebUtil.getParam(
127 this, "logout_welcome_page", welcomePage);
128 login_query = WebUtil.getRequiredParam(this, "login_query");
129 String hash_name = WebUtil.getParam(this, "password_hash", null);
130
131 log.bug("login page=", loginPage);
132 log.bug("welcome page=", welcomePage);
133 log.bug("logout welcome page=", logoutWelcomePage);
134 log.bug("login_query=", login_query);
135 log.bug("password_hash=", hash_name);
136
137 hash = (MessageDigest) MessageDigest.getInstance(hash_name);
138 session = JDBCSession.getInstance();
139 }
140 catch (Exception e) {
141 log.error(IOUtil.throwableToString(e));
142 throw new ServletException(IOUtil.throwableToString(e));
143 }
144 }
145
146 /**
147 Returns the cookie corresponding to the "sid". (this cookie
148 has key = {@link #SID_COOKIE_NAME} and the value is the SID
149 created/set at login time). Returns <tt>null</tt> is no
150 sid cookie is found.
151 */
152 public static Cookie getSIDCookie(HttpServletRequest req)
153 {
154 Cookie[] cookies = req.getCookies();
155
156 if (cookies == null)
157 return null;
158
159 for (int n = 0; n < cookies.length; n++)
160 {
161 if (cookies[n].getName().equals(SID_COOKIE_NAME))
162 {
163 return cookies[n];
164 }
165 }
166 return null;
167 }
168
169 boolean checkHasValidSID(Connection con, HttpServletRequest req)
170 throws SQLException
171 {
172 Cookie c = getSIDCookie(req);
173 if (c == null)
174 return false;
175
176 String sid = c.getValue();
177 if (session.exists(con, sid))
178 return true;
179
180 return false;
181 }
182
183 public void doGet(HttpServletRequest req, HttpServletResponse res)
184 throws ServletException, IOException
185 {
186 //to support logout only.
187 doPost(req, res);
188 }
189
190 public void doPost(HttpServletRequest req, HttpServletResponse res)
191 throws ServletException, IOException
192 {
193 Connection con = null;
194
195 try {
196 con = getConnection();
197
198 String action = req.getParameter("act");
199
200 if (action != null) {
201 if (action.equals("logout"))
202 {
203 if (doLogout(con, req, res)) {
204 goToTargetPage(req, res, logoutWelcomePage);
205 return;
206 }
207 return;
208 }
209 }
210
211 //at this point we are logging in.
212
213 //may have valid SID if login page accessed directly
214 //via browser's history or whatever.
215 if (checkHasValidSID(con, req)) {
216 goToTargetPage(req, res, welcomePage);
217 return;
218 }
219
220 if (! doLogin(con, req, res)) {
221 addFailureMessage(req);
222 WebUtil.forward(req, res, loginPage);//server side redirect
223 return;
224 }
225 else{
226 goToTargetPage(req, res, welcomePage);
227 return;
228 }
229 }
230 catch (Exception e) {
231 throw new ServletException(e);
232 }
233 finally {
234 try {
235 con.close();
236 }
237 catch (SQLException e) {
238 throw new ServletException(e);
239 }
240 }
241
242 } //~doPost
243
244 /**
245 This method validates the specified username/password.
246 <p>
247 <b>
248 This class should be subclassed to override this method and validate the
249 supplied username/password against a database in a different fashion if
250 desired.
251 </b>
252 The default implmentation of this method works with the following
253 initialization parameters.
254 <ul>
255 <li><tt>login_query</tt> (required): the query string to validate if this
256 username/passwrod combination exists. This query string should in
257 <b>PreparedStatement</b> format with 2 question marks (the first one will
258 be set with the username and the 2nd with the password). </li>
259 <li><tt>password_hash</tt> (optional): if present, should contain the
260 name of the java cryto hash function to hash the password before comparing
261 it with the database (this is for cases where the passwords are stored as
262 hashed values in the database). Examples include <tt>MD5</tt>, <tt>SHA-1</tt>
263 etc.
264 </li>
265 </ul>
266 This method should return the following values:
267 <ul>
268 <li>If authentication failed: <tt>null</tt></li>
269 <li>If authentication succeeded:
270 <ol>
271 <li>If the returned string is non-null and non-empty, then it should
272 contain the userid for the authenticated user and this
273 <tt>userid</tt> is stored in the {@link JDBCSession}.</li>
274 <li>If the user authenticated succeeds, a non-null string containing
275 a unique username or userid should be returned. (the username/userid
276 should be the Primary key field used in the database to uniquely
277 identify a user).
278 </li>
279 </ol>
280 </li>
281 </ul>
282 */
283 public String validateUser(
284 Connection con, String username, String password)
285 throws SQLException, IOException
286 {
287 PreparedStatement pstmt = null;
288
289 if (! (con instanceof fc.jdbc.PooledConnection) ) {
290 pstmt = con.prepareStatement(login_query);
291 }
292 else{
293 PooledConnection pc = (PooledConnection) con;
294 pstmt = pc.getCachedPreparedStatement(login_query);
295 }
296
297 pstmt.setString(1, username);
298 pstmt.setString(2, encodePassword(password));
299
300 log.bug("Validating logon using: ", pstmt);
301
302 ResultSet rs = pstmt.executeQuery();
303 String uid = null;
304
305 if (rs.next()) {
306 log.bug("Successfully validated username=", username, ". Got uid=[", uid + "]");
307 uid = rs.getString(1);
308 }
309
310 return uid;
311 }
312
313 /**
314 For this method to be available from application code, this servlet
315 should be set to load on startup and code similar to the following
316 example invoked.
317 <blockquote>
318 <tt>
319 LoginServlet ls = (LoginServlet) WebApp.allServletsMap.get("fc.web.servlet.LoginServlet");
320 if (ls == null) { //can happen if servlet is not loaded yet
321 throw new Exception("Unexpected error: LoginServlet was null");
322 }
323 return ls.encodePassword(passwd);
324 </tt>
325 </blockquote>
326 */
327 public String encodePassword(String password) throws IOException
328 {
329 if (hash == null || password == null)
330 return password;
331
332 try {
333 //MessageDigest getInstance not thread safe, alternative is to
334 //use that in a sychronized block.
335 final MessageDigest hash2 = (MessageDigest) hash.clone();
336 final byte[] digest_buf = hash2.digest (password.getBytes("UTF-8"));
337
338 //postgres, mysql have md5() which returns the md5 hash as a
339 //hexadecimal string. they don't really have base64 that can work
340 //seamlessly as: string->md5()->base64-->password
341 // password = new sun.misc.BASE64Encoder().encode(digest_buf);
342 //So we do this:
343
344 password = HexOutputStream.toHex(digest_buf);
345 }
346 catch (Exception e) {
347 throw new IOException(IOUtil.throwableToString(e));
348 }
349
350 return password;
351 }
352
353 /**
354 Returns true on a successful login, false otherwise.
355 */
356 boolean doLogin(
357 Connection con, HttpServletRequest req, HttpServletResponse res)
358 throws SQLException, IOException
359 {
360 String username = req.getParameter("username");
361 String password = req.getParameter("password");
362
363 if (username == null || password == null) {
364 log.error("username not recieved in request (not present at all)");
365 return false;
366 }
367
368 if (password == null) {
369 log.error("password not recieved in request (not present at all)");
370 return false;
371 }
372
373 String sid = SessionUtil.newSessionID();
374 String uid = validateUser(con, username, password);
375 boolean suxez = (uid != null);
376
377 if (suxez)
378 {
379 session.create(con, sid, uid);
380
381 addCookie(res, SID_COOKIE_NAME, sid);
382 addCookie(res, "user.name", username);
383 onLogin(con, sid, username, req, res);
384 }
385 else{
386 log.info("login FAILED for username=[", username, "]");
387 }
388
389 return suxez;
390 }
391
392 boolean doLogout(Connection con,
393 HttpServletRequest req, HttpServletResponse res)
394 throws SQLException, IOException
395 {
396 Cookie c = getSIDCookie(req);
397
398 if (c == null)
399 return false;
400
401 String sid = c.getValue();
402 log.bug("logging out: ", sid);
403
404 boolean suxez = false;
405 if (session.exists(con, sid)) {
406 session.expire(con, sid);
407 suxez = true;
408 }
409 else{
410 log.bug("session:", sid, " does not exist, ignoring logout");
411 }
412
413 deleteCookie(res, "user.name");
414 deleteCookie(res, SID_COOKIE_NAME);
415 onLogout(con, sid, req, res);
416
417 return suxez;
418 }
419
420 /**
421 This method is invoked upon successful login. By default, it does
422 nothing but subclasses can override this method as needed.
423
424 @param con a connection to the database
425 @param username the username for this user (that was used to login the
426 user via the login query)
427 @param sid the session id for this user
428 */
429 public void onLogin(Connection con, String sid, String username,
430 HttpServletRequest req, HttpServletResponse res)
431 throws SQLException, IOException
432 {
433 /* override as necessary */
434 }
435
436 /**
437 This method is invoked upon successful login. By default, it does
438 nothing but subclasses can override this method as needed.
439
440 @param con a connection to the database
441 @param sid the session id for this user
442 */
443 public void onLogout(Connection con, String sid,
444 HttpServletRequest req, HttpServletResponse res)
445 throws SQLException, IOException
446 {
447 /* override as necessary */
448 }
449
450 void goToTargetPage(
451 HttpServletRequest req, HttpServletResponse res, String defaultPage)
452 throws IOException, ServletException
453 {
454 String target = null;
455
456 final Cookie cookie = WebUtil.getCookie(req, "target");
457 if (cookie != null) {
458 target = cookie.getValue();
459 cookie.setPath("/");
460 cookie.setMaxAge(0); //delete
461 res.addCookie(cookie);
462 }
463 else
464 target = req.getParameter("target");
465
466 if (target == null)
467 {
468 //we want browser's url to change so client side
469 WebUtil.clientRedirect(req, res, defaultPage);
470 }
471 else {
472 /*
473 we don't use a server side redirect because the target
474 is the full original URL (like: http://....) and
475 server side redirect would try /http://.... (the
476 leading / because server side redirects are context
477 relative) we also want the browser's url to change
478 */
479 target = URLDecoder.decode(target);
480
481 WebUtil.clientRedirect(req, res, target);
482 }
483 }
484
485 void addFailureMessage(HttpServletRequest req)
486 throws ServletException
487 {
488 String retrycount = req.getParameter("retrycount");
489 if (retrycount == null)
490 retrycount = "0";
491
492 Integer rint = null;
493 try {
494 rint = new Integer(Integer.parseInt(retrycount) + 1);
495 }
496 catch (NumberFormatException e) {
497 rint = new Integer(1);
498 log.warn(e);
499 }
500
501 req.setAttribute("retrycount", rint);
502 }
503
504 final void addCookie(HttpServletResponse res, String key, String val)
505 {
506 Cookie cookie = new Cookie(key, val);
507 cookie.setMaxAge(session.getExpireTime());
508 res.addCookie(cookie);
509 }
510
511 final void deleteCookie( HttpServletResponse res, String key)
512 {
513 //the value is irrelevant since we are deleting the cookie
514 Cookie cookie = new Cookie(key, "");
515 cookie.setMaxAge(0);
516 res.addCookie(cookie);
517 }
518
519 } //~class
520