login templates

equanda uses an internal mechanism to configure the very granular access rights for the user interface. This is based on user information in the database. This module allows you to generate a login module which prevents duplication of user information between the application and the JAAS module. For efficiency this information is stored in a cache.

For integration you have to configure that the login module is included in the jboss security system (as referenced in the project), that the user information is made available in the web layer (for tapestry) and that there is a cache where the login information can be stored.
We show here two methods for integrating in tapestry, one with basic authentication and one using ChenilleKit-access.

Integration in the web layer with basic authentication

web.xml


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<display-name>Competency Analyzer user interface</display-name>
<context-param>
<!-- The only significant configuration for Tapestry 5, this informs Tapestry of where to look for pages, components and mixins. -->
<param-name>tapestry.app-package</param-name>
<param-value>application.package.base.gui</param-value>
</context-param>
<filter>
<filter-name>app</filter-name>
<filter-class>org.apache.tapestry5.TapestryFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>app</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<!-- uncomment when you want import to be possible directly
<servlet>
<servlet-name>import</servlet-name>
<servlet-class>org.equanda.ymport.Servlet</servlet-class>
<init-param>
<param-name>DatabaseMap</param-name>
<param-value>application.package.base.ymport.DatabaseMap</param-value>
</init-param>
</servlet>
-->

<servlet-mapping>
<servlet-name>import</servlet-name>
<url-pattern>/import</url-pattern>
</servlet-mapping>

<security-constraint>
<web-resource-collection>
<web-resource-name>Secure Content</web-resource-name>
<url-pattern>/*</url-pattern>
</web-resource-collection>

<auth-constraint>
<role-name>LocalUser</role-name>
<role-name>LocalAdmin</role-name>
</auth-constraint>

<!-- uncomment to force https to be used
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
-->
</security-constraint>

<login-config>
<auth-method>BASIC</auth-method>
<realm-name>synergetics</realm-name>
</login-config>

<security-role>
<description>Normal User</description>
<role-name>LocalUser</role-name>
</security-role>
<security-role>
<description>Administrator</description>
<role-name>LocalAdmin</role-name>
</security-role>
</web-app>

AppModule

In your tapestry application module, the following needs to be included


/* include this forwhen putting your app behind SSL/https
public void contributeMetaDataLocator( MappedConfiguration<String, String> configuration )
{
configuration.add( MetaDataConstants.SECURE_PAGE, "true" );
}
*/

/**
* User management filter, the login method on the user should be called when the user logs in. If the user does not
* yet exist (which is possible when logging in using some SSO solution, then the user should be created).
*
* @param log log
* @param requestGlobals to have access to servlet request
* @param persistentLocale locale to allow chaning to user preference
* @param loginInfoService login info service
* @return request filter
*/
public RequestFilter buildLoginFilter( final Logger log,
final RequestGlobals requestGlobals,
final PersistentLocale persistentLocale,
final LoginInfoService loginInfoService )
{
return new RequestFilter()
{
public boolean service( Request request, Response response, RequestHandler handler )
throws IOException
{
// assure we have a user object, make it available on the session, login and set locale

HttpServletRequest servletRequest = requestGlobals.getHTTPServletRequest();
HttpSession session = servletRequest.getSession();
LoginInfo loginInfo = (LoginInfo) session.getAttribute( T5guiModule.SESSION_USER );
if ( null == loginInfo )
{
try
{
LoginCache loginCache = LoginCache.getLoginCache();
Principal principal = requestGlobals.getHTTPServletRequest().getUserPrincipal();
if ( null != principal )
{
String userName = principal.getName();
loginInfo = loginCache.getWithAuth( userName );
if ( loginInfo == null )
{
EquandaUser user = EquandaUser.equandaCreate( EquandaUserConstants.TYPE_User );
user.setUserName( userName );
user.equandaUpdate();
loginInfo = loginCache.getWithAuth( userName );
}
session.setAttribute( T5guiModule.SESSION_USER, loginInfo );
}
}
catch ( Exception ex )
{
log.error( "problem while logging in user", ex );
}
}
loginInfoService.setLoginInfo( loginInfo );
try
{
if ( null != loginInfo && null != loginInfo.getUser() )
{
// manual setting of SelectorsState, cannot use login in mediator as this may be called in another thread
// .....
Object xxx = ( (EquandaUser) loginInfo.getUser() ).getXxx();
SelectorsState.setFilter( "Xxx", xxx );

// set the user's language
if ( loginInfo.getUser().getLanguage() != null )
{
persistentLocale.set( new Locale( loginInfo.getUser().getLanguage() ) );
}
}
else
{
SelectorsState.setFilter( "Xxx", null );
}
}
catch ( Exception ex )
{
log.error( "problem while logging in user", ex );
}

// The reponsibility of a filter is to invoke the corresponding method
// in the handler. When you chain multiple filters together, each filter
// received a handler that is a bridge to the next filter.
return handler.service( request, response );
}
};
}

/**
* This is a contribution to the RequestHandler service configuration. This is how we extend Tapestry using the
* timing filter. A common use for this kind of filter is transaction management or security.
*
* @param configuration configuration to add to
* @param loginFilter filter info
*/
public void contributeRequestHandler( OrderedConfiguration<RequestFilter> configuration,
@InjectService( "LoginFilter" )RequestFilter loginFilter )
{
// Each contribution to an ordered configuration has a name, When necessary, you may
// set constraints to precisely control the invocation order of the contributed filter
// within the pipeline.

configuration.add( "Login", loginFilter );
}

This does a couple of things. It assures a LoginInfo object is created which contains the user information to be used by the authorization services and binding prefixes. This is also stored in the session, though this is actually optional.
The EquandaUser record is automatically created if it doesn't exists. In principle this is not needed when the generated LoginModul is used, but can be required when the login credential come from another source (e.g. LDAP, SSO).

The login()_ action is called on the _EquandaUser object. This allows custom definition of the meaning of logging in. It could be used for auditing, billing,...

The locale is set to the locale for the user.

Integration in the web layer using chenillekit-access

You just use the normal web.xml files. The authentication and authorization are handled by the application.
The most important difference with the solution above is bootstrapping. With above solution, you can configure a user who has "superuser" access on the appserver level and this user can login to the application (in the properties files referenced by the login configuration). With this solution, a user needs to exist in the EquandaUser table to allow login to secured pages.

AppModule

The following needs to be included in the tapestry application module file.


public static void bind( ServiceBinder binder )
{
binder.bind( AppServerLoginService.class, JBossAppServerLoginService.class );

// assure that the ContextClassLoader is used by javassist (seems to not be the default, needed in JBoss)
System.out.println( "Desc.useContextClassLoader = " + Desc.useContextClassLoader );
javassist.runtime.Desc.useContextClassLoader = true;
}

public static void contributeApplicationDefaults( MappedConfiguration<String, String> configuration )
{
configuration.add( ChenilleKitAccessConstants.LOGIN_PAGE, "login" );
}

public static void contributeAuthRedirectService( MappedConfiguration<String, Class> configuration )
{
configuration.add( ChenilleKitAccessConstants.WEB_USER_AUTH_SERVICE, EquandaAuthService.class );
}

JBossAppServerLoginService

Login to the application server and fill in the AuthAndConfig information for equanda.


/**
* Assure the login in JBoss is handled correctly based on the logged in user (using chenillekit-access).
*
* @author <a href="mailto:joachim@progs.be">Joachim Van der Auwera</a>
*/
public class JBossAppServerLoginService
implements AppServerLoginService
{
private static final Logger log = Logger.getLogger( JBossAppServerLoginService.class );
private final PersistentLocale persistentLocale;
private final LoginInfoService loginInfoService;

public JBossAppServerLoginService( final PersistentLocale persistentLocale,
final LoginInfoService loginInfoService )
{
this.persistentLocale = persistentLocale;
this.loginInfoService = loginInfoService;
}

public void appServerLogin( WebSessionUser user )
{
LoginInfo loginInfo = null;

// appserver login
if ( null == user )
{
SecurityAssociation.clear();
}
else
{
loginInfo = ( (EquandaWebSessionUser) user ).getLoginInfo();
SecurityAssociation.setPrincipal( new SimplePrincipal( loginInfo.getUserName() ) );
SecurityAssociation.setCredential( loginInfo.getPassword().toCharArray() );
}

// now also assure the login info service is filled and info copied
loginInfoService.setLoginInfo( loginInfo );
try
{
if ( null != loginInfo && null != loginInfo.getUser() )
{
// manual setting of SelectorsState, cannot use login in mediator as this may be called in another thread
// .....
Object xxx = ( (EquandaUser) loginInfo.getUser() ).getXxx();
SelectorsState.setFilter( "Xxx", xxx );

// set the user's language
if ( loginInfo.getUser().getLanguage() != null )
{
persistentLocale.set( new Locale( loginInfo.getUser().getLanguage() ) );
}
}
else
{
SelectorsState.setFilter( "Organization", null );
}
}
catch ( Exception ex )
{
log.error( "problem while logging in user", ex );
}
}
}

EquandaAuthService


/**
* AuthService implementation for chenillekit-access
*
* @author <a href="mailto:joachim@synergetics.be">Joachim Van der Auwera</a>
*/
public class EquandaAuthService
implements AuthService<EquandaWebSessionUser>
{
private static final Logger log = Logger.getLogger( EquandaAuthService.class );

public EquandaWebSessionUser doAuthenticate( String userName, String password )
{
if ( log.isDebugEnabled() ) log.debug( "try to authenticate " + userName );
if ( null == userName ) return null;
if ( null == password ) password = "";
try
{
LoginCache loginCache = LoginCache.getLoginCache();
LoginInfo loginInfo = loginCache.getWithAuth( userName );
if ( !password.equals( loginInfo.getPassword() ) ) return null;
if ( log.isDebugEnabled() ) log.debug( "authentication succeeded for " + userName );
return new EquandaWebSessionUser( loginInfo );
}
catch ( Exception ex )
{
log.error( "problem while logging in user", ex );
return null;
}
}
}

EquandaWebSessionUser


/**
* WebSessionUser from chenillekit for use in equanda applications.
*
* @author <a href="mailto:joachim@progs.be">Joachim Van der Auwera</a>
*/
public class EquandaWebSessionUser
implements WebSessionUser
{
private LoginInfo loginInfo;

public EquandaWebSessionUser( LoginInfo li )
{
loginInfo = li;
}

public LoginInfo getLoginInfo() { return loginInfo; }

public int getRoleWeight()
{
if ( loginInfo.isGuiAccessProhibited() ) return 0;
if ( loginInfo.isGuiAdministrator() ) return 3;
return 1;
}

public String[] getGroups()
{
List<String> roles = loginInfo.getRoles();
return roles.toArray( new String[roles.size()] );
}
}

login page

template


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<head>
<title>${equanda-message:title.Login}</title>
</head>

<body>

<div t:type="ck/RoundCornerContainer"
fgcolor="#749572"
bgcolor="#FFFFFF"
style="padding: 40px;" >

<div style="padding: 40px; background-color:#749572;" >

<h1>${equanda-message:title.Login}</h1>

<t:form action="#">
<t:errors/>
<t:if test="isLoginAllowed()">
<t:parameter name="else"><span class="loginError">${equanda-message:error.TooManyLoginAttempts}</span></t:parameter>
<br/>
<label>Username</label>
<input t:type="TextField" t:value="userName" t:id="inputUserName" type="text" t:validate="required" />
<br/>
<label>Password</label>
<input t:type="PasswordField" t:value="password" t:id="inputPassword" type="password" t:validate="required" />
<br/>
<input type="submit"/>
</t:if>
</t:form>

</div>

</div>

</body>
</html>

code


/**
* chenillekit-access login page
*
* @author <a href="mailto:joachim@synergetics.be">Joachim Van der Auwera</a>
*/
public class Login
{
private static final int MAX_LOGIN_ATTEMPTS = 3;

@Inject
private Logger log;

@Inject
private AuthRedirectService authService;

@Persist
@Property
private String userName;

@Property
private String password;

@Inject
private ComponentResources resources;

@Persist
private int loginAttempts;

final public boolean isLoginAllowed()
{
return loginAttempts < MAX_LOGIN_ATTEMPTS;
}

final public String onSuccess()
{
loginAttempts++;
String page = authService.doAuthenticate( userName, password );
if ( null != page ) resources.discardPersistentFieldChanges();
return page;
}
}

logout page

template


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<head>
<title>${equanda-message:title.Logout}</title>
</head>

<body>

<div t:type="ck/RoundCornerContainer"
fgcolor="#749572"
bgcolor="#FFFFFF"
style="padding: 40px;" >

<div style="padding: 40px; background-color:#749572;" >
<h1>${equanda-message:title.Logout}</h1>

<p>${equanda-message:logout.YouHaveBeenLoggedOut}</p>

<p><t:pagelink t:page="Start">${equanda-message:logout.BackToStart}</t:pagelink></p>
</div>

</div>

</body>
</html>

code


/**
* Logout of the application.
*
* @author <a href="mailto:joachim@synergetics.be">Joachim Van der Auwera</a>
*/
public class Logout
{
@Inject
private WebSessionUserService webSessionUserService;

final public void beginRender()
{
// logout
webSessionUserService.setUser( null );
}
}

JBoss configuration

The login templates (amongst others) generate a LoginModule which can be integrated in JBoss. This is a login module which checks the existing users and their credentials (password) from the application database (the EquandaUser table).
To allow bootstrapping, this module falls back to a users-passwords login module which gets the username and credentials from properties files.
Configuration can be done by including a fragment like the following in JBoss in server/your-config/conf/login-config.xml.


<application-policy name = "equanda">
<authentication>
<login-module code = "org.equanda.example.login.LoginModule" flag = "required" >
<module-option name="usersProperties">props/default-users.properties</module-option>
<module-option name="rolesProperties">props/default-roles.properties</module-option>
<module-option name="unauthenticatedIdentity">anonymous</module-option>
</login-module>
</authentication>
</application-policy>
  • The package name needs to match the configuration in dm.ini ("login" section, "package" setting).
  • The application-policy name should match the "security-domain" setting in the "extra" section.
  • The referred properties files contain the fallback user definition. These files should probably be empty for a running application as these contain backdoor credentials.

To assure it all works, a cache needs to be made available in jboss for use by equanda. This cache can be shared between several equanda applications (the package names are used to disciminate objects). In your deploy directory, you need a file called "equanda-cache-service.xml" with the following content.


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

<!-- ============================================================ -->
<!-- Cache which is used by equanda -->
<!-- ============================================================ -->
<mbean code="org.jboss.cache.TreeCache"
name="jboss.cache:service=equandaCache">

<depends>jboss:service=Naming</depends>
<depends>jboss:service=TransactionManager</depends>

<!-- Configure the TransactionManager -->
<attribute name="TransactionManagerLookupClass">org.jboss.cache.JBossTransactionManagerLookup</attribute>

<!--
Node locking level : SERIALIZABLE
REPEATABLE_READ (default)
READ_COMMITTED
READ_UNCOMMITTED
NONE
-->
<attribute name="IsolationLevel">REPEATABLE_READ</attribute>

<!-- Valid modes are LOCAL
REPL_ASYNC
REPL_SYNC
-->
<attribute name="CacheMode">LOCAL</attribute>

<!-- Must be true if any entity deployment uses a scoped classloader -->
<attribute name="UseRegionBasedMarshalling">true</attribute>
<!-- Must match the value of "useRegionBasedMarshalling" -->
<attribute name="InactiveOnStartup">true</attribute>

<!-- The max amount of time (in milliseconds) we wait until the
initial state (ie. the contents of the cache) are retrieved from
existing members.
-->
<attribute name="InitialStateRetrievalTimeout">17500</attribute>

<!-- Number of milliseconds to wait until all responses for a
synchronous call have been received.
-->
<attribute name="SyncReplTimeout">17500</attribute>

<!-- Max number of milliseconds to wait for a lock acquisition -->
<attribute name="LockAcquisitionTimeout">15000</attribute>

<!-- Name of the eviction policy class. -->
<attribute name="EvictionPolicyClass">org.jboss.cache.eviction.LRUPolicy</attribute>

<!-- Specific eviction policy configurations. This is LRU -->
<attribute name="EvictionPolicyConfig">
<config>
<attribute name="wakeUpIntervalSeconds">5</attribute>
<!-- Cache wide default -->
<region name="/_default_">
<attribute name="maxNodes">500</attribute>
<attribute name="timeToLiveSeconds">1800</attribute>
</region>
</config>
</attribute>

</mbean>

</server>
  • 1. login templates
  • 1.1. Integration in the web layer with basic authentication
  • 1.1.1. web.xml
  • 1.1.2. AppModule
  • 1.2. Integration in the web layer using chenillekit-access
  • 1.2.1. AppModule
  • 1.2.2. JBossAppServerLoginService
  • 1.2.3. EquandaAuthService
  • 1.2.4. EquandaWebSessionUser
  • 1.2.5. login page
  • 1.2.6. logout page
  • 1.3. JBoss configuration