Category Archives: tapestry5

authentication in tapestry5 using chenillekit-access and application server login

Most web applications need authentication and authorization support. For tapestry5, there are a couple of libraries which promise to help in providing this. The two most advanced are probably tapestry-spring-security and chenillekit-access. For the application I am working on, my choice was to use chenillekit. Both of them are rather thin in documentation, but chenillekit is reasonable easy to grasp from the source.

One of the principal extra requirements I have is to be able to link with the application server to assure the web application is also logged in using the current user to allow the JEE beans to work in the right security context.

Plain chenillekit-access to protect your pages

Base configuration for chenillekit-acces is to configure the class used to store the “WebSessionUser” information and service which checks whether the credentials are correct.

These need to be wired in the application module.

    public static void contributeAuthenticationService( OrderedConfiguration<AuthenticationService> configuration )
    {
        configuration.add( "MINE", new MyAuthService() );
    }

    public static void contributeAccessValidator( MappedConfiguration<String, Class> configurations )
    {
        configurations.add( ChenilleKitAccessConstants.WEB_SESSION_USER_KEY, MyWebSessionUser.class );
    }

The WebSessionUser basically gives access to the role information to check base authorizations (which can be configured on page level). The actual authentication is provided in the AuthService, which could look like the following.

public class MyAuthService
    implements AuthenticationService
{
    private static final Logger log = Logger.getLogger( MyAuthService.class );

    public MyWebSessionUser 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 ( loginInfo != null && !loginInfo.checkPassword( password) ) return null;
            if ( log.isDebugEnabled() ) log.debug( "authentication succeeded for " + userName );
            return new MyWebSessionUser( loginInfo );
        }
        catch ( Exception ex )
        {
            log.error( "problem while logging in user", ex );
            return null;
        }
    }
}

When the user tries to access a page which needs login, the user should be redirected to the login page, supply credentials and then continue on the requested page.
Chenillekit provides a login component which can be embedded in your pages, but if you want a separate login page, a bit more effort is required. The return is not fully implemented in 1.0 (probably because you need tapestry 5.1 to get this fully working with all parameters), so we need some tricks to get that to work.

First wire the login page in your application module.

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

Then the login page code. This explicitly only allows you three attempts to login, and forces you to create a new session (most likely by closing the browser) before you can try again.

public class Login
{
    private static final int MAX_LOGIN_ATTEMPTS = 3;

    @Inject
    private Logger logger;

    @Persist
    @Property
    private String userName;

    @Property
    private String password;

    @ApplicationState
    private EquandaWebSessionUser webSessionUser;

    @Inject
    private Messages messages;

    @Inject
    @Local
    private AuthenticationService authenticationService;

    @Inject
    private ComponentResources resources;

    @Persist
    private int loginAttempts;

    private EquandaWebSessionUser tmpUser;

    @Inject
    private Cookies cookies;

    @Inject
    private ContextValueEncoder valueEncoder;

    @Inject
    private LinkFactory linkFactory;

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

    void onValidateForm()
    {
        loginAttempts++;
        tmpUser = (EquandaWebSessionUser)authenticationService.doAuthenticate( userName, password );
    }

    Object onSuccess()
    {
        if ( null != tmpUser ) resources.discardPersistentFieldChanges();
        webSessionUser = tmpUser;
        if ( null != tmpUser )
        {
            //asoManager.set(  );
            String prevPage = cookies.readCookieValue( ChenilleKitAccessConstants.REQUESTED_PAGENAME_COOKIE );
            String prevContext = cookies.readCookieValue( ChenilleKitAccessConstants.REQUESTED_EVENTCONTEXT_COOKIE );
            if ( prevPage != null )
            {
                cookies.removeCookieValue( ChenilleKitAccessConstants.REQUESTED_EVENTCONTEXT_COOKIE );
                cookies.removeCookieValue( ChenilleKitAccessConstants.REQUESTED_PAGENAME_COOKIE );

                return prevPage;
            }
        }
        return null;
    }
}

The template looks like this (using a but more chenillekit to make it look “better”).

<!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 style="text-align:center;">${equanda-message:title.Login}</h1>

        <t:form action="#">
            <t:errors/>
            <t:if test="isLoginAllowed()">
                <t:parameter name="else">
                    <div class="loginError" style="text-align:center;">
                        ${equanda-message:error.TooManyLoginAttempts}
                    </div>
                </t:parameter>

                <table align="center">
                    <tr>
                        <td>
                            <label>${equanda-message:login.UserName}</label>
                        </td>
                        <td>
                            <input t:type="TextField" t:value="userName" t:id="inputUserName" type="text" t:validate="required" />
                        </td>
                    </tr>
                    <tr>
                        <td>
                            <label>${equanda-message:login.Password}</label>
                        </td>
                        <td>
                            <input t:type="PasswordField" t:value="password" t:id="inputPassword" type="password" t:validate="required" />
                        </td>
                    </tr>
                    <tr>
                        <td colspan="2" style="text-align:center;">
                            <input type="submit" value="${equanda-message:login.Submit}"/>
                        </td>
                    </tr>
                </table>
            </t:if>
        </t:form>

    </div>

</div>
</body>
</html>

Application server login integration

The application server integration can be handled by using the filter chaining which is done in tapestry. Both page render and component event filters start with the “AccessControl” filter from chenillekit-access. You can add your own filters after that and use this to pickup the WebSessionUser object to check whether a user a logged in (on the web side) to propagate the authentication to the entire application server.

In your application module, contribute to the filters.

    /* Contributes AppServerLoginFilter which handles appserver login */
    public static void contributePageRenderRequestHandler( OrderedConfiguration<PageRenderRequestFilter> configuration, final AppServerLoginFilter accessFilter )
    {
        configuration.add( "AppServerLogin", accessFilter, "after:AccessControl" );
    }

    /* Contributes AppServerLoginFilter which handles appserver login */
    public static void contributeComponentEventRequestHandler( OrderedConfiguration<ComponentEventRequestFilter> configuration, final AppServerLoginFilter accessFilter )
    {
        configuration.add( "AppServerLogin", accessFilter, "after:AccessControl" );
    }

We explicitly ordered these to be after the “AccessControl” filter though this is not really required as these will be in front anyway (they are added with “before:*”).

Your AppServerLoginFilter can now pickup the WebSessionUser object if it exists and forward the login to the application server login service.

public class AppServerLoginFilter
    implements ComponentEventRequestFilter, PageRenderRequestFilter
{
    private final ApplicationStateManager asoManager;
    private final AppServerLoginService appServerLoginService;

    public AppServerLoginFilter( ApplicationStateManager asoManager,
                                 AppServerLoginService appServerLoginService )
    {
        this.asoManager = asoManager;
        this.appServerLoginService = appServerLoginService;
    }

    public void handle( ComponentEventRequestParameters componentEventRequestParameters,
                        ComponentEventRequestHandler componentEventRequestHandler )
        throws IOException
    {
        WebSessionUser wsu = asoManager.getIfExists( EquandaWebSessionUser.class );
        if ( null != wsu ) appServerLoginService.appServerLogin( wsu );
        componentEventRequestHandler.handle( componentEventRequestParameters );
    }

    public void handle( PageRenderRequestParameters pageRenderRequestParameters,
                        PageRenderRequestHandler pageRenderRequestHandler )
        throws IOException
    {
        WebSessionUser wsu = asoManager.getIfExists( EquandaWebSessionUser.class );
        if ( null != wsu ) appServerLoginService.appServerLogin( wsu );
        pageRenderRequestHandler.handle( pageRenderRequestParameters );
    }
}

Complex forms, volatile and FormFragment in tapestry

Using equanda, it is possible to easily generate very complex forms from your domain model.
Such form can have inline display of referenced records, can have client-side switching between display and edit mode, full support for linked records etc.

Unfortunately, this is the theory. It seems that in the last few weeks, some things had gone wrong and certain features stopped working. I have spent the last couple of days trying to track these problems down.
Part of the problem being that it was not known when exactly certain forms stopped working. It had happened in the last month or so, but a lot has changed in this period. Including an upgrade to the GA version of tapestry 5, various enhancements in the forms, including the display/view switching, improvements in some components to make them better and/or faster etc.

Happy hunting.

I was facing two problems.

  • In inline components, the data entered by the user was not persisted
  • An exception occured in some cases when adding items in a (multiple) link field

The second problems seemed easy enough to solve. The loops in the form were marked as volatile. The exception clearly had something to do with that, so changing the loops tot non-volatile seemed to fix that.

For the first problem I wrote a small test application to verify that saving values works when the values to save are stored in container objects. This proved to work just fine, but only when volatile is true.

Documentation says (loop) :

If volatile is true and the Loop is enclosed by a Form, then the normal state saving logic is turned off. Defaults to false, enabling state saving logic within Forms.

or (grid)

If true and the Grid is enclosed by a Form, then the normal state persisting logic is turned off. Defaults to false, enabling state persisting within Forms. If a Grid is present for some reason within a Form, but does not contain any form control components (such as org.apache.tapestry5.corelib.components.TextField), then binding volatile to false will reduce the amount of client-side state that must be persisted.

I think the last part is wrong and should be “binding volatile to true reduces the amount of client-side state”.

Apart from that, it only hints at the problem. When volatile is false, the components seem to be individually serialized to the client. The end result is that the data is stored in deserialized objects which are no longer combined in a list (which is looped). When volatile is true, the objects are not serialized (thus less client-side state), but the form remembers the number of items in the loop. If this changes server side, then uninformative errors occur. So when using objects in a collection which are looped and need updating, volatile has to be set to true.

The caveat of course is that the collection needs to be either persisted or rebuilt.

So, volatile needs to be be true according to the test application. Problem is that it doesn’t make a difference in the full application and causes the second problem I needed to fix. Hmmm.

Tests indicated that the list was iterated and that all the updates were done at the end, all in the same object, overwriting previously stored values. This contrary to the test application.
More tests showed this to be the case for the fields in the embedded component, but not for separate TextField components in the loop.
Then the penny dropped, the component uses FormFragments. These dynamically include parts of a form on the client side, when visualized. Apparently this does not consider the nesting in the form. When changing the component to not use the FormFragment component, the first problem was solved.

Unfortunately, as all loops were volatile again, the second problem has resurfaced and it seemed like I had to choose which bug to solve and which to keep.

Back to square one. I had to figure out which component on the page caused the problem (which “loop”). By accident, I changed the order in which I do my tests, and this helped to pinpoint the offending loop. This was a PagedLoop which seems to have some changing state between requests. Replacing this with a plain Loop component solved (though it changes the display).

including a string in the url for a tapestry event

In tapestry5, events are handled by calling a page. You can pass so-called “context” parameters in such calls and these are passed in the URL (after conversion to string, thanks to the translators and coercers).

When you want to manually modify or add such parameters from the client-side, you need some javascript code which does the url encoding in the same way as tapestry does it internally. The following code snippet shows a way this can be done.

/**
 * Url encode a string to ensure it can be passed to tapestry5
 * @param string
 */
function eqUrlEnc( string )
{
    string = string.replace(/\r\n/g,"\n");
    var res = "";
    for (var n = 0; n < string.length; n++)
    {
        var c = string.charCodeAt( n );
        if ( '$' == string.charAt( n ) )
        {
            res += '$$';
        }
        else if ( eqInRange( c, "AZ" ) || eqInRange( c, "az" ) || eqInRange( c, "09" ) || eqInRange( c, ".." ) )
        {
            res += string.charAt( n )
        }
        else
        {
            var tmp = c.toString(16);
            while ( tmp.length < 4 ) tmp = "0" + tmp;
            res += '$' + tmp;
        }
    }
    return res;
}

function eqInRange( code, range )
{
    return code >= range.charCodeAt( 0 ) &&  code <= range.charCodeAt( 1 );
}