måndag 25 oktober 2010

Dum guide to OpenId GWT and Grails

This is a short extension to the blogg post written by Armel Nene on the OpenId GWT subject.

I found Armels post when trying to enable OpenId for my mobile webkit webapp. Doing so I notice the shortage  in the post (pointed out by wsstrange) in regard to verification of the OpenID response. I have now my self implemented such a verification and here is what I did.

Using the command pattern with GWT and Grails make it real easy to communicate with the backend server.

Like Armel visulized the flow of the openid process, I below have made a rather simple sketch over the flow of this solution.



  1. User enters id in applicaiton.
  2. GWT application sends command with id to web server.
  3. Webserver associates id with provider and saves information in http-session.
  4. Webserver returns GWT command-response to browser with provider endpoint (url).
  5. GWT app makes GET http request with supplied endpoint and openid against provider.
  6. Provider authenticates user and redirects browser back to GWT app.
  7. GWT app sends command with openid-response to webserver.
  8. Webserver verifies response with information stored in http-session (step 3).
  9. Webserver returns GWT command-response to browser.
  10. GWT app lets user in depending on success.
Code
All communication with the back-end server is done through a command pattern (implemented by "grails gwt-dispatch plugin"). Doing so I have two separate actions (commands) and associated ActionHandlers to handle the OpenId authentication process. 

The first one "CreateOpenIdAuthenticationUrlActionHandler" has the responsibility to retrive a OpenId provider endpoint from the user supplied OpenId identifier and store what it discovered for later retrival (verification). This action is preferably invoked at a login view of some sort.

At login view

...
public void onClick(ClickEvent event) {
        dispatcher.execute(new CreateOpenIdAuthenticationUrlAction(strings.googleOpenIdUrl(),
                strings.idiveOpenIdReturnUrl()), new AsyncCallback<createopenidauthenticationurlresponse>(){

            public void onFailure(Throwable caught) {
                eventbus.fireEvent(new UnhandledErrorEvent(caught));
            }

            public void onSuccess(CreateOpenIdAuthenticationUrlResponse result) {
                if(result != null){
                    Window.Location.assign(result.getAuthenticationUrl());
                } else {
                    eventbus.fireEvent(new UnhandledErrorEvent(new Error("Ooops!!! Couldn't find your provider")));
                }
            }
        });
    }
...

Action

public class VerifyOpenIdAuthenticationAction implements Action<verifyopenidauthenticationresponse> {
    private static final long serialVersionUID = 1L;

    private String returnRequestUrl;
    private String returnRequestQueryString;
    private String identifier;
    private String openIdEndpoint;

    private VerifyOpenIdAuthenticationAction(){}

    public VerifyOpenIdAuthenticationAction(String returnRequestUrl,
                                              String returnRequestQueryString) {
        this.returnRequestUrl = returnRequestUrl;
        this.returnRequestQueryString = returnRequestQueryString;
    }

    public String getReturnRequestUrl() {
        return returnRequestUrl;
    }

    public String getReturnRequestQueryString() {
        return returnRequestQueryString;
    }

    public static long getSerialVersionUID() {
        return serialVersionUID;
    }

    public String getIdentifier() {
        return identifier;
    }

    public String getOpenIdEndpoint() {
        return openIdEndpoint;
    }
}

Handler

public class CreateOpenIdAuthenticationUrlActionHandler {

  def static consumerManager;

  public CreateOpenIdAuthenticationUrlActionHandler(){
    consumerManager = new ConsumerManager();
  }

  CreateOpenIdAuthenticationUrlResponse execute(CreateOpenIdAuthenticationUrlAction action) {
    List discoveries = consumerManager.discover(action.getOpenIdIdentifier());
    DiscoveryInformation discovered = storeDiscovery(discoveries);
    AuthRequest authRequest = consumerManager.authenticate(discovered, action.getReturnUrl());
    return new CreateOpenIdAuthenticationUrlResponse(authRequest.getDestinationUrl(true),
            action.getOpenIdIdentifier());
  }

  private DiscoveryInformation storeDiscovery(List discoveries) {
    DiscoveryInformation discovered = consumerManager.associate(discoveries);
    RequestContextHolder.currentRequestAttributes().getSession().discoveryInformation = discovered
    return discovered
  }
}

Response

public class CreateOpenIdAuthenticationUrlResponse implements Response {
    private static final long serialVersionUID = 1L;

    private String authenticationUrl;
    private String openIdIdentifier;

    private CreateOpenIdAuthenticationUrlResponse(){}

    public CreateOpenIdAuthenticationUrlResponse(String authenticationUrl, String openIdIdentifier){
        this.authenticationUrl = authenticationUrl;
        this.openIdIdentifier = openIdIdentifier;
    }

    public String getAuthenticationUrl() {
        return authenticationUrl;
    }

    public String getOpenIdIdentifier() {
        return openIdIdentifier;
    }
}

The second one "VerifyOpenIdAuthenticationActionHandler" has the responsibility to verify the get request redirected from the OpenId provider against the discovery information stored in the session earlier. This action should be invoked as a response on a request containing OpenId parameters to the GWT application. This could easily be done in junction with the code presenting the user with the login view.

At login view

...
 private void checkForOpenId() {
        if(Window.Location.getParameter("openid.mode") != null){
            this.dispatcher.execute(
                    new VerifyOpenIdAuthenticationAction(Window.Location.getHref(),
                            Window.Location.getQueryString()), new AsyncCallback<verifyopenidauthenticationresponse>(){

                        public void onFailure(Throwable caught) {
                            eventbus.fireEvent(new UnhandledErrorEvent(caught));
                        }

                        public void onSuccess(VerifyOpenIdAuthenticationResponse result) {
                            if(result.isSuccess()){
                                eventbus.fireEvent(new LogedOnEvent(result.getIdentity()));
                            }
                        }
                    });
        }
    }

...

Action
public class VerifyOpenIdAuthenticationAction implements Action<verifyopenidauthenticationresponse> {
    private static final long serialVersionUID = 1L;

    private String returnRequestUrl;
    private String returnRequestQueryString;
    private String identifier;
    private String openIdEndpoint;

    private VerifyOpenIdAuthenticationAction(){}

    public VerifyOpenIdAuthenticationAction(String returnRequestUrl,
                                              String returnRequestQueryString) {
        this.returnRequestUrl = returnRequestUrl;
        this.returnRequestQueryString = returnRequestQueryString;
    }

    public String getReturnRequestUrl() {
        return returnRequestUrl;
    }

    public String getReturnRequestQueryString() {
        return returnRequestQueryString;
    }

    public static long getSerialVersionUID() {
        return serialVersionUID;
    }

    public String getIdentifier() {
        return identifier;
    }

    public String getOpenIdEndpoint() {
        return openIdEndpoint;
    }
}

Handler

public class VerifyOpenIdAuthenticationActionHandler {

  def static consumerManager;

  public VerifyOpenIdAuthenticationActionHandler(){
    consumerManager = new ConsumerManager();
  }

  VerifyOpenIdAuthenticationResponse execute(VerifyOpenIdAuthenticationAction action) {
    ParameterList openIdResponse = new ParameterList(buildListParamMap(action.getReturnRequestQueryString()));
    VerificationResult verificationResult = consumerManager.verify(action.getReturnRequestUrl(), openIdResponse, getStoredDiscoveryInformation());
    Identifier verified = verificationResult.getVerifiedId();

    if (verified != null){
      AuthSuccess authenticationSuccess = (AuthSuccess) verificationResult.getAuthResponse();
      if (authenticationSuccess.hasExtension(AxMessage.OPENID_NS_AX)){
        FetchResponse fetchResp = (FetchResponse) authenticationSuccess.getExtension(AxMessage.OPENID_NS_AX);
        List emails = fetchResp.getAttributeValues("email");
        String email = (String) emails.get(0);
      }
      return new VerifyOpenIdAuthenticationResponse(true, null ,null);
    }
    else{
      return new VerifyOpenIdAuthenticationResponse(false, null, null);
    }
  }

  private DiscoveryInformation getStoredDiscoveryInformation() {
    DiscoveryInformation discoveryInformation = RequestContextHolder.currentRequestAttributes().getSession().discoveryInformation
    return discoveryInformation
  }

  private String getUrl(VerifyOpenIdAuthenticationAction action) {
    StringBuffer receivingURL = new StringBuffer(action.getReturnRequestUrl());
    if (action.getReturnRequestQueryString() != null && action.getReturnRequestQueryString().length() > 0)
      receivingURL.append("?").append(action.getReturnRequestQueryString());
    String url = receivingURL.toString()
    return url
  }

  public  Map<string, string[]> buildListParamMap(String queryString) {
     Map<string, string[]> out = new HashMap>string, string[]<();

      if (queryString != null && queryString.length() > 1) {
        String qs = queryString.substring(1);

        for (String kvPair : qs.split("&")) {
          String[] kv = kvPair.split("=", 2);
          if (kv[0].length() == 0) {
            continue;
          }

          String[] values = out.get(kv[0]);
          if (values == null) {
            values = new String[1];
            out.put(kv[0], values);
          }
          values[0] = kv.length > 1 ? kv[1].decodeURL() : "";
        }
      }
      return out;
    }
}

Response

public class VerifyOpenIdAuthenticationResponse implements Response {
   private static final long serialVersionUID = 1L;

    private boolean success;
    private String identity;
    private String email;

    private VerifyOpenIdAuthenticationResponse(){}

    public VerifyOpenIdAuthenticationResponse(boolean success, String identity, String email) {
        this.success = success;
        this.identity = identity;
        this.email = email;
    }

    public boolean isSuccess() {
        return success;
    }

    public String getIdentity() {
        return identity;
    }

    public String getEmail() {
        return email;
    }
}

I use:

  • Grails 1.3
    • gwt-0.5.1 plugin
    • grails gwt-dispatch plugin (found here)

2 kommentarer:

  1. Hey, could you please share the full code of this great example.

    It is imcomplete.

    Thanks.

    SvaraRadera
  2. Hi there Yolki!
    Please forgive me for my late reply.

    Unfortunately I cant reveal the whole application code. However if you feel something particular is missing please let me know, it should not be impossible for me to add it.

    Best regards!

    SvaraRadera