Friday, September 10, 2010

Using OpenID on GWT with Google App Engine


OpenID (TM) is the way forward for providing secure and intuitive access to your website.  The increasing proliferation of websites offering multiple methods for authentication is indicative this is a trend is here to stay.  Websites commonly contain access via Google Accounts, Facebook, Twitter, MyOpenId, and many other providers.  Not all of these use the OpenID standard but the term is likely to spread given its open source status.  Using OpenID with GWT on Google App Engine is fairly easy to implement, but has its perils which you should be aware of.  In this article we discuss the code and methods used to build a solution using the dyuproject framework.

Why not use the new developmental federated login provided by Google App Engine?

This article documents very clearly the process to implement OpenID using the GAE SDK.
Using Federated Authentication via OpenID in Google App Engine - Link

I tried this method and it works very well and is easy to implement.  However from using it I had two issues.

Firstly, and probably the most importantly, is the behavior in debug mode.  When logging in the user is directed to the GAE SDK development server login page, rather than the OpenID provider selected when authenticating.  This makes debugging of your authentication system very difficult, especially when you have to upload a new version of the application with increased logging, if you have a problem with authentication.

Secondly, if the OpenID authorization URL is incorrect, the application just sits on a blank page after trying to redirect to the authorization URL.  Now this maybe a functionality of the OpenID protocol rather than its implementation here, but the user experience was strange so I decided to abandon it.
This is an experimental feature at present so I am far from saying do not use it.  What I am saying is that if you prefer to have a similar authentication experience in debug as in live, then use the suggestions given further down.

User Interface

Many of the articles written here are concerned with the steps taken to produce the website SohoCRM, a small business, Google Apps focussed CRM system - http://sohocrm.appspot.com.  The screen shot below shows the initial implementation of OpenID.  I say initial because there are plans to improve its look, but at present in its draft form it looks like this:



As you can see the OpenID providers are split into two groups.  Those which require a username and those that do not.  The purpose for the username relates to the construction of the OpenID URL.
The OpenID URL is a value passed to the OpenID library duyproject which handles how the application redirects to the authentication page of the OpenID provider.  The RelyingParty is the application requiring access, an industry standard term worth remembering.  The URL hierarchy and construction of the login images is rendered from a HashMap containing all the OpenID providers the application currently supports:

//Doesn't require username first
insertProvider("Google","https://www.google.com/accounts/o8/id","/openid2/nascar/large/google.png");
insertProvider("Yahoo","https://me.yahoo.com","/openid2/nascar/large/yahoo.png");
insertProvider("YahooJapan","https://me.yahoo.co.jp","/openid2/nascar/large/yahoo-jp.png");
insertProvider("MyId","https://myid.net/","/openid2/nascar/large/myidnet.png");
insertProvider("Mixi","https://mixi.jp","/openid2/nascar/large/mixi_jp.png");
//Does require username
insertProvider("MyOpenId","http://username.myopenid.com/","/openid2/nascar/large/myopenid.png");
insertProvider("Flickr","http://www.flickr.com/","/openid2/nascar/large/flickr.png");
insertProvider("AOL","http://openid.aol.com/username","/openid2/nascar/large/aol.png");
insertProvider("Blogger","http://username.blogspot.com/","/openid2/nascar/large/blogger.png");
insertProvider("LiveJournal","http://username.livejournal.com/","/openid2/nascar/large/livejournal.png");
insertProvider("Verisign","http://username.pip.verisignlabs.com/","/openid2/nascar/large/verisign.png");
insertProvider("ClaimId","http://claimid.com/username","/openid2/nascar/large/claimid.png");
insertProvider("Wordpress","http://username.wordpress.com","/openid2/nascar/large/wordpress.png");

On rendering, if the login widget finds the keyword username in the URL component it renders the login image button to the bottom flow panel.  This is the one that requires the user to enter a username first.
The second parameter for the inserProvider function is the base URL needed to access the OpenID authentication screen.  This URL is passed to the duyproject by redirecting the screen to a URL of the type:

"/openid2/login?openid_identifier={providerUrl}"

Authentication Servlet
In GWT we have setup this URL pattern to map to our OpenID Authentication servlet.  In this application we have used GUICE to setup our mappings.  In our DispatchServletModule we have configured the mapping as below:

serve("/openid2/login").with(OpenIdServlet.class);

Which then maps to an object of this type:

public class OpenIdServlet extends HttpServlet {

The Authentication Process

This took a while because I was new to the OpenID protocol.  I had the typical programmers dilema where you know you are tackling a problem someone else has probably completed and published online but just need the quick heads up on how to implement.  I already has some code from GWTP Puzzlebazar's project so I quickly adapted that for my purposes.

The doGet method points to the doPost in this servlet so the code below is where this will be found.

Step 1: Establish Parameters and Logout
String openIdIdentifier = request.getParameter("openid_identifier");
if(openIdIdentifier!=null){
this.userDAO.get().logoutSessionUser();
}

The openid_identifier is an important parameter in the URL so you must make sure that this is passed.  The duyproject expects this parameter so however you construct your redirect to the servlet it is expecting this in the querystring.

Step 2: Create RelyingParty instance and attempt to authorization
RelyingParty relyingParty = RelyingParty.getInstance();
try {
  OpenIdUser openIdUser = relyingParty.discover(request);

Here an instance of the relying party is created which attempts to authorize the URL passed in.  It checks whether the user has access to this URL and populates the openIdUser with information related to its status.
After verifying the user the application then checks on how to proceed.  The duyproject covers this process very well and the javadoc here is an excellent resource on how to continue: link.  The example code given is shown below:

 OpenIdUser user = _relyingParty.discover(request);
   if(user==null)
   {                
       if(RelyingParty.isAuthResponse(request))
       {
           // authentication timeout                    
           response.sendRedirect(request.getRequestURI());
       }
       else
       {
           // set error msg if the openid_identifier is not resolved.
           if(request.getParameter(_relyingParty.getIdentifierParameter())!=null)
               request.setAttribute(OpenIdServletFilter.ERROR_MSG_ATTR, errorMsg);
           
           // new user
           request.getRequestDispatcher("/login.jsp").forward(request, response);
       }
       return;
   }
   
   if(user.isAuthenticated())
   {
       // user already authenticated
       request.getRequestDispatcher("/home.jsp").forward(request, response);
       return;
   }
   
   if(user.isAssociated() && RelyingParty.isAuthResponse(request))
   {
       // verify authentication
       if(_relyingParty.verifyAuth(user, request, response))
       {
           // authenticated                    
          // redirect to home to remove the query params instead of doing:
           // request.setAttribute("user", user); request.getRequestDispatcher("/home.jsp").forward(request, response);
           response.sendRedirect(request.getContextPath() + "/home/");
       }
       else
       {
           // failed verification
           request.getRequestDispatcher("/login.jsp").forward(request, response);
       }
       return;
   }
   
   StringBuffer url = request.getRequestURL();
   String trustRoot = url.substring(0, url.indexOf("/", 9));
   String realm = url.substring(0, url.lastIndexOf("/"));
   String returnTo = url.toString();            
   if(_relyingParty.associateAndAuthenticate(user, request, response, trustRoot, realm, returnTo))
   {
       // successful association
       return;
   } 

As you can see this handles each particular outcome of the interaction with the OpenID provider.  Providing you have supplied this example code with a good url, then the duyproject handles all the redirection and processing required to authenticate the user.  The only thing you need to do is implement above the code required to register that a user has authenticated in your application, i.e. logging in DB and creating a session variable.   This would occur after the line that reads:

if (openIdUser.isAssociated() && RelyingParty.isAuthResponse(request)) {

Attribute Exchange

The OpenIDUser object contains all the methods needed to extract any AttributeExchange information such as the Country, Language and any other attributes that are part of the AttributeExchange specification.

Do not use the email address as the unique ID!!!!!

I cannot stress this enough because this issue may arrive as you transfer from one authentication system to another.  For example say you have a current auth system which requires the user to login via their email address and a password.  Fine so far.  Now in order to keep your current data model you assume that the email address returned from the OpenID provider can be used as the key again, because you have incorrectly assumed that the OpenID provider checks to make sure that the user who registers with an email address has been verified.

STOP!!!

You cannot rely on the email address returned from the OpenID Provider because some providers allow you to specify what email address you want to return to the RelyingParty website.  Thereby changing this to another value of an account you may want access may give you a backdoor access to someones account.

The only uniqueID you can rely on from the OpenID provider is what is known as the ClaimedID (openIdUser.getClaimedId()).  This value is the authentication URL the user is allowed access to.  For example my AOL ClaimedId is: http://openid.aol.com/thinkjones

Summary

This took me a while to complete, the code not the article, because I was too caught up in thinking that I needed to write absolutely everything of the OpenID process.  Relying on the duyproject was a good move as it allowed me to just slot in my current authentication process at the appropriate point.  All I needed to do was create a good URL to pass to this component.  
When testing the project with the different OpenID providers I needed to test each URL individually because each OID Provider had slightly different requirements in their URL syntax.  Hopefully the fairly comprehensive list above will help you implement many solutions into your current web work.
I apologize if this article is a bit vague or badly written.  I have jet lag at the moment and wanted to get this article out as quickly as possible.  As usual leave questions in the comments.


No comments:

Post a Comment