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 006package fc.web.servlet; 007 008import java.io.*; 009import java.net.*; 010import java.sql.*; 011import java.util.*; 012import java.security.*; 013import javax.servlet.*; 014import javax.servlet.http.*; 015 016import fc.jdbc.*; 017import fc.io.*; 018import fc.web.*; 019import fc.util.*; 020 021/** 022Logs in/logs out the user. Works in conjunction with {@link JDBCAuthFilter} and 023the login page of the application. (handles the submit from the HTML login 024form). Moreover, the application can logout the user by invoking this servlet 025with an additional <tt><b>act=logout</b></tt> query string. Uses {@link 026JDBCSession} to create/delete session id's automatically on successful 027logins/logouts. 028<p> 029Requires 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 041before control is transferred to the login page via a server side 042redirect. 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 052the 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> 062In addition, upon successful login, the {@link #onLogin} method is invoked. 063This method can be overriden as necessary by subclasses. Similarly, In addition, upon 064successful logout, the {@link #onLogout} method is invoked. 065<p> 066The servlet requires the following request parameters from the 067login form: 068<ul> 069 <li><tt>username</tt> 070 <li><tt>password</tt> 071</ul> 072The 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> 083Requires the following database schema: 084<blockquote> 085A <b>Users</b> table must exist. (note: "user" is a reserved word in many 086databases, the table must be called User<b>s</b>. 087<ol> 088The 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> 095The 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 097must exist in the classpath of this servlet. 098</blockquote> 099Since this class uses {@link JDBCSession}, the default database 100tables required by {@link JDBCSession} also must exist. 101<p> 102For security reasons, for logging in, the username/password form must be 103submitted via a POST (GET is fine when logging out). 104 105@author hursh jain 106**/ 107public class LoginServlet extends FCBaseServlet 108{ 109/** value = "sid" **/ 110public static final String SID_COOKIE_NAME = "sid"; 111 112protected String login_query; 113protected String loginPage; 114protected String welcomePage; 115protected String logoutWelcomePage; 116protected JDBCSession session; 117protected MessageDigest hash; 118 119public 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/** 147Returns the cookie corresponding to the "sid". (this cookie 148has key = {@link #SID_COOKIE_NAME} and the value is the SID 149created/set at login time). Returns <tt>null</tt> is no 150sid cookie is found. 151*/ 152public 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 169boolean checkHasValidSID(Connection con, HttpServletRequest req) 170throws 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 183public void doGet(HttpServletRequest req, HttpServletResponse res) 184throws ServletException, IOException 185 { 186 //to support logout only. 187 doPost(req, res); 188 } 189 190public void doPost(HttpServletRequest req, HttpServletResponse res) 191throws 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/** 245This method validates the specified username/password. 246<p> 247<b> 248This class should be subclassed to override this method and validate the 249supplied username/password against a database in a different fashion if 250desired. 251</b> 252The default implmentation of this method works with the following 253initialization parameters. 254<ul> 255<li><tt>login_query</tt> (required): the query string to validate if this 256username/passwrod combination exists. This query string should in 257<b>PreparedStatement</b> format with 2 question marks (the first one will 258be set with the username and the 2nd with the password). </li> 259<li><tt>password_hash</tt> (optional): if present, should contain the 260name of the java cryto hash function to hash the password before comparing 261it with the database (this is for cases where the passwords are stored as 262hashed values in the database). Examples include <tt>MD5</tt>, <tt>SHA-1</tt> 263etc. 264</li> 265</ul> 266This 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*/ 283public String validateUser( 284 Connection con, String username, String password) 285throws 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/** 314For this method to be available from application code, this servlet 315should be set to load on startup and code similar to the following 316example invoked. 317<blockquote> 318<tt> 319LoginServlet ls = (LoginServlet) WebApp.allServletsMap.get("fc.web.servlet.LoginServlet"); 320if (ls == null) { //can happen if servlet is not loaded yet 321 throw new Exception("Unexpected error: LoginServlet was null"); 322 } 323return ls.encodePassword(passwd); 324</tt> 325</blockquote> 326*/ 327public 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/** 354Returns true on a successful login, false otherwise. 355*/ 356boolean doLogin( 357 Connection con, HttpServletRequest req, HttpServletResponse res) 358throws 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 392boolean doLogout(Connection con, 393 HttpServletRequest req, HttpServletResponse res) 394throws 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/** 421This method is invoked upon successful login. By default, it does 422nothing 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*/ 429public void onLogin(Connection con, String sid, String username, 430 HttpServletRequest req, HttpServletResponse res) 431throws SQLException, IOException 432 { 433 /* override as necessary */ 434 } 435 436/** 437This method is invoked upon successful login. By default, it does 438nothing 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*/ 443public void onLogout(Connection con, String sid, 444 HttpServletRequest req, HttpServletResponse res) 445throws SQLException, IOException 446 { 447 /* override as necessary */ 448 } 449 450void goToTargetPage( 451 HttpServletRequest req, HttpServletResponse res, String defaultPage) 452throws 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 485void addFailureMessage(HttpServletRequest req) 486throws 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 504final 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 511final 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