Devs,
Recently, I wanted to integrate wicket-auth-roles with servlet 3.0
container security. What I ultimately did was create a simple SigninPage
as so:
public class LoginInterceptorPage extends WebPage {
public LoginInterceptorPage() {
continueToOriginalDestination();
}
}
In my Application class that extends AuthenticatedWebApplication I
implement the getSignInPageClass() to return the LoginInterceptorPage.
The trick is to mount the LoginInterceptorPage to a url that the
servletContainer will pick up as a security-contraint.
web.xml snippet
<security-constraint>
<display-name>Constraint1</display-name>
<web-resource-collection>
<web-resource-name>secure</web-resource-name>
<description/>
<url-pattern>/sec/*</url-pattern> <!------------- If
this pattern is seen then redirect to login scheme if not logged in -->
</web-resource-collection>
<auth-constraint>
<description>user</description>
<role-name>USER</role-name>
</auth-constraint>
</security-constraint>
and in Application.init() I added
mountPage("sec/login.html",LoginInterceptorPage.class); // maps to a url
matching pattern set in web.xml
Also I added getSecuritySettings().setAuthorizationStrategy(new
AnnotationsRoleAuthorizationStrategy(this));
to use the annotation @AuthorizeInstantiation to mark pages or packages as
needing authorization.
For the WebSession I implement as so:
public class ServletContainerAuthenticatedWebSession extends
AbstractAuthenticatedWebSession {
private static final long serialVersionUID = 1L;
/**
* @return Current authenticated web session
*/
public static ServletContainerAuthenticatedWebSession get() {
return (ServletContainerAuthenticatedWebSession) Session.get();
}
public ServletContainerAuthenticatedWebSession(Request request) {
super(request);
}
@Override
public Roles getRoles() {
if (isSignedIn()) {
return new UserPrincipalRoles();
}
return null;
}
@Override
public boolean isSignedIn() {
return getRequest().getUserPrincipal() != null;
}
public void signOut() {
if (isSignedIn()) {
try {
getRequest().logout();
} catch (ServletException ex) {
throw new RuntimeException(ex);
}
}
}
private static HttpServletRequest getRequest() {
return (HttpServletRequest)
RequestCycle.get().getRequest().getContainerRequest();
}
}
Where UserPrincipalRoles is :
public class UserPrincipalRoles extends Roles{
public UserPrincipalRoles() {
}
@Override
public boolean hasAllRoles(Roles roles) {
Iterator<String> allRoles = roles.iterator();
boolean result = true;
while(allRoles.hasNext() && result) {
result = getRequest().isUserInRole(allRoles.next());
}
return result;
}
@Override
public boolean hasAnyRole(Roles roles) {
Iterator<String> allRoles = roles.iterator();
boolean result = false;
while(allRoles.hasNext() && !result) {
result = getRequest().isUserInRole(allRoles.next());
}
return result;
}
@Override
public boolean hasRole(String role) {
return getRequest().isUserInRole(role);
}
private static HttpServletRequest getRequest() {
return
(HttpServletRequest)RequestCycle.get().getRequest().getContainerRequest();
}
With this I solely rely on the container to determine if the session is
logged in via getRequest().getUserPrincipal() != null; and role matching
using the container.
What happens is when a page that is annotated with @AuthorizedInstantion
and the user is not authenticated, the
AuthenticatedWebApplication.onUnauthorizedInstantiation is triggered which
then redirects to the LoginInterceptorPage. Since the LoginInterceptorPage
is mounted as sec/Login.html and the security-constraint is looks for
anything in sec/* this forces the auth-method of the login-config in the
web.xml to occur.
<login-config>
<auth-method>FORM</auth-method>
<realm-name>file-realm</realm-name>
<form-login-config>
<form-login-page>/login.html</form-login-page>
<form-error-page>/error.html</form-error-page>
</form-login-config>
</login-config>
In my case I setup the auth-method to be FORM and added a login.html to the
root of the war file. The login.html:
<form action="j_security_check" method=post>
<p><strong>Please Enter Your User Name: </strong>
<input type="text" name="j_username" size="25">
<p><p><strong>Please Enter Your Password: </strong>
<input type="password" size="15" name="j_password">
<p><p>
<input type="submit" value="Submit">
<input type="reset" value="Reset">
</form>
I set up the file-realm for testing, but obviously ldap, database, SPENAGO,
etc can replace this for production. Also html can be jazzed up :)
Once the user successfully authenticated, the servlet container redirects
back to sec/login.html which then calls continueToOriginalDestination() and
ultimately to the page annotated with @AuthorizedInstantion, assuming the
user had the proper role the page would be displayed.
This works great. So from there what I realized is I should mount all
pages that are annotated with @AuthorizedInstantion to a url that is
caught in the security-constraint of web.xml. This I call double checking,
that is first the security container automatically intercepts any call to a
bookmarkable page ie setResponse(SomeSecurePage.class) and forces the
login page, or if a non bookmarkable call occurs the
onUnauthorizedInstantiation is triggered which redirects to the
LoginInterceptorPage which is mounted to a url that is mapped in the
security-contraint in the web.xml. Furthurmore if a bookmarkable secure
page is not mapped to a url matching the security-constraint the
onUnauthorizedInstantiation is triggered which intercepts and forces the
login page of the container.
Nice, with no changes to wicket I can easily integrate wicket-auth-role to
use the servlet 3.0 security. (pre 3.0 the getUserPrincipal and such was
not in HttpServletRequest) Also one could create a login page with wicket
and call request.authenticate(user,pass), but login-config did not like it
when I set the url to that page.
All that being said what I needed was a way to AutoMount any page or
package that had the @AuthorizedInstantion annotation. Based on that I
created an AnnotationProcessor that generated source which was a list of
url and classes. Here is a sample of the generated source
public class MyAppMountInfo implements MountInfo
{
@Override
public List<Mount> getMountPoints() {
List<Mount> ret = new ArrayList<Mount>();
ret.add(new
Mount("sec/custom/Page3.shtml", com.example.ui.user2.Page3.class));
ret.add(new Mount("sec/yo/Yo.html", com.example.ui.user2.Page4.class));
ret.add(new Mount("sec/user.html", com.example.ui.user.Page2.class));
ret.add(new
Mount("sec/AdminPage.shtml", com.example.ui.admin.AdminPage.class));
return ret;
}
}
The class is generated as AppName + MountInfo and implements MountInfo so
that in the app code you can do something like so
public class AutoMounter {
public static boolean mountAll(WebApplication app) {
String mapInfoClassName = app.getClass().getCanonicalName() +
"MountInfo";
try {
MountInfo mountInfo = (MountInfo)
Class.forName(mapInfoClassName).newInstance();
for (MountInfo.Mount mp : mountInfo.getMountPoints()) {
app.mountPage(mp.path, (Class<Page>) mp.pageClass);
}
return true;
} catch (Exception ex) {
return false;
}
}
}
where MountInfo is:
public interface MountInfo {
List<Mount> getMountPoints();
public static class Mount {
String path;
Class<? extends Page> pageClass;
public Mount(String path, Class<? extends Page> pageClass) {
this.path = path;
this.pageClass = pageClass;
}
}
}
In MyApp,init() just call AutoMounter.mountAll(this);
For the processor to actually generate the code you also nee to add the
@AutoMount(secure=true) to the AppClass, this is the annotation that the
processor looks for to process the code.
public @interface AutoMount {
String defaultRoot() default "";
String mimeExtension() default "";
boolean secure() default false;
String secureRoot() default "secure";
}
When implementing this I also realized that maybe users just wanted to
AutoMount pages even without doing the secureMount stuff, so I made it also
possible to Mount any page. To set a Mount Point you add the @MountPoint
to a page or package-info if you would like to automount all pages in a
package.
examples:
@MountPoint(path="users/ImportantPage.html")
public class ImportantUserPage extends WebPage {...
ultimately creates mountPage("users/ImportantPage.html",
com.example.ui.user.ImportantUserPage.class);
However assume AutoMount(defaultRoot="users", mimeExtension="php") is set
on the App Class and
@MountPoint
public class ImportantUserPage extends WebPage {...
creates mountPage("users/ImportantUserPage.php",
com.example.ui.user.ImportantUserPage.class); // Really php it is just to
prove a point
Finally the reason I initially created this code
AutoMount(secure=true, secureRoot="sec")
public class MyApp ....
@AuthorizedInstantiation({"USERS","ADMIN"})
public class SecureUserPage extends WebPage {
creates mountPage("sec/SecureUserPage",
com.example.ui.user.secure.SecureUserPage.class);
I already created code and wonder if I should create a pull request, or
make it a wicketstuff or just use it for myself. I personally think it
would be nice in wicket itself but I wanted to ask first before going any
further.
Thanks,
John Sarman