001: /***************************************************************
002: * This file is part of the [fleXive](R) project.
003: *
004: * Copyright (c) 1999-2008
005: * UCS - unique computing solutions gmbh (http://www.ucs.at)
006: * All rights reserved
007: *
008: * The [fleXive](R) project is free software; you can redistribute
009: * it and/or modify it under the terms of the GNU General Public
010: * License as published by the Free Software Foundation;
011: * either version 2 of the License, or (at your option) any
012: * later version.
013: *
014: * The GNU General Public License can be found at
015: * http://www.gnu.org/copyleft/gpl.html.
016: * A copy is found in the textfile GPL.txt and important notices to the
017: * license from the author are found in LICENSE.txt distributed with
018: * these libraries.
019: *
020: * This library is distributed in the hope that it will be useful,
021: * but WITHOUT ANY WARRANTY; without even the implied warranty of
022: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
023: * GNU General Public License for more details.
024: *
025: * For further information about UCS - unique computing solutions gmbh,
026: * please see the company website: http://www.ucs.at
027: *
028: * For further information about [fleXive](R), please see the
029: * project website: http://www.flexive.org
030: *
031: *
032: * This copyright notice MUST APPEAR in all copies of the file!
033: ***************************************************************/package com.flexive.core.security;
034:
035: import com.flexive.core.Database;
036: import static com.flexive.core.DatabaseConst.TBL_ACCOUNTS;
037: import static com.flexive.core.DatabaseConst.TBL_ACCOUNT_DETAILS;
038: import com.flexive.shared.CacheAdmin;
039: import com.flexive.shared.FxContext;
040: import com.flexive.shared.FxLanguage;
041: import com.flexive.shared.FxSharedUtils;
042: import com.flexive.shared.exceptions.FxAccountExpiredException;
043: import com.flexive.shared.exceptions.FxAccountInUseException;
044: import com.flexive.shared.exceptions.FxApplicationException;
045: import com.flexive.shared.exceptions.FxLoginFailedException;
046: import com.flexive.shared.security.AuthenticationSource;
047: import com.flexive.shared.security.UserTicket;
048: import org.apache.commons.logging.Log;
049: import org.apache.commons.logging.LogFactory;
050:
051: import javax.security.auth.login.LoginException;
052: import java.sql.Connection;
053: import java.sql.PreparedStatement;
054: import java.sql.ResultSet;
055: import java.sql.SQLException;
056: import java.text.SimpleDateFormat;
057: import java.util.Calendar;
058: import java.util.Date;
059:
060: /**
061: * Authentication against the divisions database
062: *
063: * @author Markus Plesser (markus.plesser@flexive.com), UCS - unique computing solutions gmbh (http://www.ucs.at)
064: * @version $Rev
065: */
066: class FxDBAuthentication {
067:
068: private static transient Log LOG = LogFactory
069: .getLog(FxDBAuthentication.class);
070:
071: public static UserTicket login(String username, String password,
072: FxCallback callback) throws FxAccountInUseException,
073: FxLoginFailedException, FxAccountExpiredException {
074: final long SYS_UP = CacheAdmin.getInstance()
075: .getSystemStartTime();
076: FxContext inf = FxContext.get();
077:
078: // Avoid null pointer exceptions
079: if (password == null)
080: password = "";
081: if (username == null)
082: username = "";
083:
084: String curSql;
085: PreparedStatement ps = null;
086: Connection con = null;
087: try {
088: // Obtain a database connection
089: con = callback.getDataSource().getConnection();
090: // 1-6 7 8 9 10 11 12 13 14 15
091: curSql = "SELECT d.*,a.ID,a.IS_ACTIVE,a.IS_VALIDATED,a.ALLOW_MULTILOGIN,a.VALID_FROM,a.VALID_TO,NOW(),a.PASSWORD,a.MANDATOR "
092: + "FROM "
093: + TBL_ACCOUNTS
094: + " a "
095: + "LEFT JOIN "
096: + " (SELECT ID,ISLOGGEDIN,LAST_LOGIN,LAST_LOGIN_FROM,FAILED_ATTEMPTS,AUTHSRC FROM "
097: + TBL_ACCOUNT_DETAILS
098: + " WHERE APPLICATION=?) d ON a.ID=d.ID WHERE a.LOGIN_NAME=?";
099: ps = con.prepareStatement(curSql);
100: ps.setString(1, inf.getApplicationId());
101: ps.setString(2, username);
102: final ResultSet rs = ps.executeQuery();
103:
104: // Anything found?
105: if (rs == null || !rs.next())
106: throw new FxLoginFailedException(
107: "Login failed (invalid user or password)",
108: FxLoginFailedException.TYPE_USER_OR_PASSWORD_NOT_DEFINED);
109:
110: // check if the hashed password matches the hash stored in the database
111: final long id = rs.getLong(7);
112: final boolean passwordMatches = FxSharedUtils.hashPassword(
113: id, password).equals(rs.getString(14));
114: if (!passwordMatches) {
115: increaseFailedLoginAttempts(con, id);
116: throw new FxLoginFailedException(
117: "Login failed (invalid user or password)",
118: FxLoginFailedException.TYPE_USER_OR_PASSWORD_NOT_DEFINED);
119: }
120:
121: // Read data
122: final boolean loggedIn = rs.getBoolean(2);
123: final Date lastLogin = new Date(rs.getLong(3));
124: final String lastLoginFrom = rs.getString(4);
125: final long failedAttempts = rs.getLong(5);
126: final boolean active = rs.getBoolean(8);
127: final boolean validated = rs.getBoolean(9);
128: final boolean allowMultiLogin = rs.getBoolean(10);
129: final Date validFrom = new Date(rs.getLong(11));
130: final Date validTo = new Date(rs.getLong(12));
131: final Date dbNow = rs.getTimestamp(13);
132: final long mandator = rs.getLong(15);
133:
134: // Account active?
135: if (!active
136: || !validated
137: || !CacheAdmin.getEnvironment().getMandator(
138: mandator).isActive()) {
139: if (LOG.isDebugEnabled())
140: LOG.debug("Login for user ["
141: + username
142: + "] failed, account is inactive. Active="
143: + active
144: + ", Validated="
145: + validated
146: + ", Mandator active: "
147: + CacheAdmin.getEnvironment().getMandator(
148: mandator).isActive());
149: increaseFailedLoginAttempts(con, id);
150: throw new FxLoginFailedException("Login failed",
151: FxLoginFailedException.TYPE_INACTIVE_ACCOUNT);
152: }
153:
154: // Account date from-to valid?
155: //Compute the day AFTER the dValidTo
156: Calendar endDate = Calendar.getInstance();
157: endDate.setTime(validTo);
158: endDate.add(Calendar.DAY_OF_MONTH, 1);
159: if (validFrom.getTime() > dbNow.getTime()
160: || endDate.getTimeInMillis() < dbNow.getTime()) {
161: SimpleDateFormat sdf = new SimpleDateFormat(
162: "dd-MM-yyyy");
163: if (LOG.isDebugEnabled())
164: LOG
165: .debug("Login for user ["
166: + username
167: + "] failed, from/to date not valid. from='"
168: + sdf.format(validFrom) + "' to='"
169: + validTo + "'");
170: increaseFailedLoginAttempts(con, id);
171: throw new FxAccountExpiredException(username, dbNow);
172: }
173:
174: // Check 'Account in use and takeOver false'
175: if (!allowMultiLogin && !callback.getTakeOverSession()
176: && loggedIn && lastLogin != null) {
177: // Only if the last login time was AFTER the system started
178: if (lastLogin.getTime() >= SYS_UP) {
179: FxAccountInUseException aiu = new FxAccountInUseException(
180: username, lastLoginFrom, lastLogin);
181: if (LOG.isInfoEnabled())
182: LOG.info(aiu);
183: increaseFailedLoginAttempts(con, id);
184: throw aiu;
185: }
186: }
187:
188: // Clear any old data
189: curSql = "DELETE FROM " + TBL_ACCOUNT_DETAILS
190: + " WHERE ID=? AND APPLICATION=?";
191: ps.close();
192: ps = con.prepareStatement(curSql);
193: ps.setLong(1, id);
194: ps.setString(2, inf.getApplicationId());
195: ps.executeUpdate();
196:
197: // Mark user as active in the database
198: curSql = "INSERT INTO "
199: + TBL_ACCOUNT_DETAILS
200: + " (ID,APPLICATION,ISLOGGEDIN,LAST_LOGIN,LAST_LOGIN_FROM,FAILED_ATTEMPTS,AUTHSRC) "
201: + "VALUES (?,?,?,?,?,?,?)";
202: ps.close();
203: ps = con.prepareStatement(curSql);
204: ps.setLong(1, id);
205: ps.setString(2, inf.getApplicationId());
206: ps.setBoolean(3, true);
207: ps.setLong(4, System.currentTimeMillis());
208: ps.setString(5, inf.getRemoteHost());
209: ps.setLong(6, 0); //reset failed attempts
210: ps.setString(7, AuthenticationSource.Database.name());
211: ps.executeUpdate();
212:
213: // Load the user and construct a user ticket
214: try {
215: final UserTicketImpl ticket = (UserTicketImpl) UserTicketStore
216: .getUserTicket(username);
217: ticket.setFailedLoginAttempts(failedAttempts);
218: ticket
219: .setAuthenticationSource(AuthenticationSource.Database);
220: return ticket;
221: } catch (FxApplicationException e) {
222: if (callback.getSessionContext() != null)
223: callback.getSessionContext().setRollbackOnly();
224: throw new FxLoginFailedException(e
225: .getExceptionMessage().getLocalizedMessage(
226: FxLanguage.DEFAULT_ID),
227: FxLoginFailedException.TYPE_UNKNOWN_ERROR);
228: }
229: } catch (SQLException exc) {
230: if (callback.getSessionContext() != null)
231: callback.getSessionContext().setRollbackOnly();
232: throw new FxLoginFailedException("Database error: "
233: + exc.getMessage(),
234: FxLoginFailedException.TYPE_SQL_ERROR);
235: } finally {
236: Database.closeObjects(FxDBAuthentication.class, con, ps);
237: }
238: }
239:
240: /**
241: * Mark a user as no longer active in the database.
242: *
243: * @param ticket the ticke of the user
244: * @throws javax.security.auth.login.LoginException
245: * if the function failed
246: */
247: public static void logout(UserTicket ticket) throws LoginException {
248: PreparedStatement ps = null;
249: String curSql;
250: Connection con = null;
251: FxContext inf = FxContext.get();
252: try {
253:
254: // Obtain a database connection
255: con = Database.getDbConnection();
256:
257: // EJBLookup user in the database, combined with a update statement to make sure
258: // nothing changes between the lookup/set ISLOGGEDIN flag.
259: curSql = "UPDATE "
260: + TBL_ACCOUNT_DETAILS
261: + " SET ISLOGGEDIN=FALSE WHERE ID=? AND APPLICATION=?";
262: ps = con.prepareStatement(curSql);
263: ps.setLong(1, ticket.getUserId());
264: ps.setString(2, inf.getApplicationId());
265:
266: // Not more than one row should be affected, or the logout failed
267: final int rowCount = ps.executeUpdate();
268: if (rowCount > 1) {
269: // Logout failed.
270: LoginException le = new LoginException(
271: "Logout for user [" + ticket.getUserId()
272: + "] failed");
273: LOG.error(le);
274: throw le;
275: }
276:
277: } catch (SQLException exc) {
278: LoginException le = new LoginException("Database error: "
279: + exc.getMessage());
280: LOG.error(le);
281: throw le;
282: } finally {
283: Database.closeObjects(FxDBAuthentication.class, con, ps);
284: }
285: }
286:
287: /**
288: * Increase the number of failed login attempts for the given user
289: *
290: * @param con an open and valid connection
291: * @param userId user id
292: * @throws SQLException on errors
293: */
294: private static void increaseFailedLoginAttempts(Connection con,
295: long userId) throws SQLException {
296: PreparedStatement ps = null;
297: try {
298: ps = con
299: .prepareStatement("UPDATE "
300: + TBL_ACCOUNT_DETAILS
301: + " SET FAILED_ATTEMPTS=FAILED_ATTEMPTS+1 WHERE ID=?");
302: ps.setLong(1, userId);
303: if (ps.executeUpdate() == 0) {
304: ps.close();
305: ps = con
306: .prepareStatement("INSERT INTO "
307: + TBL_ACCOUNT_DETAILS
308: + " (ID,APPLICATION,ISLOGGEDIN,LAST_LOGIN,LAST_LOGIN_FROM,FAILED_ATTEMPTS,AUTHSRC) "
309: + "VALUES (?,?,?,?,?,?,?)");
310: ps.setLong(1, userId);
311: ps.setString(2, FxContext.get().getApplicationId());
312: ps.setBoolean(3, false);
313: ps.setLong(4, System.currentTimeMillis());
314: ps.setString(5, FxContext.get().getRemoteHost());
315: ps.setLong(6, 1); //one failed attempt
316: ps.setString(7, AuthenticationSource.Database.name());
317: ps.executeUpdate();
318: }
319: } finally {
320: if (ps != null)
321: ps.close();
322: }
323: }
324: }
|