When using an AJAX framework like GWT, it is necessary to authenticate each service call as well as the front end or else your services are open to the world. With AppEngine, there are several approaches we could take, such as wrapping all service servlets with a servlet filter or Spring Security or calling a checkLoggedIn() method as the first statement in each service method.
The mechanics of authenticating an AppEngine on the server side are simple enough. We just call the AppEngine UserService. The browser automatically passes the AppEngine cookie with each service request, so we don’t have to modify our service interfaces just to authenticate the user. This example shows a simple checkLoggedIn() method that could be called from each service method or wired into a servlet filter:
package com.turbomanage.server.util;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
import com.turbomanage.exception.NotLoggedInException;
public class ServiceUtil
{
public static User checkLoggedIn() throws NotLoggedInException
{
User user = getUser();
if (user == null)
{
throw new NotLoggedInException("Not logged in.");
}
return user;
}
private static User getUser()
{
UserService userService = UserServiceFactory.getUserService();
return userService.getCurrentUser();
}
}
In addition to authenticating the user, it’s a good idea to protect our services against CSRF/XSRF attacks. Google recommends (see section on XSRF and GWT) that each service request be accompanied by a token that is available to the server and the client application, but not to JavaScript code in an email link or running on other pages. The simplest scheme is simply to pass the sessionId cookie as an argument to each service method. With standard GWT-RPC, however, this is a pain because we have to add an argument to every method of every service:
public interface MyService extends RemoteService {
public boolean doSomething(String cookieValue);
public void doAnotherThing(String cookieValue, String arg);
}
This is where gwt-dispatch comes to the rescue and makes you thankful for people who turn verbs into nouns. Because gwt-dispatch uses the Command pattern, all standard GWT-RPC services are replaced with a single DispatchService that accepts an Action and Result argument. Each method in a standard GWT-RPC service is now represented by a corresponding Action and Result class. Because there is now only one real GWT-RPC service, the DispatchService, and because its execute method gets called for every request, we now have only place we need to add a session token argument. The gwt-dispatcher SecureDispatchService does exactly this:
package net.customware.gwt.dispatch.client.secure;
import net.customware.gwt.dispatch.shared.Action;
import net.customware.gwt.dispatch.shared.Result;
import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.RemoteServiceRelativePath;
@RemoteServiceRelativePath("dispatch")
public interface SecureDispatchService extends RemoteService {
Result execute( String sessionId, Action<?> action ) throws Exception;
}
By implementing this service on the server, we now have one place where we can verify the user is logged in as well as verify that the session ID passed as an argument with the request matches the session ID cookie sent automatically by the browser.
So here is my take on an AppEngineDispatchService that builds on gwt-presenter’s SecureDispatchService to achieve both user authentication and a measure of CSRF/XSRF protection. Because gwt-presenter’s SecureDispatchAsync class has a dependency on its own SessionUtil class, I had to replace it with AppEngineDispatchAsync; however, this could easily be factored out as an interface in a future release of gwt-presenter (and indeed has been already on the server side).
One last thing to mention: AppEngine uses a cookie named “ACSID”, so that’s what we’re passing with each request.
Our first class is just a stripped-down version of gwt-presenter’s SecureDispatchServiceAsync which passes the AppEngine session ID as an argument with each request.
package com.turbomanage.gwt.client.dispatch;
import net.customware.gwt.dispatch.client.DispatchAsync;
import net.customware.gwt.dispatch.shared.Action;
import net.customware.gwt.dispatch.shared.Result;
import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.Cookies;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.turbomanage.gwt.client.service.AppEngineDispatchService;
import com.turbomanage.gwt.client.service.AppEngineDispatchServiceAsync;
/**
* An AppEngine-aware implementation of {@link DispatchAsync}
*
* (c) 2009 David M. Chandler
* Licensed under the Apache License, Version 2.0
*/
public class AppEngineDispatchAsync implements DispatchAsync {
private static final AppEngineDispatchServiceAsync realService = GWT.create( AppEngineDispatchService.class );
public AppEngineDispatchAsync() {
}
/* (non-Javadoc)
* @see com.turbomanage.gwt.client.dispatch.AppEngineSecureDispatchServiceAsync#execute(A, com.google.gwt.user.client.rpc.AsyncCallback)
*/
public <A extends Action<R>, R extends Result> void execute( final A action, final AsyncCallback<R> callback ) {
// Get AppEngine session ID
String sessionId = Cookies.getCookie("ACSID");
realService.execute( sessionId, action, new AsyncCallback<Result>() {
public void onFailure( Throwable caught ) {
callback.onFailure( caught );
}
public void onSuccess( Result result ) {
callback.onSuccess( ( R ) result );
}
} );
}
}
To use this class instead of gwt-presenter’s Standard or SecureDispatchAsync, simply modify the binding in your GIN module:
@Override
protected void configure() {
...
bind( DispatchAsync.class ).to( AppEngineDispatchAsync.class ).in( Singleton.class );
...
}
Now for the service interfaces. These are just renamed from gwt-presenter’s SecureDispatchService and SecureDispatchServiceAsync, which was necessary because I could not use gwt-presenter’s SecureDispatchServiceAsync on account of the above-mentioned dependency on SessionUtil and GWT seems to require that a service and its corresponding Async class exist in the same package.
package com.turbomanage.gwt.client.service;
import net.customware.gwt.dispatch.shared.Action;
import net.customware.gwt.dispatch.shared.Result;
import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.RemoteServiceRelativePath;
@RemoteServiceRelativePath("dispatch")
public interface AppEngineDispatchService extends RemoteService {
Result execute( String sessionId, Action<?> action ) throws Exception;
}
And the corresponding Async interface, likewise just renamed from gwt-presenter’s SecureDispatchServiceAsync:
package com.turbomanage.gwt.client.service;
import net.customware.gwt.dispatch.shared.Action;
import net.customware.gwt.dispatch.shared.Result;
import com.google.gwt.user.client.rpc.AsyncCallback;
public interface AppEngineDispatchServiceAsync
{
void execute(String sessionId, Action<?> action,
AsyncCallback<Result> callback);
}
Finally, our server implementation of the DispatchService:
package com.turbomanage.gwt.server.service;
import java.util.logging.Logger;
import javax.servlet.ServletContext;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import net.customware.gwt.dispatch.client.secure.SecureDispatchService;
import net.customware.gwt.dispatch.server.Dispatch;
import net.customware.gwt.dispatch.shared.Action;
import net.customware.gwt.dispatch.shared.Result;
import net.customware.gwt.dispatch.shared.secure.InvalidSessionException;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.turbomanage.gwt.client.service.AppEngineDispatchService;
/**
* A servlet implementation of the {@link SecureDispatchService}. This verifies
* that an AppEngine user is logged in and inspects the passed AppEngine
* session ID to prevent CSRF/XSRF attacks
*
* (c) 2009 David M. Chandler
* Licensed under the Apache License, Version 2.0
*/
@Singleton
public class AppEngineDispatchServiceServlet extends RemoteServiceServlet
implements AppEngineDispatchService
{
private static final long serialVersionUID = -1456388230348266500L;
private static final Logger LOG = Logger.getLogger(AppEngineDispatchServiceServlet.class
.getName());
private final Dispatch dispatch;
private Provider<HttpServletRequest> req;
@Inject
public AppEngineDispatchServiceServlet(Dispatch dispatch, Provider<HttpServletRequest> req, Provider<ServletContext> context)
{
this.dispatch = dispatch;
this.req = req;
}
public Result execute(String clientSessionId, Action<?> action) throws Exception
{
try
{
String serverName = req.get().getServerName();
UserService userService = UserServiceFactory.getUserService();
User user = userService.getCurrentUser();
if (user != null)
{
// User is logged in, now try to match session tokens
// to prevent CSRF
String sessionId = "";
Cookie[] cookies = req.get().getCookies();
for (Cookie cookie : cookies)
{
if (cookie.getName().equals("ACSID"))
{
sessionId = cookie.getValue();
break;
}
}
// Skip check on localhost so we can test in AppEngine local dev env
if (("localhost".equals(serverName)) || (sessionId.equals(clientSessionId)))
{
return dispatch.execute(action);
}
}
throw new InvalidSessionException();
}
catch (RuntimeException e)
{
LOG.warning("Exception while executing " + action.getClass().getName()
+ ": " + e.getMessage());
throw e;
}
}
}
For each request, we’re simply calling the AppEngine UserService to verify the user is logged in and attempting to match the AppEngine cookie passed as an argument with the one passed as a cookie. In order to get access to the cookie on the server, we utilize a Guide Provider that lets us get to the ServletRequest. Likewise, we obtain the ServletContext in order to get the server name and skip the CSRF check on localhost, since AppEngine doesn’t set the ACSID cookie when running in the local development environment.
Lastly, we replace the gwt-presenter Standard or SecureDispatchServiceServlet with our AppEngine equivalent in the server’s Guice module:
@Override
public void configureServlets()
{
// NOTE: change the servlet context for your app
serve("/your_app/dispatch").with(AppEngineDispatchServiceServlet.class);
}
Thanks to gwt-presenter and the Command pattern, every AppEngine service request in our application now checks for a logged in user and provides a measure of CSRF protection.
Like this:
Like Loading...