Wednesday, May 19, 2010

GWT and Spring Security

Update! - Based on the post below, and my other post regarding Spring Security and OpenID, I have added Open-ID support to the sample application below. For those interested, here's the write-up of changes.



I've spent quite some time digging into ways of integrating GWT and Spring Security. It all started by reading the following post in the GWT Forum - Best practices/ideas for GWT with Spring Security (or equivalent), and then checking out this blog - GWT and Spring Security. To make matters worse, I started reading Security for GWT Applications and specifically about the "Cross-Site Request Forging"-attacks.

Now, what could I do about it?

Well, starting by setting up my own project (Maven-based) with all updated dependencies (GWT 2.0.3 etc) and started reading the Spring Security Reference Documentation (puh!).

Instead of See Wah Cheng's approach of implementing a custom authentication service, I decided to rely on standard namespace configuration (with some extra configuration, of course). So, after adding the normal filter declaration into web.xml I came up with this configuration for my "applicationContext-security.xml":

<http auto-config="true" entry-point-ref="http401UnauthorizedEntryPoint"
  create-session="always">
  <form-login authentication-success-handler-ref="authenticationSuccessHandler"
   authentication-failure-handler-ref="authenticationFailureHandler" />
  <logout success-handler-ref="logoutSuccessHandler" />

  <custom-filter before="CONCURRENT_SESSION_FILTER" ref="XSRFAttackFilter" />
 </http>

 <beans:bean id="XSRFAttackFilter"
  class="com.myappenginecookbook.security.XSRFAttackFilter" />

 <!--
  Use this entry point to signal to the GWT-caller that the user needs
  to log in to access the resource
 -->
 <beans:bean id="http401UnauthorizedEntryPoint"
  class="com.myappenginecookbook.security.Http401UnauthorizedEntryPoint" />

 <beans:bean id="authenticationSuccessHandler"
  class="com.myappenginecookbook.security.GWTAuthenticationSuccessHandler" />
 <beans:bean id="authenticationFailureHandler"
  class="com.myappenginecookbook.security.GWTAuthenticationFailureHandler" />
 <beans:bean id="logoutSuccessHandler"
  class="com.myappenginecookbook.security.GWTLogoutSuccessHandler" />

Don't worry about the XSRFAttackFilter class for now (we'll get to it).

I'll start by explaining how the entry-point works, and work my way deeper into the code from there...

Basically, I'm relying on standard HTTP Status Codes to signal to the caller (GWT) what Spring Security is requesting, hence the entry-point will always return HttpServletResponse.SC_UNAUTHORIZED:

/**
     * Always returns a 401 error code to the client.
     */
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException arg2) throws IOException,
            ServletException {
        if (logger.isDebugEnabled()) {
            logger.debug("Pre-authenticated entry point called. Rejecting access");
        }
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authentication required");
    }

The GWTAuthenticationSuccessHandler, GWTAuthenticationFailureHandler and GWTLogoutSuccessHandler works the same way, i.e. always returning HttpServletResponse.SC_OK, HttpServletResponse.SC_UNAUTHORIZED and HttpServletResponse.SC_OK respectively.

The response needs to be picked up in the onFailure()-method of the calling method and handled. I created an AutoErrorHandlingAsyncCallback that will show a login dialog whenever a user needs to login. It's capable of re-submitting the unauthorized command once the user is successfully logged in, but the interesting part is the login functionality itself:

@UiHandler("loginButton")
 void login(ClickEvent e) {
  RequestBuilder rb = new RequestBuilder(RequestBuilder.POST, "/j_spring_security_check");
  rb.setHeader("Content-Type", "application/x-www-form-urlencoded");
  rb.setRequestData("j_username=" + URL.encode(email.getText() + "&j_password=" + URL.encode(password.getText())));
  
  rb.setCallback(new RequestCallback() {
      public void onError(Request request, Throwable exception) {
       showError();
          Log.error(exception.getMessage());
      }
      public void onResponseReceived(Request request, Response response) {
          if (response.getStatusCode() == 200) {
           // notify all interested components
           fireEvent(new LoginEvent(true));
           
           // issue the command that triggered the dialog
           if (cmd != null) {
            cmd.execute();
           }
           
           hide();
           
           Log.debug("[success (" + response.getStatusCode() + "," + response.getStatusText() + ")]");
          } else {
           showError();
           Log.error(response.getStatusCode() + "," + response.getStatusText());
          }
      }
  });
  
        try {
   rb.send();
  } catch (RequestException re) {
   re.printStackTrace();
  }
 }

As you can see, I'm posting to the standard form login page using the RequestBuilder, and the returning response will hold the status codes from the handlers described above.

Given that we came here due to trying to access a restricted resource, the login dialog will re-issue the same command. Also note that it will fire a custom event whenever a user is successfully authenticated, so that other widgets can take appropriate actions.

Now - back to the XSRFAttackFilter! Reading about how to protect your application from XSRF-attacks I stumbled over the following text: Perhaps the simplest solution is to simply add the cookie value to your URL as a GET parameter. The important thing is to get the cookie value up to the server, somehow..

I decided to add a way for the caller to submit the cookie value with any call to the server by modifying the ServiceEntryPoint of the Async-interface:

/**
     * Utility class to get the RPC Async interface from client-side code, with the improved
     * duplication of session cookie information, see {@link http://groups.google.com/group/Google-Web-Toolkit/web/security-for-gwt-applications}.
     * 
     * To be used in conjunction with the {@link XSRFAttackFilter}. 
     */
    public static final class SecureUtil  { 
     private static String entryPoint = null;
     
        public static final SecureServiceAsync getInstance() {
         SecureServiceAsync instance = SecureServiceAsync.Util.getInstance();
         ServiceDefTarget target = (ServiceDefTarget) instance;
         
         if (entryPoint == null) {
          entryPoint = target.getServiceEntryPoint();
         }
                
            target.setServiceEntryPoint(entryPoint + "?JSESSIONID="+Cookies.getCookie("JSESSIONID"));
                
                return instance;
            }
            

        private SecureUtil()
        {
            // Utility class should not be instantiated
        }
    } 

So, instead of calling SecureServiceAsync.Util.getInstance() you'd call SecureService.SecureUtil.getInstance(). This extra piece of information is picked up by the custom XSRFAttackFilter and will be matched against the session id on the server:

@Override
 public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
     throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        
        String cookieId = request.getParameter("JSESSIONID");
        String sessionId = request.getSession().getId();
        
        if (log.isLoggable(Level.FINE)) {
         log.fine("cookieId=" + cookieId + " / sessionId=" + sessionId);
        }
        
        // if the user is authenticated, the cookie session id and the id sent as a request param must match
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        
        if (authentication != null && !sessionId.equals(cookieId)) {
         SecurityContextHolder.clearContext();         
         throw new SessionAuthenticationException("Invalid session - you have been logged out!");
        }
        
        chain.doFilter(request, response);
 }

That's it! Hopefully this will give you some more ideas of how GWT and Spring Security can be used together!

For those of you that want a running example, here's the full project source.

34 comments:

  1. Excellent Mattias, thanks for this post.

    A while ago (2008) there was some talk between GWT users and some of the GWT developers (I believe Miguel Mendez was involved) about the possible need for the session ID to be included INSIDE the RPC payload as a prevention against XSRF attacks. With Spring this could be ubiquitously checked for by some AOP or Filter. The payload version and the cookie version must match otherwise you have a forgery. If I find the discussion I'll post a link to it.

    I haven't gone through your working properly yet, I will do so shortly.

    ReplyDelete
  2. Looks quite promising, tho isen't SecureServiceAsync.java missing from the source archive?

    ReplyDelete
  3. Glad you liked it! SecureServiceAsync.java is being generated by the Maven gwt plug-in.

    ReplyDelete
  4. Okay, any chance you can throw it up there along with the rest. Trying to make this work in plain old ant, and im having a few issues.

    Another question, while looking at your code, won't SecureServiceAsync instance = SecureServiceAsync.Util.getInstance(); recurse indefinitely?
    Or is this again handled in some way by Maven?

    ReplyDelete
  5. Make yourself a favor and use Maven, as this project is set up for it. Meanwhile, here's the generated interface:

    package com.myappenginecookbook.gwt.client;

    import com.google.gwt.user.client.Cookies;
    import com.google.gwt.user.client.rpc.RemoteServiceRelativePath;
    import com.google.gwt.user.client.rpc.ServiceDefTarget;
    import com.myappenginecookbook.security.XSRFAttackFilter;
    import com.google.gwt.core.client.GWT;
    import com.google.gwt.user.client.rpc.AsyncCallback;
    import com.google.gwt.user.client.rpc.ServiceDefTarget;

    public interface SecureServiceAsync
    {

    /**
    * GWT-RPC service asynchronous (client-side) interface
    * @see com.myappenginecookbook.gwt.client.SecureService
    */
    void getSecureNumber( AsyncCallback callback );


    /**
    * GWT-RPC service asynchronous (client-side) interface
    * @see com.myappenginecookbook.gwt.client.SecureService
    */
    void getNumber( AsyncCallback callback );


    /**
    * Utility class to get the RPC Async interface from client-side code
    */
    public static final class Util
    {
    private static SecureServiceAsync instance;

    public static final SecureServiceAsync getInstance()
    {
    if ( instance == null )
    {
    instance = (SecureServiceAsync) GWT.create( SecureService.class );
    ServiceDefTarget target = (ServiceDefTarget) instance;
    target.setServiceEntryPoint( GWT.getModuleBaseURL() + "SecureService.rpc" );
    }
    return instance;
    }

    private Util()
    {
    // Utility class should not be instanciated
    }
    }
    }

    As you can see, the SecureServiceAsync.Util is a separate class holding the one instance.

    ReplyDelete
  6. Download file not found! Can you post again?

    ReplyDelete
  7. Sorry - wikiupload only seems to keep the files for a month or so. Here's a new link - http://www.wikiupload.com/7jWeFFRW. Btw, anyone knows of a better place to upload files?

    ReplyDelete
  8. Mattias, i like 4shared and DropBox.
    Im a gtug member from Brazil, keep in touch. Can you access my email?

    ReplyDelete
  9. No, your profile isn't public. I'm not sure that it would display your email anyway?

    ReplyDelete
  10. Mattias, i setup a new project using eclipse appengine plugin. I copied your files and begin to test. The first thing that i changed was:

    the value to transactions-optional. It worked!
    Now i have this problem:

    Error creating bean with name 'secureServiceImpl': Injection of autowired dependencies failed; nested exception is


    I dont know, i changed this line:
    @RemoteServiceRelativePath("/protos/SecureService.rpc")

    Can you help me?

    ReplyDelete
  11. Hi, the exception doesn't show up so it's difficult for me to do anything here. Make sure you do a "mvn clean package", or equivalent, so that you get new Async-interfaces generated. The RemoteServiceRelativePath-annotaion is used when generating these... As for other issues, I suggest you search the gwt usergroup.

    ReplyDelete
  12. Now its working. Thanks dude! Nice post

    ReplyDelete
  13. Mattias thanks for this great article. This is exactly what I need at the moment. However it seems that the link to source download expired again. Could re-upload it please ?

    I would suggest giving http://www.wowupload.com a try - it acts as a proxy and uploads the file to multiple hosting sites at the same time.

    ReplyDelete
  14. I've updated the link in the post above. In fact, I started using the new feature at Google Docs where you can upload any document and share it.

    Hope it works! :)

    ReplyDelete
  15. Got it. Thanks a lot!

    ReplyDelete
  16. Hi,

    I've a question. How can I redirect to another GWT entrypoint html after a successfull login?

    Kind regards

    ReplyDelete
  17. I guess you'd use the default successhandler and the "default-target-url" attribute to point to your entrypoint, possible together with the "always-use-default-target" attribute.

    Please look in the documentation at Setting a Default Post-Login Destination, or in a book e.g. Spring Security 3

    ReplyDelete
  18. Wow, this is so awesome, just what I was struggling to figure out how to do.

    ReplyDelete
  19. Having read through this twice, i'm left wondering: why not use GWT-RPC to send the user credentials? it seems more flexible than using the request builder, but i may have missed some subtle point?

    ReplyDelete
  20. The point is to post directly to "/j_spring_security_check" and let Spring Security handle everything from there, as opposed to having to do any Spring Security coding within the GWT-RPC call (or forwarding to "/j_spring_security_check" etc).

    I'm certain there are many different ways of integrating the two - this is just my way.

    ReplyDelete
  21. It looks like this is exactly the kind of solution I'm looking for, but the example code doesn't work for me: it shows the login window just fine, but when I click the "login" button (inside the LoginDialog) , I get the message

    Aug 4, 2010 2:53:25 PM com.allen_sauer.gwt.log.server.ServerLogImplJDK14 error
    SEVERE: [10.33.132.15] 2010-08-04 14:53:25,126 [ERROR] 400,Bad Request

    The only changes I've applied are some version number changes for the dependencies, as maven couldn't find the ones mentioned in the pom. Could you help me out? Thanks!

    ==========
    diff -u ../pristine-security//pom.xml ./pom.xml
    --- ../pristine-security//pom.xml 2010-06-13 21:55:04.000000000 +0200
    +++ ./pom.xml 2010-08-04 12:13:56.932582224 +0200
    @@ -9,8 +9,8 @@
    3.0.1.RELEASE
    2.0.3
    war
    - 1.3.4
    - 1.1.6
    + 1.3.1
    + 1.0.5



    @@ -262,7 +262,7 @@




    @@ -299,7 +299,7 @@

    com.google.appengine.orm
    datanucleus-appengine
    - 1.0.7.final
    + ${datanucleus.version}


    org.datanucleus
    @@ -482,4 +482,4 @@


    ==========

    ReplyDelete
  22. I just tested it using the downloadable source above (including your changes) with a "mvn package" and "mvn gwt:run" which works for me.

    Please try again, or try using the source from my write-up post at the top of this page (which builds on top of this source)

    /Mattias

    ReplyDelete
  23. Hi,

    I found out what the problem was: my tomcat server was replying with "Bad-Request" to the authentication attempts. Turned out that I had to substitute

    "/j_spring_security_check"

    with

    GWT.getHostPageBaseURL() + "j_spring_security_check"

    Same thing for "j_spring_security_logout".

    With these changes, it worked beautifully -- I like it a lot more than GwtIncubatorSecurity! The only thing I'm missing is the IoC from gwt-sl.

    If you turned this into a library & posted it on code.google.com, I bet it would become quite popular!

    ReplyDelete
  24. Hello, may you send to my e-mail working Eclipse project?

    ReplyDelete
  25. No, I can't because of two reasons.

    1) You're email is not exposed on your Blogger profile and
    2) if you download the maven project and run "mvn eclipse:eclipse" it will create an Eclipse project for you.

    ReplyDelete
  26. This comment has been removed by the author.

    ReplyDelete
  27. This comment has been removed by the author.

    ReplyDelete
  28. This comment has been removed by the author.

    ReplyDelete
  29. Question: why you use in LoginDialog.java cmd.execute line, fi I comment this line nothing happening with program? What this line does?

    ReplyDelete
  30. Please - you've got to understand that this is a small project I've created on my spare time. I don't have the time to give everyone free support on everything I post here.

    For this specific question, I suggest you read the inline java-comment, i.e. "issue the command that triggered the dialog" and the text in my post above, i.e. "It's capable of re-submitting the unauthorized command once the user is successfully logged in" and figure it our for yourself.

    ReplyDelete
  31. Hi Mattias,

    Thanks for your code. Do you have plan to integrate your spring security example with gwt-dispatch? This can gain command pattern benefits like parameterize clients with different requests, queue or log requests, support undoable operations etc.

    Thanks.

    ReplyDelete
  32. Sounds interesting, but no - I have no such plans...

    ReplyDelete
  33. I have some question regarding "if (authentication != null && !sessionId.equals(cookieId))" check in the XSRFAttackFilter class. Since this filter is set before CONCURRENT_SESSION_FILTER hence it's also set before SECURITY_CONTEXT_FILTER. So "Authentication authentication = SecurityContextHolder.getContext().getAuthentication()" is ALWAYS null.

    ReplyDelete
  34. If the user isn't authenticated, it will be null. Once authenticated it will be stored in the session by the SecurityContextPersistenceFilter and used for all requests after the actual authentication-request.

    See http://static.springsource.org/spring-security/site/docs/3.1.x/reference/springsecurity-single.html#tech-intro-sec-context-persistence.

    ReplyDelete