package org.identityconnectors.oracle; import java.sql.Connection; import java.sql.SQLException; import java.text.MessageFormat; import org.identityconnectors.common.StringUtil; import org.identityconnectors.common.logging.Log; import org.identityconnectors.common.security.GuardedString; import org.identityconnectors.dbcommon.LocalizedAssert; import org.identityconnectors.dbcommon.SQLUtil; import org.identityconnectors.framework.common.exceptions.ConnectorException; import org.identityconnectors.framework.common.exceptions.InvalidCredentialException; import org.identityconnectors.framework.common.exceptions.PasswordExpiredException; import org.identityconnectors.framework.common.objects.ObjectClass; import org.identityconnectors.framework.common.objects.OperationOptions; import org.identityconnectors.framework.common.objects.Uid; import org.identityconnectors.framework.spi.operations.AuthenticateOp; import org.identityconnectors.oracle.OracleConfiguration.ConnectionType; /** * Authenticate operation. It just tries to create new jdbc connection with * passed user/password. It delegates creation of connection to * {@link OracleConfiguration} * * @author kitko * */ final class OracleOperationAuthenticate extends AbstractOracleOperation implements AuthenticateOp { private final static Log LOG = Log.getLog(OracleOperationAuthenticate.class); OracleOperationAuthenticate(OracleConfiguration cfg, Connection adminConn) { super(cfg, adminConn); } public Uid authenticate(ObjectClass objectClass, String username, GuardedString password, OperationOptions options) { OracleConnectorHelper.checkObjectClass(objectClass, cfg.getConnectorMessages()); new LocalizedAssert(cfg.getConnectorMessages()).assertNotBlank(username, "username"); new LocalizedAssert(cfg.getConnectorMessages()).assertNotNull(password, "password"); if (options != null && Boolean.TRUE.equals(options.getOptions().get("returnUidOnly"))) { return findUserByName(username); } LOG.info("Authenticate user: [{0}]", username); if (cfg.isUseDriverForAuthentication()) { // This means datasource is used, so bypass it doDriverAuthenticate(username, password); } else { doCfgAuthenticate(username, password); } LOG.info("User authenticated : [{0}]", username); return new Uid(username); } /** * @param username * @param password */ private void doDriverAuthenticate(String username, GuardedString password) { OracleDriverConnectionInfo connInfo = OracleSpecifics.parseConnectionInfo(adminConn, cfg.getConnectorMessages()); OracleDriverConnectionInfo newInfo = new OracleDriverConnectionInfo.Builder().setvalues(connInfo).setUser(username) .setPassword(password).build(); Connection conn = null; try { conn = OracleSpecifics.createDriverConnection(newInfo, cfg.getConnectorMessages()); } catch (RuntimeException e) { handleAuthenticationException(username, password, e); } finally { SQLUtil.closeQuietly(conn); } } private void doCfgAuthenticate(String username, GuardedString password) { Connection conn = null; try { conn = cfg.createUserConnection(username, password); } // adminConn.getClass().getInterfaces() // adminConn.getClass().getMethod("getConnection").invoke(adminConn) catch (RuntimeException e) { handleAuthenticationException(username, password, e); } // When we get connection from DS, test the connection try { if (ConnectionType.DATASOURCE.equals(cfg.getConnType())) { doExtraConnectionTest(username, conn); killDSConnection(conn); } } finally { SQLUtil.closeQuietly(conn); } } private void handleAuthenticationException(String username, GuardedString password, RuntimeException e) { LOG.info("Authentication of user [{0}] failed", username); if (e.getCause() instanceof SQLException) { SQLException sqlE = (SQLException) e.getCause(); if (StringUtil.isBlank(sqlE.getSQLState())) { handleNotCompletedSQLEXception(e, sqlE, username, password); } else { handleSQLException(e, sqlE, username, password); } } throw e; } private void killDSConnection(Connection conn) { try { // Here we will kill the session on oracle to not pool the // connection OracleSpecifics.killConnection(adminConn, conn); } catch (SQLException e) { throw new IllegalStateException("Cannot kill the getConnection retrieved from DS", e); } // And now force the usage of the connection, which should hint the app // server to discard the connection from pool try { OracleSpecifics.testConnection(conn); // If we get here, we could have security hole, because next // connection from DS will not check password (will return cached // connection) // throwing the exception we will force the admin to fix the problem throw new IllegalArgumentException("Connection from DS not killed"); } catch (Exception e) { // Expected, the connection test should not succeed } } private Uid findUserByName(String username) { if (new OracleUserReader(adminConn, cfg.getConnectorMessages()).userExist(username)) { return new Uid(username); } throw new InvalidCredentialException(cfg.getConnectorMessages().format( OracleMessages.MSG_CANNOT_FIND_USER, null, username)); } /** * Maybe we do not need this method, because now ds connection are killed. * But there could be some other reason why the connections can remain/be * initialized in the pool */ private void doExtraConnectionTest(String username, Connection conn) { try { OracleSpecifics.testConnection(conn); } catch (RuntimeException e) { // This should not happen throw new ConnectorException("Error testing connection after succesufull authenticate", e); } // Now imagine the case we get connection from pool (key is // user/password), but the user is already expired or locked. // Then we will get connection, that is open and we cannot find out that // user is locked // The solution would be , that datasource pool would not cache // connections retrieved by ds.getConnection(user, password) // But this is not configurable // so we will look at DBA_USERS view to find the state of user try { UserRecord userRecord = new OracleUserReader(adminConn, cfg.getConnectorMessages()) .readUserRecord(username); if (userRecord == null) { throw new ConnectorException( MessageFormat .format("Cannot find userRecord for user [{0}] at authenticate, probably user is deleted", username)); } if (StringUtil.isBlank(userRecord.getStatus())) { // should not happen throw new ConnectorException(MessageFormat.format( "userRecord.getStatus() is blank for user [{0}]", username)); } if (OracleUserReader.isUserLocked(userRecord)) { throw new InvalidCredentialException("User account is locked"); } else if (OracleUserReader.isPasswordExpired(userRecord)) { PasswordExpiredException passwordExpiredException = new PasswordExpiredException("Password expired"); passwordExpiredException.initUid(new Uid(username)); throw passwordExpiredException; } // http://www.dbforums.com/oracle/1617629-account_status-dba_users.html // We will not look at other values } catch (SQLException e) { throw new ConnectorException(MessageFormat.format( "Cannot find userRecord for user [{0}] at authenticate", username), e); } } private void handleSQLException(RuntimeException e, SQLException sqlE, String username, GuardedString password) { if ("72000".equals(sqlE.getSQLState()) && 1017 == sqlE.getErrorCode()) { // By contract we must throw PasswordExpiredException when account // is expired // Wrong user or password, log it here and rethrow LOG.info(sqlE, "Oracle.authenticate : Invalid user/passord for user: {0}", username); throw new InvalidCredentialException("Oracle.authenticate : Invalid user/password", sqlE); } else if ("99999".equals(sqlE.getSQLState()) && 28000 == sqlE.getErrorCode()) { InvalidCredentialException icException = new InvalidCredentialException("User account is locked", sqlE); throw icException; } else if ("99999".equals(sqlE.getSQLState()) && 28001 == sqlE.getErrorCode()) { PasswordExpiredException passwordExpiredException = new PasswordExpiredException("Password expired", sqlE); passwordExpiredException.initUid(new Uid(username)); throw passwordExpiredException; } throw e; } private void handleNotCompletedSQLEXception(RuntimeException e, SQLException sqlE, String username, GuardedString password) { // If we get exception without sql state, we must try to look at the // message of exception. // Status of user in DBA_USERS view could also help, but the real cause // of exception can be absolutely different String msg = sqlE.getMessage(); if (StringUtil.isBlank(msg)) { // here we cannot do anything to determine the cause, just throw the // original wrapper throw e; } if (msg.contains("ORA-01017")) { LOG.info(sqlE, "Oracle.authenticate : Invalid user/passord for user: {0}", username); throw new InvalidCredentialException("Oracle.authenticate : Invalid user/password", sqlE); } else if (msg.contains("ORA-28000")) { InvalidCredentialException icException = new InvalidCredentialException("User account is locked", sqlE); throw icException; } else if (msg.contains("ORA-28001")) { PasswordExpiredException passwordExpiredException = new PasswordExpiredException("Password expired", sqlE); passwordExpiredException.initUid(new Uid(username)); throw passwordExpiredException; } throw e; } }