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 );
}
}