Tuesday, July 13, 2010

GWT and Open-ID using Spring Security

In this post I'll combine the GWT and Spring Security integration from http://technowobble.blogspot.com/2010/05/gwt-and-spring-security.html and the Open-ID using Spring Security from http://technowobble.blogspot.com/2010/06/using-spring-securitys-openid.html. I'm assuming you've read them before reading further... :)

I was also inspired by http://www.sociallipstick.com/?p=86 and http://code.google.com/p/dyuproject/wiki/OpenidLoginWithoutLeavingPage to get this working with a pop-up as my sample application is based on GWT - hence, I don't want to direct the user to another page and loose the application state etc.

I'm also showing how to exchange Open-ID attributes with e.g. Google. As with the previous blogposts, the sample application is runnable on Google App Engine.

With no further ado, this is basically what is needed to add Open-ID support to my previous sample application:

From my second post, add Openid4javaFetcher, MyHttpCacheProvider and OpenIdUserDetailsServiceImpl-classes, update the pom.xml with the necessary Open-ID dependencies and add the customized configuration of the OpenIDAuthenticationFilter into applicationContext-security.xml.

Now that all necessary Open-ID stuff is in place, let's start adding the functionality to the application from my first post:

First of all, let's add a "Sign in with Google" button to the LoginDialogBox:

 @UiHandler("googleLoginButton")
 void googleLogin(ClickEvent e) {
  Window.open("/j_spring_openid_security_check?openid_identifier=https://www.google.com/accounts/o8/id", "openid_popup", 

"width=450,height=500,location=1,status=1,resizable=yes");
 }

It will open up a pop-up window posting to the OpenIDAuthenticationFilter, which will take care of the rest of the Open-ID functionality. As discussed in the links mentioned above, I need a callback function that can be called once the Open-ID-request is return, i.e. from the pop-up window to the GWT application window. I therefor made the following changes:

 // general callback for any type of authentication scheme
 RequestCallback callback = new RequestCallback() {
     public void onError(Request request, Throwable exception) {
      showError();
         Log.error(exception.getMessage());
     }
     
     public void onResponseReceived(Request request, Response response) {
         if (response.getStatusCode() == Response.SC_OK) {
          // 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());
         }
     }
 };

 // make the callback function available from JavaScript
 private native void exportMethods(LoginDialog instance) /*-{
  $wnd.handleOpenIDResponse = function(statusCode, statusText) {
   return instance.@com.myappenginecookbook.gwt.client.LoginDialog::onAuthentication(ILjava/lang/String;)(statusCode, 

statusText);
  }
 }-*/;
 
 public void onAuthentication(final int statusCode, final String statusText) {
  // call callback with the needed parameters
  callback.onResponseReceived(null, new Response() {
         @Override
         public String getHeader(String header) {
          return null;
         }

         @Override
         public Header[] getHeaders() {
           return null;
         }

         @Override
         public String getHeadersAsString() {
           return null;
         }

         @Override
         public int getStatusCode() {
           return statusCode;
         }

         @Override
         public String getStatusText() {
           return statusText;
         }

         @Override
         public String getText() {
           return "";
         }
      });
 }

Note that I've broken out the anonymous RequestCallback so that it can be used with both ways of authenticating.

What's left now is to make sure the callback function will be called, which is the responsiblity of the OpenIdAuthenticationFailureHandler and OpenIdAuthenticationSuccessHandler-classes, which will be called by Spring Security at the end of the Open-ID request:

package com.myappenginecookbook.security;

import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

public class OpenIdAuthenticationSuccessHandler implements
  AuthenticationSuccessHandler {

 @Override
 public void onAuthenticationSuccess(HttpServletRequest request,
   HttpServletResponse response, Authentication authentication)
   throws IOException, ServletException {
  
  response.setContentType("text/html");
  PrintWriter out = response.getWriter();
  
  String html = 
         "<html>" +
      "<head>" +
      "</head>" +
      "<body onload=\"window.opener.handleOpenIDResponse("+HttpServletResponse.SC_OK+",'Authentication accepted');window.close

();\">" +
      "</body>" +
      "</html>";
        
        out.print(html);          
 }
}

The OpenIdAuthenticationSuccessHandler will render a simple html-fragment which utilizes JavaScript to call the exported GWT-method to signal the sucess/failure of the request. The OpenIdAuthenticationFailureHandler is basically identical, but will call the callback with the SC_UNAUTHORIZED http-code instead.

To finalize the upgraded sample app I searched the forums on how to get hold of the exchanged attributes and came up with this thread, and created a CustomOpenIDAuthenticationProvider-class to deal with it:

package com.myappenginecookbook.security;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.openid.OpenIDAttribute;
import org.springframework.security.openid.OpenIDAuthenticationProvider;
import org.springframework.security.openid.OpenIDAuthenticationToken;

public class CustomOpenIDAuthenticationProvider extends
  OpenIDAuthenticationProvider {

 @Override
 protected Authentication createSuccessfulAuthentication(
   UserDetails userDetails, OpenIDAuthenticationToken auth) {
  
  if (userDetails instanceof CustomUser) {
   CustomUser user = (CustomUser) userDetails;
   for (final OpenIDAttribute attribute : auth.getAttributes()) {
        if ("email".equals(attribute.getName())) {
          String email = attribute.getValues().get(0);
          user.setEmail(email);
        }
      }
  }

  return super.createSuccessfulAuthentication(userDetails, auth);
 }
}

Hope this helps in getting your own applications Open-ID aware! Sourcecode can be found here.