Well, I give it a start...
I begin with the definition of the exception classes for not
authenticated and not authorized. I base them on SecurityException
(can be the one of java.lang) which is on its turn based on
RunTimeException so we don't have to specify them in the declaration
of the remote service methods.
package com.google.gwt.mytest.client;
public class SecurityException extends RunTimeException implements
IsSerializable {
public SecurityException() {
}
}
public class NoLoginException extends SecurityException implements
IsSerializable {
public NoLoginException() {
}
}
public class NoAccessException extends SecurityException implements
IsSerializable {
public NoAccessException() {
}
}
I use also a remote service example 'MyService' that has two simple
'ping' methods. The remote service is protected with the
'MyServiceRole' in web.xml:
package com.google.gwt.mytest.client;
public interface MyService extends RemoteService {
String ping1(String s);
String ping2(String s);
}
public interface MyServiceAsync {
void ping1(String s, AsyncCallback callback);
void ping2(String s, AsyncCallback callback);
}
package com.google.gwt.mytest.server;
public class MyServiceImpl extends RemoteServiceServlet implements
MyService {
public String ping1(String s) {
return s;
}
public String ping2(String s) {
return s;
}
}
in web.xml:
<servlet>
<description>
</description>
<display-name>MyServiceImpl</display-name>
<servlet-name>MyServiceImpl</servlet-name>
<servlet-class>com.google.gwt.mytest.server.MyServiceImpl</servlet-
class>
</servlet>
<servlet-mapping>
<servlet-name>MyServiceImpl</servlet-name>
<url-pattern>/MyService</url-pattern>
</servlet-mapping>
<security-constraint>
<display-name>MyService Protected URIs</display-name>
<web-resource-collection>
<web-resource-name>MyService Protected URIs</web-resource-name>
<url-pattern>/MyService</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>MyServiceRole</role-name>
</auth-constraint>
</security-constraint>
<security-role>
<role-name>MyServiceRole</role-name>
</security-role>
Authorization
-------------
When a user is not authorized for a protected URL of a web application
(e.g. the user does not have the role), the HTML error code 403
(forbidden) is returned. This error code can be replaced with an
application defined 'no access' page of a web application in web.xml.
Similar, when a protected remote service is accessed by a GWT
application for which the user has not the correct authorization, the
RPC call will also receive that 'no access' page.
in web.xml:
<error-page>
<error-code>403</error-code>
<location>NoAccess.rpc</location>
</error-page>
If this 'no access' page returns NoAccessException as the result of
any RPC call, the RPC caller will receive the exception as if it was
returned by the actual RPC method.
The most easiest way to create the 'no access' RPC call is to record
the response of an RPC method that returns the NoAccessException and
to put that in the file NoAccess.rpc. This is of course not a very
portable solution because the response of the RPC method might change
in between GWT releases. I assume also that the serialization
signature of the object type (number after the type) is fixed. Anyway,
a coded alternative is presented at the end, but that one requires GWT
1.4.
NoAccess.rpc
//EX[1,["com.google.gwt.mytest.client.NoAccessException/2055489008"],
0,2]
Authentication
--------------
When a user is not authenticated for a protected URL of a web
application with form based login, the user is redirected to the
'login page'. Similar, when a protected remote service is accessed by
a GWT application with a user that is not authenticated, the RPC call
will also be redirected to that 'login page'.
in web.xml:
<login-config>
<auth-method>FORM</auth-method>
<form-login-config>
<form-login-page>NoLogin.rpc</form-login-page>
<form-error-page>LoginError.txt</form-error-page>
</form-login-config>
</login-config>
If this 'login' page returns the NoLoginException as the result of any
RPC call, the RPC caller will receive the exception as if it was
returned by the actual RPC method.
The 'no login' RPC call is the recorded response of an RPC method that
returns the NoLoginException and to put these in the file NoLogin.rpc.
NoLogin.rpc
//EX[1,["com.google.gwt.mytest.client.NoLoginException/1507318063"],
0,2]
Note: The behaviour of Tomcat is not what I expected: As long as the
user didn't do a login, an InvocationException is received in the GWT
application and the server log shows an error that the 'Client did not
send 77 bytes as expected'.
Login
-----
Calling a method of the protected MyService will return
NoLoginException because there was no login done yet. As it was
already mentioned in the previous paragraph, the web application
requires the configuration of form based login. This means that the
actual login need to be done by posting a login form (with the named
'j_username' and 'j_password' inputs) to the URL 'j_security_check' on
the web server (see servlet specs for more details).
In a traditional web application the 'login' page holds this login
form that allows a user to post its credentials back to the web server
for authentication. Because it contains now the NoLoginException RPC
response, the GWT application will need to create its own login form
(what is also the preferred thing to do anyway).
After a successful login the user is redirected to the URL of the
protected resource he was trying to access. Because that could be an
RPC call, the login will try to execute that last RPC call. However,
because the server only keeps track of the URL of the RPC call (and
not the data of the POST) the web server is producing an error.
So it is important, prior to posting the login form, to access (GET) a
'save' protected resource that will not perform any functions in the
web server. Accessing this 'save' protected resource is also necessary
in case no protected resource was accessed before (some application
servers enable the 'j_security_check' URL only after a protected page
has been accessed.).
This 'save' protected resource will be a simple text file that can be
used to identify a successful login:
LoginOk.txt
[OK]
In the next paragraph the web.xml contains also the protection of
LoginOk.txt.
When the login fails, the user receives the 'login error' page defined
for the form based login configuration. It will be also a simple text
file that can be used to identify a failed login:
LoginError.txt
[ERROR]
public class Login extends Composite {
final FormPanel formPanel = new FormPanel();
final VerticalPanel verticalPanel = new VerticalPanel();
final Label usernameLabel = new Label("UserName");
final TextBox username = new TextBox();
final Label passwordLabel = new Label("Password");
final PasswordTextBox password = new PasswordTextBox();
final Button loginButton = new Button("login");
public Login() {
formPanel.setAction("j_security_check");
formPanel.setMethod(FormPanel.METHOD_POST);
initWidget(formPanel);
formPanel.setWidget(verticalPanel);
verticalPanel.add(usernameLabel);
username.setName("j_username");
verticalPanel.add(username);
verticalPanel.add(passwordLabel);
password.setName("j_password");
verticalPanel.add(password);
verticalPanel.add(loginButton);
loginButton.addClickListener(new ClickListener() {
public void onClick(Widget sender) {
HTTPRequest.asyncGet("LoginOk.txt", new ResponseTextHandler() {
public void onCompletion(String responseText) {
Window.alert("onCompletion:"+responseText);
if(!"[OK]".equals(responseText)) {
formPanel.submit();
}
}
});
}
});
formPanel.addFormHandler(new FormHandler() {
public void onSubmitComplete(FormSubmitCompleteEvent event) {
Window.alert("onSubmitComplete:"+event.getResults());
}
public void onSubmit(FormSubmitEvent event) {
}
});
}
}
LogOut
------
For the logout, an additional remote service is created with a
logout() method that invalidates the session. It is protected with the
'LoginServiceRole' that should be mapped to all authenticated users.
An additional isLogin() RPC call is added that can be used to test if
the current user is still authenticated:
package com.google.gwt.mytest.client;
public interface LoginService extends RemoteService {
void isLogin() throws SecurityException;
void logout() throws SecurityException;
}
public interface LoginServiceAsync {
void isLogin(AsyncCallback callback);
void logout(AsyncCallback callback);
}
package com.google.gwt.mytest.server;
public class LoginServiceImpl extends RemoteServiceServlet implements
LoginService {
public void isLogin() throws SecurityException {
}
public void logout() throws SecurityException {
HttpSession session=getThreadLocalRequest().getSession(false);
if(session!=null) {
session.invalidate();
}
}
}
in web.xml:
<servlet>
<description>
</description>
<display-name>LoginServiceImpl</display-name>
<servlet-name>LoginServiceImpl</servlet-name>
<servlet-class>com.google.gwt.mytest.server.LoginServiceImpl</
servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>LoginServiceImpl</servlet-name>
<url-pattern>/LoginService</url-pattern>
</servlet-mapping>
<security-constraint>
<display-name>LoginService Protected URIs</display-name>
<web-resource-collection>
<web-resource-name>LoginService Protected URIs</web-resource-name>
<url-pattern>/LoginOk.txt</url-pattern>
<url-pattern>/LoginService</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>LoginServiceRole</role-name>
</auth-constraint>
</security-constraint>
<security-role>
<role-name>LoginServiceRole</role-name>
</security-role>
Exception throwing Remote Service
---------------------------------
As already said before, the most easiest way to create the 'no access'
RPC call is to record the response of an RPC method that returns the
NoAccessException and to put that in the file NoAccess.rpc.
NoAccess.rpc
//EX[1,["com.google.gwt.mytest.client.NoAccessException/2857546340"],
0,2]
With GWT 1.4 (build 858) (com.google.gwt.user.server.rpc.RPC) it is
however possible to override remoteServiceServlet.processCall() and
throw the exception for any incoming RPC call. Unfortunately the
encodeResponse() method to encode objects of any class is declared
private in build 858, else the remote service could have been written
like this:
class NoAccessImpl extends RemoteServiceServlet {
public String processCall(String payload) throws
SerializationException {
return RPC.encodeResponse(NoAccessException.class, new
NoAccessException(),true);
}
}
Without a GWT change request to make the encodeResponse() method
protected, the exception can still be thrown but with some more code:
public class RemoteServiceServletWithSecurity extends
RemoteServiceServlet {
protected String encodeException(Exception exception,String
methodName) throws SerializationException {
System.out.println("encodeException("+exception.getClass().getName()
+")");
try {
Method method=getClass().getMethod(methodName,null);
return RPC.encodeResponseForFailure(method, exception);
} catch (NoSuchMethodException e) {
System.out.println("encodeException():"+e);
throw new SerializationException();
}
}
// Dummy method that is declared to throw the exception
public void throwNoAccessException() throws NoAccessException {
}
protected String encodeNoAccessException() throws
SerializationException {
return encodeException(new NoAccessException(),
"throwNoAccessException");
}
}
class NoAccessImpl extends RemoteServiceServletWithSecurity {
public String processCall(String payload) throws
SerializationException {
return encodeNoAccessException();
}
}
in web.xml:
<servlet>
<description>
</description>
<display-name>NoAccessImpl</display-name>
<servlet-name>NoAccessImpl</servlet-name>
<servlet-class>com.google.gwt.mytest.server.NoAccessImpl</servlet-
class>
</servlet>
<servlet-mapping>
<servlet-name>NoAccessImpl</servlet-name>
<url-pattern>/NoAccess.rpc</url-pattern>
</servlet-mapping>
The NoLoginImpl could be implemented in a similar way throwing the
NoLoginException.
This would do the job, wasn't it that the 'no login' and 'no access'
pages are accessed (redirected) via GET and not via POST. The
RemoteServiceServlet does not support GET and it is not possible with
build 858 to subclass RemoteServiceServlet with a doGet() that is
forwarded to doPost(): The readPayloadAsUtf8() method can not be
overloaded with a method that is not throwing an exception when the
request content length is not specified, so the doGet() must be
implemented in a similar way as the doPost(). Some private constants
and methods are repeated from RemoteServiceServlet to get things
working:
public class RemoteServiceServletWithSecurityAndGet extends
RemoteServiceServletWithSecurity {
private static final String CHARSET_UTF8 = "UTF-8";
private static final String CONTENT_TYPE_TEXT_PLAIN_UTF8 = "text/
plain; charset=utf-8";
private static final String GENERIC_FAILURE_MSG = "The call failed on
the server; see server log for details";
protected void doGet(HttpServletRequest request, HttpServletResponse
response) throws ServletException, IOException {
try {
String responsePayload = processCall(null);
writeResponse(request, response, responsePayload);
return;
} catch (Throwable e) {
ServletContext servletContext = getServletContext();
servletContext.log("Exception while dispatching incoming RPC
call", e);
response.setContentType("text/plain");
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.getWriter().write(GENERIC_FAILURE_MSG);
}
}
protected void writeResponse(HttpServletRequest request,
HttpServletResponse response, String responsePayload)
throws IOException {
byte[] reply = responsePayload.getBytes(CHARSET_UTF8);
response.setContentLength(reply.length);
response.setContentType(CONTENT_TYPE_TEXT_PLAIN_UTF8);
response.setStatus(HttpServletResponse.SC_OK);
response.getOutputStream().write(reply);
}
}
Extend NoAccessImpl and NoLoginImpl from
RemoteServiceServletWithSecurityAndGet in stead of
RemoteServiceServletWithSecurity and the GET will return also the
exception.
Remote service method security
------------------------------
A simple extension to protect the methods of remote services requires
also GWT 1.4.
The servlet initialization parameters are used to get the role
required to access a method.
public class RemoteServiceServletWithSecurity extends
RemoteServiceServlet {
private boolean isLogin() {
return getThreadLocalRequest().getUserPrincipal()!=null;
}
private boolean hasAccess(Method method) {
String methodRole=getInitParameter(method.getName());
return methodRole==null ||
getThreadLocalRequest().isUserInRole(methodRole);
}
public String processCall(String payload) throws
SerializationException {
RPCRequest rpcRequest = RPC.decodeRequest(payload,
this.getClass());
if(!isLogin()) {
return encodeNoLoginException();
} else if(!hasAccess(rpcRequest.getMethod())) {
return encodeNoAccessException();
} else {
return RPC.invokeAndEncodeResponse(this, rpcRequest.getMethod(),
rpcRequest.getParameters());
}
}
}
The 'ping2' method of the remote service example 'MyService' will be
protected with the 'Ping2Role'
in web.xml:
<servlet>
<description>
</description>
<display-name>MyServiceImpl</display-name>
<servlet-name>MyServiceImpl</servlet-name>
<servlet-class>com.google.gwt.mytest.server.MyServiceImpl</servlet-
class>
<init-param>
<param-name>ping2</param-name>
<param-value>Ping2Role</param-value>
</init-param>
<security-role-ref>
<role-name>Ping2Role</role-name>
</security-role-ref>
</servlet>
Tests:
------
I did some tests with Tomcat 5.5.23/6.0.13, WebSphere 6.1.0.3 and OC4J
10.1.3.2 and I found out that there are some issues:
- Accessing a not authorized page in OC4J does not return the HTML
error code 403 (forbidden) or the 'no access' page but returns the
'login error' (on first time access) or the 'login' page. So the
'NoAccessException' is never returned to the GWT application (except
with remote service method security).
- The Container Managed Security of a Tomcat server has an unreported
bug when a POST is executed to a protected URL when there is no
authenticated user yet. The GWT application receives an
'InvocationException' (The call failed on the server). The protected
RPC call passes wrongly into the server but produces there a severe
exception:
SEVERE: Exception while dispatching incoming RPC call
javax.servlet.ServletException: Client did not send 108 bytes as
expected
at
com.google.gwt.user.server.rpc.RemoteServiceServlet.readPayloadAsUtf8(RemoteServiceServlet.java:
123)
at
com.google.gwt.user.server.rpc.RemoteServiceServlet.doPost(RemoteServiceServlet.java:
164)
...
Once a user is authenticated, the POST to an unauthorized remote
service is correctly blocked.
- Invalidating a session to do a logout for WebSphere is not enough
due to the single sign-on implementation of WebSphere. There is a
special URL (/ibm_security_logout) that need to be called.
Any comments are welcome...
Best regards,
GeertB
Window.alert ("onCompletion:"+responseText);
With GWT 1.4 (build 858) (com.google.gwt.user.server.rpc.RPC ) it is
however possible to override remoteServiceServlet.processCall() and
throw the exception for any incoming RPC call. Unfortunately the
encodeResponse() method to encode objects of any class is declared
private in build 858, else the remote service could have been written
like this:
class NoAccessImpl extends RemoteServiceServlet {
public String processCall(String payload) throws
SerializationException {
return RPC.encodeResponse(NoAccessException.class , new