Monday, October 17, 2011

How to use google app engine's (experimental) federated identity option for providing OpenID support

Google App Engine provides two simple ways for providing authentication services for your app.
  • Google Accounts API
    • Using Google Accounts API, you can allow users to login to your app using their google account (gmail id). 
  • Federated Login
    • Using Federated Login, you can allow users to login using any OpenID identity provider (Yahoo, Google Apps account etc.)
The focus of this post is to explain the latter using Google Apps Domain as the identity provider. If you are interested in learning about the former, you can refer to this link. Google App Engine team did a good job of documenting this approach whereas documentation for federated login approach is not comprehensive. 

Here are the requirements we are going to fulfill in this example:
  • Provide users ability to login using their Google Apps email ID
  • Provide users ability to invoke your app from their google apps universal navigation bar [SSO implementation - users should not be prompted to enter their password again because they are already logged into their google apps]. 

How-to
  • Set authentication option in Google App Engine Settings to Federated Login

  • Set up security constraints in web.xml so that when users go to any of your app urls directly, App Engine will prompt them for authentication. When App Engine encounters a url that needs to be accessed only by logged in users and the user is not logged in, it directs the user to URL /_ah/login_required. You will have to create a servlet that handle requests to this URL.  We will address this an a later step but it's important to keep in mind that /_ah/login_required URL itself should be accessible without user being logged in. Otherwise, it would result in a recursion error. 
          <security-constraint>
                   <web-resource-collection>
                            <web-resource-name>accessible without login</web-resource-name>
                            <url-pattern>/_ah/login_required</url-pattern>
                   </web-resource-collection>
          </security-constraint>

          <security-constraint>
                 <web-resource-collection>
                          <web-resource-name> accessible with login</web-resource-name>
                          <url-pattern>/*</url-pattern>
                 </web-resource-collection>
                <auth-constraint>
                            <role-name>*</role-name>
                </auth-constraint>
           </security-constraint>
  • Create servlet mapping for the servlet you'll be creating to handle requests to /_ah/login_required URL
          <servlet>
                  <servlet-name>LoginRequiredServlet</servlet-name>
                  <servlet-class>com.yourcompany.server.LoginRequiredServlet</servlet-class>
          </servlet>
         <servlet-mapping>
                  <servlet-name>LoginRequiredServlet</servlet-name>
                  <url-pattern>/_ah/login_required</url-pattern>
         </servlet-mapping>
  • Create a simple login page to which LoginRequiredServlet can direct users to for entry of login id/domain
  • In LoginRequiredServlet, use App Engine's UserService class to create login url and redirect user to that url if the user is not already logged in
          UserService userService = UserServiceFactory.getUserService();
          User user = userService.getCurrentUser();
          if (user != null) {
                  //Forward the user to you app start page
           } else {

                //forwardingUrl is the link to which you want user  to be
                // sent to after successful authentication
                String loginUrl = userService.createLoginURL(forwardingUrl,
                 null, domain, null);

                 resp.sendRedirect(loginUrl);
           }

You will find more information the following article useful:
Using Federated Authentication via OpenID in Google App Engine



Thursday, October 13, 2011

Lessons



I am a self taught web programmer. So, everything I do takes a longer that I would like because I have to do a lot research to understand a topic before I code for it. For instance, I wanted to implement openID. So, I had to first learn what OpenID was all about and then try to implement it. Along the way, I have learned a few lessons

  • If, at first, you can't find what you are looking for, try harder - Every day my admiration for search engines increases. It's amazing how you can solve majority of the issues by googling around. Sometime the initial keywords do not work but if you apply some creativity to come up with the right keywords, you can always find the information that can help in solving the problems you are facing. 

  • Main constraint for doing anything is mental tenacity. Serious works requires tremendous amount of concentration and achieving that is very exhausting but if you persist the results will appear.

  • Don't give up - There were several occasions when i felt like giving up on some issues but I continued to persist only to realize I was really close to success. Had I given up I wouldn't have attained success. This theory is exemplified by this video. 

Tuesday, October 11, 2011

Google Apps Integration and App Engine's namespaces

I have integrated our GAE app with Google Apps Marketplace. I set the namespace strategy to GOOGLE_APPS_DOMAIN so that each Google Apps domain will have it's own isolated dataset.

When I enter the app from the app's login page by entering Google App's email id and password, everything works fine. Namespace is being set to the domain name. However, when I enter the app from Google Apps universal navigation link, Namespace is being set to blank instead of the domain name. After a quick troubleshooting session, I found that NamespaceManager.getGoogleAppsNamespace(). method is not returning the domain name when the app is accessed from the universal navigation bar.

After some delving into App Engine's API source code, I came up with a workaround that I can use until I find the root cause of why the original approach didn't work.


case GOOGLE_APPS_DOMAIN: {

/*
* Original code. Commented out because getGoogleAppsNamespace
* not returning the apps domain name. Unable to find out the
* reason
*/

// NamespaceManager.set(NamespaceManager.getGoogleAppsNamespace());



if (NamespaceManager.getGoogleAppsNamespace().equals("")) {

Object userOrganizationObject;
String userOrganization;

userOrganizationObject = com.google.apphosting.api.ApiProxy
.getCurrentEnvironment()
.getAttributes()
.get("com.google.appengine.api.users.UserService.user_organization");

if (userOrganizationObject == null) {
userOrganization = "";
} else {
userOrganization = userOrganizationObject.toString();
}

NamespaceManager.set(userOrganization);

} else {

NamespaceManager.set(NamespaceManager
.getGoogleAppsNamespace());
}

break;
}