Tuesday 18 May 2010

Using OpenID in Java on GAE

After a week of reading and searching through diverse blogs and discussion groups, here are my findings:

Google App Engine (GAE) is supporting the Google Account API. My understanding is that the OAuth protocol is used. Hence in this blog I will refer to it as OAuth. Additionally, GAE is also supporting the OpenID schema, referred as the [Experimental] Federated Login (the word "[Experimental]" is being added in the release 1.3.4)

The User API is providing methods for obtaining information about the authenticated user, as well as the redirection URL's for logging in and out.

Here is the example how it is being used in the Authentication DemoApp running on http://super-easy.appspot.com

UserService userService = UserServiceFactory.getUserService();

User user = userService.getCurrentUser();

String logoutURL = userService.createLogoutURL(destinationURL, authDomainMap.get(provider));

String loginURL = userService.createLoginURL(destinationURL, authDomainMap.get(provider), federatedIdentityMap.get(provider), attributesRequest);
where the authDomainMap and the federatedIdentityMap are defined like:

Map authDomainMap = new HashMap();
authDomainMap.put(ProviderEnum.Google, "google.com");
authDomainMap.put(ProviderEnum.Yahoo, "yahoo.com");

Map federatedIdentityMap = new HashMap();
federatedIdentityMap.put(ProviderEnum.Google, "https://www.google.com/accounts/o8/id");
federatedIdentityMap.put(ProviderEnum.Yahoo, "http://open.login.yahooapis.com/openid20/www.yahoo.com/xrds");
I would hope that there is a well defined way to obtain those two values automatically, knowing the provider of an OpenID, but currently my best is to hard code them.

Additionally, the last argument is a set of attributs requested, even thought it looks more like OpenID configuration (will investigate further):
Set attributesRequest = new HashSet();
attributesRequest.add("openid.mode=checkid_immediate");
attributesRequest.add("openid.ns=http://specs.openid.net/auth/2.0");
attributesRequest.add("openid.return_to=http://super-easy.appspot.com");
Currently I am getting the following data when authenticated with my Google account:
adminFlag: True
authDomain: https://www.google.com/accounts/o8/ud
email: drasko.kokic@googlemail.com
federatedIdentity: https://www.google.com/accounts/o8/id?id=AItOawl5p-d5O-OFO4Lv2iNWsboV3jLWr1Bp1qk
nickname: drasko.kokic@googlemail.com
userId: 108555035464369674882
I am still not sure about the meaning of some fields but definitely sure that the nickname is wrong. I would really appreciate some pointers about it.

One more thing, when using the RPXNOW service, the following is the information obtained from my provider:
{"profile":{"googleUserId":"103067513055141045393","verifiedEmail":"drasko.kokic@googlemail.com","name":{"givenName":"Drasko","familyName":"Kokic","formatted":"Drasko Kokic"},"displayName":"drasko.kokic","preferredUsername":"drasko.kokic","providerName":"Google","identifier":"https:\/\/www.google.com\/accounts\/o8\/id?id=AItOawkw-h3geWs619E2MYmexqY2W5FjdL-cBbo","email":"drasko.kokic@googlemail.com"},"stat":"ok"}

12 comments:

  1. Thank you for providing some documentation on this!

    ReplyDelete
  2. First of all, thank you for your precious advice.
    I want to add a note from my work to get authentication done using Google (directly) as a provider:

    If you use your web.xml to force users to log in before they can see your page, i.e. add


    /*


    *



    Your app gets redirected to _ah/login_required?continue=....[your app url].

    If then you deploy a servlet using your code at _ah/login_required and use resp.sendRedirect(loginURL) this does not work, since you get stuck in a redirection loop because loginURL points to the same page. I guess this is why your demo requires an explicit redirect from a popup.

    To get things done you must simply exclude _ah/login_required from your security constraint, let's say by putting all your app files in war/protected/ and changing security constraint to



    /protected/*


    *



    I hope this helps if someone gets stuck in redirection loops trying to use openID on GAE.

    Thank you again
    Lorenzo

    ReplyDelete
  3. Sorry for bad formatting, tried to include some XML in my comment...

    Simply stated, first case has /* as "url-pattern" tag in security constraint, second case /protected/* for the same value, thus excluding /_ah from Google's filters.

    ReplyDelete
  4. Thanks Lorenzo. I would like to learn more what exactly you are trying to avoid. The way I understood the federated login is that it *needs* to do redirects here and there (from your app to the provider and then back to your app). Please, contact me on googletalk or googlemail. Thx ;-)

    ReplyDelete
  5. Hello,
    I do redirect in fact, since it's the way openID works, and it's unavoidable. What I did was to redirect the user directly to Google Accounts, since it's the only openID provider I wanted to support, without prompting the user to choose their provider.
    So I called generateLoginURL from a servlet deployed at the page _ah/login_required, and called response.sendRedirect to the generated URL.

    The issue was with the way generateLoginURL is implemented in App Engine.
    That method invariably returns _ah/login_required if called from any page which is listed in the "url-pattern" list if you require a user to be logged in using your web.xml.
    That is, if your web.xml states /* (all pages) require login, you can't generate the login url from anything you deploy to allow your user to log in. That will point to _ah/login_required even if called from that address, so if you redirect to it it will call the page again, resulting in a loop.

    If you give users free access to the _ah/login_required page, calls to generateLoginUrl correctly point to Google login page, so you can redirect to that address without further problems.

    I discussed this with another user facing the same behavior here:
    http://groups.google.com/group/google-appengine/browse_thread/thread/96c4248f5d289fba/ec3c9f56f8f2908c?lnk=gst&q=l.denardo#ec3c9f56f8f2908c

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

    ReplyDelete
  7. First of all thanks for this post.
    I used your example and it works great. But i am not able to get it work with other ipen id providers like msn etc.
    basically i dont have this infor for other providers.

    Map authDomainMap = new HashMap();
    authDomainMap.put(ProviderEnum.Google, "google.com");
    authDomainMap.put(ProviderEnum.Yahoo, "yahoo.com");

    Map federatedIdentityMap = new HashMap();
    federatedIdentityMap.put(ProviderEnum.Google, "https://www.google.com/accounts/o8/id");
    federatedIdentityMap.put(ProviderEnum.Yahoo, "http://open.login.yahooapis.com/openid20/www.yahoo.com/xrds");

    Do you know what need to enter for othe rproviders or where can i get such list?

    Thanks,
    Ravi.

    ReplyDelete
  8. This is the first example that I've found that combines openid and oauth in a java app using the userservice, so thank you for publishing this!

    My question is what in the code triggers the oauth approval screen to appear - the one that asks permission for my email address.

    Is it something in the mysterious, undocumented attributes parameter to userService.createLoginURL?

    Thanks, Blair

    ReplyDelete
  9. I may be using a different version of com.google.appengine.api.users.UserService, but there isn't a createLoginURL that accepts the attributeRequest Set. The example looks great on appspot, and I would like to use your implementation. Thanks for the write up! Any additional details are much appreciated!

    ReplyDelete
  10. Hi great work!!! I was struggling on this for 3 days, do you mind providing your source code?

    Thansk in advance.

    ReplyDelete
  11. @Ravi Sharma,

    Can you send me the code you have used to bind because I have to use this code and am not good at it?

    Thanks

    ReplyDelete
  12. Could you share the location of your code?

    ReplyDelete