четверг, 25 февраля 2010 г.

End to end identity propagation with jboss 4, hibernate and oracle. Part 1

Several times i was asked by customers for create high security solutions with following requirements:
  • End to end identity propagation across all application levels - web/middle tier and database.
  • No password must be stored in jboss data source configuration.
  • Secure solution must work on public enviroment.
  • etc.
Is it possible ? - Yes ! But how to implement this and what does it mean?
  • It means, that Egor Lupan, for example, with account elupan can not have anonymous/impersonalized resourses or connections.
  • Also it means, that elupan must have accounts on web app/middle tier/db.
  • It means, that administrative overhead for manage accounts not allowed.
Do you think, that need to use LDAP as central point of authorization and authentication ? Probably, but i found another way. Lets look how it can be solved for typical CRUD web application with hibernate on jboss 4 with oracle rdbms as backend.

To minimaze administrative overhead oracle will be used as authorization, authentication and role provider for JAAS login module without any additional user/roles tables, just reuse the oracle all_users dictionary.


OracleDatabaseServerLoginModule source code:


package org.jboss.security.auth.spi;

import org.jboss.tm.TransactionDemarcationSupport;
import org.jboss.security.SimpleGroup;

import javax.security.auth.login.LoginException;
import javax.security.auth.login.FailedLoginException;
import javax.naming.NamingException;
import javax.naming.InitialContext;
import javax.sql.DataSource;
import javax.transaction.Transaction;
import java.sql.SQLException;
import java.sql.ResultSet;
import java.sql.PreparedStatement;
import java.sql.Connection;
import java.security.acl.Group;
import java.security.Principal;
import java.util.HashMap;


import java.security.Principal;
import java.security.acl.Group;


import javax.resource.spi.security.PasswordCredential;
import javax.security.auth.Subject;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.login.LoginException;


import org.jboss.security.SimplePrincipal;


/**
* User: iazarny
* Date: 04.06.2008
* Time: 17:15:08
*/
public class OracleDatabaseServerLoginModule extends DatabaseServerLoginModule {



/**
* Oracle already validate the user
*
* @param inputPassword
* @param expectedPassword
* @return true
*/
protected boolean validatePassword(String inputPassword, String expectedPassword) {
return expectedPassword != null;
}

/**
* Get the expected password for the current username available via
* the getUsername() method. This is called from within the login()
* method after the CallbackHandler has returned the username and
* candidate password.
*
* @return the valid password String
*/
protected String getUsersPassword() throws LoginException {

String[] info = getUsernameAndPassword();
String usernameInput = info[0];
String passwordInput = info[1];


String username = getUsername();
String password = null;
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;

Transaction tx = null;
if (suspendResume) {
tx = TransactionDemarcationSupport.suspendAnyTransaction();
}

try {
InitialContext ctx = new InitialContext();
DataSource ds = (DataSource) ctx.lookup(dsJndiName);
// This is main point if connection obtained succesfuly - than pwd is valid
conn = ds.getConnection(username, passwordInput);
// Get the password

// log.trace("Excuting query: "+principalsQuery+", with username: "+username);
ps = conn.prepareStatement(principalsQuery);
ps.setString(1, username.toUpperCase());
rs = ps.executeQuery();
if (rs.next() == false) {
throw new FailedLoginException("No matching username found in Principals");
}
password = rs.getString(1);
password = convertRawPassword(password);
}
catch (NamingException ex) {
LoginException le = new LoginException("Error looking up DataSource from: " + dsJndiName);
le.initCause(ex);
throw le;
}
catch (SQLException ex) {
LoginException le = new LoginException("Query failed");
le.initCause(ex);
throw le;
}
finally {
if (rs != null) {
try {
rs.close();
}
catch (SQLException e) {
}
}
if (ps != null) {
try {
ps.close();
}
catch (SQLException e) {
}
}
if (conn != null) {
try {
conn.close();
}
catch (SQLException ex) {
}
}
if (suspendResume) {
TransactionDemarcationSupport.resumeAnyTransaction(tx);
}
}
return password;
}


/**
* Execute the rolesQuery against the dsJndiName to obtain the roles for
* the authenticated user.
*
* @return Group[] containing the sets of roles
*/
protected Group[] getRoleSets() throws LoginException {
String username = getUsername();
Group[] roleSets = getRoleSets(username, dsJndiName, rolesQuery, this,
suspendResume);
return roleSets;
}


/**
* Execute the rolesQuery against the dsJndiName to obtain the roles for
* the authenticated user.
*
* @return Group[] containing the sets of roles
*/
private Group[] getRoleSets(String username, String dsJndiName,
String rolesQuery, AbstractServerLoginModule aslm, boolean suspendResume)
throws LoginException {
Connection conn = null;
HashMap setsMap = new HashMap();
PreparedStatement ps = null;
ResultSet rs = null;

Transaction tx = null;
if (suspendResume) {
tx = TransactionDemarcationSupport.suspendAnyTransaction();
}

try {

String[] info = getUsernameAndPassword();
String usernameInput = info[0];
String passwordInput = info[1];

InitialContext ctx = new InitialContext();
DataSource ds = (DataSource) ctx.lookup(dsJndiName);
conn = ds.getConnection(usernameInput,passwordInput);
// Get the user role names
ps = conn.prepareStatement(rolesQuery);
try {
ps.setString(1, username);
}
catch (ArrayIndexOutOfBoundsException ignore) {
// The query may not have any parameters so just try it
}
rs = ps.executeQuery();
if (rs.next() == false) {
if (aslm.getUnauthenticatedIdentity() == null)
throw new FailedLoginException("No matching username found in Roles");
/* We are running with an unauthenticatedIdentity so create an
empty Roles set and return.
*/
Group[] roleSets = {new SimpleGroup("Roles")};
return roleSets;
}

do {
String name = rs.getString(1);
String groupName = rs.getString(2);
if (groupName == null || groupName.length() == 0)
groupName = "Roles";
Group group = (Group) setsMap.get(groupName);
if (group == null) {
group = new SimpleGroup(groupName);
setsMap.put(groupName, group);
}

try {
Principal p = aslm.createIdentity(name);
group.addMember(p);
}
catch (Exception e) {
//log.debug("Failed to create principal: " + name, e);
}
} while (rs.next());
}
catch (NamingException ex) {
LoginException le = new LoginException("Error looking up DataSource from: " + dsJndiName);
le.initCause(ex);
throw le;
}
catch (SQLException ex) {
LoginException le = new LoginException("Query failed");
le.initCause(ex);
throw le;
}
finally {
if (rs != null) {
try {
rs.close();
}
catch (SQLException e) {
}
}
if (ps != null) {
try {
ps.close();
}
catch (SQLException e) {
}
}
if (conn != null) {
try {
conn.close();
}
catch (Exception ex) {
}
}
if (suspendResume) {
TransactionDemarcationSupport.resumeAnyTransaction(tx);
//if (trace)
// log.trace("resumeAnyTransaction");
}
}
Group[] roleSets = new Group[setsMap.size()];
setsMap.values().toArray(roleSets);
return roleSets;
}



}

How it works ?
Login module get the provided login user and password and try to create the real connection to oracle via data source, that configured as application managed security, for this kind of data sources not need to provide login and password in data source configuration. In getUsersPassword() method need to verify provided username/password in oracle via getting connection. In case if connection succesful login module get configured roles for username.

Data source configuration for login module will be following:


<?xml version="1.0" encoding="UTF-8"?>

<datasources>

<local-tx-datasource>

<jndi-name>CrTimeAuthDS</jndi-name>

<connection-url>jdbc:oracle:thin:@127.0.0.1:1521:az3</connection-url>

<driver-class>oracle.jdbc.driver.OracleDriver</driver-class>

<!-- username and password not necessary, because of application-managed-security -->

<user-name></user-name>

<password></password>

<exception-sorter-class-name>org.jboss.resource.adapter.jdbc.vendor.OracleExceptionSorter

</exception-sorter-class-name>

<min-pool-size>10</min-pool-size>

<max-pool-size>20</max-pool-size>

<blocking-timeout-millis>1000</blocking-timeout-millis>

<idle-timeout-minutes>1</idle-timeout-minutes>

<application-managed-security/>

<metadata>

<type-mapping>Oracle9i</type-mapping>

</metadata>

</local-tx-datasource>

</datasources>


JBoss login configuration:


<!-- part of login-config.xml -->

<application-policy name="crtime-webapp">

<authentication>

<login-module code="org.jboss.security.auth.spi.OracleDatabaseServerLoginModule" flag="required">

<module-option name="unauthenticatedIdentity">anonymous</module-option>

<module-option name = "dsJndiName">java:/CrTimeAuthDS</module-option>

<module-option name = "principalsQuery">SELECT USER_ID FROM all_users WHERE username=?</module-option>

<module-option name = "rolesQuery">SELECT granted_role, 'Roles' FROM user_role_privs WHERE upper(username)=upper(?)</module-option>

</login-module>

</authentication>

</application-policy>




For advanced programmers thats enough to catch the main idea. Only just small hint - we will use two datasources one for login module, second for application. In next part - Jboss/Tomcat context configuration, second datasource configuration, hibernate triks.

Комментариев нет:

Отправить комментарий