пятница, 26 февраля 2010 г.

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

Complete web application to test oracle login module.



First of all we have to understand where username/password stored after successful authentication to reuse it for our future needs.
By default jboss and tomcat not allow you to get password via SecurityAssociation class, so need additional configuration in context.xml.
Tomcat (and derived jboss web server) has a very useful valve elements for configuration
and additional request/response processing
see http://tomcat.apache.org/tomcat-5.5-doc/config/valve.html for more details.



Following changes in context.xml allow to get user password via SecurityAssociationValve class in jboss and tomcat


<Context cookies="true" crossContext="true">


<Valve className="org.jboss.web.tomcat.security.ExtendedFormAuthenticator"


includePassword="true" />


</Context>









As far as you remember from part 1 im try to minimize administrative overhead, but unfortunately i have to duplicate roles in web and db.

In any case lets create a simple web app with 3 pages and login form. Each role has access to one page only. See security-constraint nodes for page names and roles. web.xml shall looks like this:



web.xml


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

<web-app version="2.4"

xmlns="http://java.sun.com/xml/ns/j2ee"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">




<servlet>

<servlet-name>logon</servlet-name>

<servlet-class>com.crtime.web.Logon</servlet-class>

</servlet>

<servlet-mapping>

<servlet-name>logon</servlet-name>

<url-pattern>*.do</url-pattern>

</servlet-mapping>







<security-role>

<role-name>CRTIME_ADMIN</role-name>

</security-role>




<security-role>

<role-name>CRTIME_MANAGER</role-name>

</security-role>




<security-role>

<role-name>anonymous</role-name>

</security-role>




<security-role>

<role-name>CRTIME_SOMEOTHERROLE</role-name>

</security-role>




<security-constraint>

<display-name>CRTIME_ADMIN</display-name>

<web-resource-collection>

<web-resource-name>Protected Area</web-resource-name>

<url-pattern>/crtime_admin.jsp</url-pattern>

</web-resource-collection>

<auth-constraint>

<role-name>CRTIME_ADMIN</role-name>

</auth-constraint>

<user-data-constraint>

<description>SSL required</description>

<transport-guarantee>CONFIDENTIAL</transport-guarantee>

</user-data-constraint>

</security-constraint>




<security-constraint>

<display-name>CRTIME_MANAGER</display-name>

<web-resource-collection>

<web-resource-name>Protected Area</web-resource-name>

<url-pattern>/crtime_manager.jsp</url-pattern>

</web-resource-collection>

<auth-constraint>

<role-name>CRTIME_MANAGER</role-name>

</auth-constraint>

<user-data-constraint>

<description>SSL required</description>

<transport-guarantee>CONFIDENTIAL</transport-guarantee>

</user-data-constraint>

</security-constraint>




<security-constraint>

<display-name>CRTIME_SOMEOTHERROLE</display-name>

<web-resource-collection>

<web-resource-name>Protected Area</web-resource-name>

<url-pattern>/crtime_someotherrole.jsp</url-pattern>

</web-resource-collection>

<auth-constraint>

<role-name>CRTIME_SOMEOTHERROLE</role-name>

</auth-constraint>

<user-data-constraint>

<description>SSL required</description>

<transport-guarantee>CONFIDENTIAL</transport-guarantee>

</user-data-constraint>

</security-constraint>




<login-config>

<auth-method>FORM</auth-method>

<realm-name>CrTime</realm-name>

<form-login-config>

<form-login-page>/login.jsp</form-login-page>

<form-error-page>/loginError.jsp</form-error-page>

</form-login-config>

</login-config>







<welcome-file-list>

<welcome-file>index.jsp</welcome-file>

</welcome-file-list>




<error-page>

<error-code>403</error-code>

<location>/login.jsp</location>

</error-page>




</web-app>




Login form:





<form name="logonForm" action="j_security_check" method=post>

<input type="text" name="j_username" maxlength=20>

<input type="password" name="j_password" maxlength=20>

<input type="submit" value="Login">

</form>




Login servlet



In case of successful authentication servlet will redirect to referred page.



package com.crtime.web;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.ServletException;
import java.io.IOException;
import org.jboss.web.tomcat.security.login.WebAuthentication;
import org.jboss.web.tomcat.security.SecurityAssociationValve;
import org.apache.catalina.connector.Request;
import org.apache.catalina.Session;
import org.apache.catalina.authenticator.Constants;

/**
* User: iazarny
* Date: 10.06.2008
* Time: 11:21:38
*
* see following link for more details
* http://roneiv.wordpress.com/2008/03/
* http://forum.java.sun.com/thread.jspa?threadID=5293266&tstart=0
* http://www.javaworld.com/javaforums/printthread.php?Board=JavaSecurity&main=2500&type=post
*
*
*/
public class Logon extends HttpServlet {


public void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
try {

WebAuthentication webAuthentication = new WebAuthentication();


boolean stat = webAuthentication.login(request.getParameter("j_username"), request.getParameter("j_password"));

if (stat) {

Request activeRequest = (Request) SecurityAssociationValve.activeRequest.get();
Session session = activeRequest.getSessionInternal(false);
String userNameFromTomCatsession = (String)session.getNote(Constants.SESS_USERNAME_NOTE);
String userPasswordFromTomCatsession = (String)session.getNote(Constants.SESS_PASSWORD_NOTE);
System.out.println("\nuserNameFromTomCatsession =" + userNameFromTomCatsession);
System.out.println("\nuserPasswordFromTomCatsession =" + userPasswordFromTomCatsession);

String referer = request.getHeader("Referer");
System.out.println("\nreferer = " + referer);
response.sendRedirect(referer);
} else {
response.sendRedirect(request.getContextPath() + "/loginError.jsp");
}


} catch (Exception e) {
e.printStackTrace();
response.sendRedirect("loginError.jsp");
}


}


public void doPost(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {

doGet(request, response);

}

}



To be continued ....

четверг, 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.