Tuesday, June 22, 2010

Using Spring Security's OpenID implementation (openid4java) on Google App Engine

The goal with this exercise is to have a running example of an OpenID login on a simple Spring application, using Google as the OpenID Provider. Note that the application will be running on Google App Engine and that Spring Roo is only used for simplicity of creating the project files. Any Spring-based application could use the same implementation.

First of all, create a simple project using Spring Roo (or any equivalent framework), including the default security setup:

project --topLevelPackage com.technowobble
persistence setup --provider DATANUCLEUS --database GOOGLE_APP_ENGINE
entity --class ~.domain.MyEntity
field string --fieldName name
controller all --package com.technowobble.controller
security setup

This setup only provides us with a form-login, which is not what we wanted. So what about OpenID?

Well, if it wasn't for Google App Engine, I would happily have added an <openid-login>-tag to applicationContext-security.xml, but things are never that easy, are they? What happens is that the OpenIDAuthenticationFilter that would have been injected relys on openid4java, which in turn uses HttpClient4 - known not to work on GAE. There is, however, a workaround that can be made which involves using the latest snapshot of openid4java (0.9.6-SNAPSHOT) instead of the stable release (0.9.5) and using the HttpFetcherFactory-functionality to inject a HttpFetcher that
actually works with GAE.

Let's add the dependencies to the pom.xml:

dependency add --groupId org.springframework.security --artifactId spring-security-openid --version  ${spring.version}
dependency add --groupId org.openid4java --artifactId openid4java-consumer --version 0.9.6-SNAPSHOT

Make sure to exlude the dependency for the stable release by adding the following to the spring-security-openid artifact in your pom.xml:

<exclusions>
 <exclusion>
 <groupId>org.openid4java</groupId>
 <artifactId>openid4java</artifactId>
 </exclusion>
</exclusions>

Note - you need to build openid4java 0.9.6-SNAPSHOT yourself and install it to your repository, unless you can't find a public repository that has it.

Next thing is to configure the OpenIDAuthenticationFilter manually in applicationContext-security.xml, in order to get to the point where the HttpFetcherFactory can be injected:

<?xml version="1.0" encoding="UTF-8"?>

<beans:beans xmlns="http://www.springframework.org/schema/security"
    xmlns:beans="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
                        http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-

3.0.xsd">

 <!-- HTTP security configurations -->
    <http auto-config="true" use-expressions="true">
     <form-login login-processing-url="/static/j_spring_security_check" login-page="/login" authentication-failure-url="/login?

login_error=t"/>
        <logout logout-url="/static/j_spring_security_logout"/>
        
        <!-- Configure these elements to secure URIs in your application -->
        <intercept-url pattern="/myentitys/**" access="isAuthenticated()" />
 <intercept-url pattern="/choices/**" access="hasRole('ROLE_ADMIN')"/>        
        <intercept-url pattern="/member/**" access="isAuthenticated()" />
        <intercept-url pattern="/resources/**" access="permitAll" />
        <intercept-url pattern="/static/**" access="permitAll" />
        <intercept-url pattern="/**" access="permitAll" />
 
 <!-- Can't add the <openid-login>-tag, as it's default filter violates app engine's whitelist -->
 <custom-filter position="OPENID_FILTER" ref="myOpenIDAuthenticationFilter" /> 
    </http>
    
    <!-- Start configuration of OpenId-filter -->    
     <beans:bean id="myOpenIDAuthenticationFilter" class="org.springframework.security.openid.OpenIDAuthenticationFilter">
  <beans:property name="authenticationManager" ref="authenticationManager"/>
    <beans:property name="consumer" ref="myOpenID4JavaConsumer"></beans:property>
 </beans:bean>
 
 <beans:bean id="myOpenID4JavaConsumer" class="org.springframework.security.openid.OpenID4JavaConsumer">
  <beans:constructor-arg index="0" ref="myConsumerManager"></beans:constructor-arg>
  <beans:constructor-arg index="1">
   <beans:list value-type="org.springframework.security.openid.OpenIDAttribute">
    <beans:bean class="org.springframework.security.openid.OpenIDAttribute">
     <beans:constructor-arg index="0" value="email"/>
     <beans:constructor-arg index="1" value="http://axschema.org/contact/email"/>
     <beans:property name="required" value="true"/>
    </beans:bean>
   </beans:list>
  </beans:constructor-arg>
 </beans:bean>
 
 <beans:bean id="myConsumerManager" class="org.openid4java.consumer.ConsumerManager">
  <beans:constructor-arg index="0" ref="myRealmVerifierFactory"></beans:constructor-arg>
  <beans:constructor-arg index="1" ref="myDiscovery"></beans:constructor-arg>
  <beans:constructor-arg index="2" ref="myHttpFetcherFactory"></beans:constructor-arg>
 </beans:bean>
 
 <beans:bean id="myRealmVerifierFactory" class="org.openid4java.server.RealmVerifierFactory">
  <beans:constructor-arg index="0" ref="myYadisResolver"></beans:constructor-arg>
 </beans:bean>
 
 <beans:bean id="myYadisResolver" class="org.openid4java.discovery.yadis.YadisResolver">
  <beans:constructor-arg index="0" ref="myHttpFetcherFactory"></beans:constructor-arg>
 </beans:bean>
 
 <beans:bean id="myHttpFetcherFactory" class="org.openid4java.util.HttpFetcherFactory">
  <beans:constructor-arg index="0" ref="myProvider"></beans:constructor-arg>
 </beans:bean>
 
 <beans:bean id="myProvider" class="com.technowobble.security.MyHttpCacheProvider"></beans:bean>
 
 <beans:bean id="myDiscovery" class="org.openid4java.discovery.Discovery">
  <beans:constructor-arg index="0" ref="myHtmlResolver"></beans:constructor-arg>
  <beans:constructor-arg index="1" ref="myYadisResolver"></beans:constructor-arg>
  <beans:constructor-arg index="2" ref="myXriResolver"></beans:constructor-arg>
 </beans:bean>
 
 <beans:bean id="myHtmlResolver" class="org.openid4java.discovery.html.HtmlResolver">
  <beans:constructor-arg index="0" ref="myHttpFetcherFactory"></beans:constructor-arg>
 </beans:bean> 
 
 <beans:bean id="myXriResolver" class="org.openid4java.discovery.xri.XriDotNetProxyResolver">
  <beans:constructor-arg index="0" ref="myHttpFetcherFactory"></beans:constructor-arg>
 </beans:bean>
 <!-- End configuration of OpenId-filter --> 

 <!-- Configure Authentication mechanism -->
    <authentication-manager alias="authenticationManager">
     <!-- SHA-256 values can be produced using 'echo -n your_desired_password | sha256sum' (using normal *nix environments) -->
     <authentication-provider>
      <password-encoder hash="sha-256"/>
         <user-service>
             <user name="admin" password="8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918" authorities="ROLE_ADMIN"/>
          <user name="user" password="04f8996da763b7a969b1028ee3007569eaf3a635486ddab211d512c85b9df8fb" 

authorities="ROLE_USER"/>
      </user-service>
     </authentication-provider>
 <authentication-provider ref="myOpenIDAuthenticationProvider" />
 </authentication-manager>
 
 <beans:bean id="myOpenIDAuthenticationProvider" class="org.springframework.security.openid.OpenIDAuthenticationProvider">
  <beans:property name="userDetailsService" ref="myOpenIdUserDetailsService"></beans:property>
 </beans:bean>
 
 <beans:bean id="myOpenIdUserDetailsService" class="com.technowobble.security.OpenIdUserDetailsServiceImpl"></beans:bean> 

</beans:beans>

There is only one class-reference that is interesting from a GAE perspective in the configuration above - MyHttpCacheProvider. This will inject an Openid4javaFetcher instance that is using a GAE-friendly URLFetchService (taken from the step2 project:

package com.technowobble.security;

import com.google.appengine.api.urlfetch.URLFetchServiceFactory;
import org.openid4java.util.HttpFetcher;
import com.google.inject.Provider;
import com.google.step2.example.consumer.appengine.Openid4javaFetcher;

public class MyHttpCacheProvider implements Provider<HttpFetcher> {

 @Override
 public HttpFetcher get() {
  return new Openid4javaFetcher(URLFetchServiceFactory.getURLFetchService());
 } 
}

Another thing to note is the added intercept-url pattern for "myentitys/**" which secures access to the domain objects created earlier. (This could of course be changed to whatever that needs to be secured).

There's also a custom UserDetailsService that handles the creation of UserDetails-objects using the supplied OpenID information:

package com.technowobble.security;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.springframework.dao.DataAccessException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.GrantedAuthorityImpl;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

/**
 * Implementation of {@link UserDetailsService} for OpenId
 */
public class OpenIdUserDetailsServiceImpl implements UserDetailsService {

 @Override
 public UserDetails loadUserByUsername(String username)
   throws UsernameNotFoundException, DataAccessException {
  return new User(username, "", true, true, true, true, getAuthorities());
 }
 
 /**
  * Utility method for creating a list of {@link GrantedAuthority} objects
  * @return
  */
 private Collection<GrantedAuthority> getAuthorities() {
  List<GrantedAuthority> authList = new ArrayList<GrantedAuthority>(2);
  authList.add(new GrantedAuthorityImpl("ROLE_USER"));
  
  return authList;
 }
}

The last thing to be done is to add an option to log in using OpenID, by adding the following form to webapp/WEB-INF/views/login.jspx:

<spring:url value='/static/j_spring_openid_security_check' var="openid_url"/>
<form name="o" action="${openid_url}" method="POST">    
 <div>
     <input type="hidden" id="openid_identifier" name='openid_identifier' value="https://www.google.com/accounts/o8/id"/>
 </div>
 <div class="submit">
     <script type="text/javascript">Spring.addDecoration(new Spring.ValidateAllDecoration({elementId:'proceed', 

event:'onclick'}));</script>
     <input id="proceed" type="submit" value="Sign in with Google"/>
 </div>         
</form>


Everything regarding security is now set up, but there are a few more things to do before we can deploy it using "mvn gae:deploy" (at least if using Spring Roo 1.1.0.M1).

First of all we need to downgrade the datanucleus-appengine artifact to version 1.0.4, as GAE throws an exception when using the default 1.0.7.final from Spring Roo. Secondly, to make the jspx-files compile, we need to add an empty dummy.jsp to webapp/WEB-INF/views (triggers the pre-compilation) and a <jsp:directive.page isELIgnored="false"/> directive to webapp/WEB-INF/layouts/default.jspx (takes care of jstl-versioning problems).

Make sure to update appengine-web.xml with your own application id/version and type "mvn gae:deploy". This command will take an unacceptable amount of time due to precompilation of all jspx-files (supposedly), but will eventually succeed, and you'll be asked for your app engine credentials. Feet up and relax!

A full source of the example project can be found here.

Please note that I haven't successfully managed to run this locally using "mvn gae:run", so that's one question I won't be able to answer. One option might be to switch to HIBERNATE/HYPERSONIC and use "mvn tomcat:run" instead, but I haven't tried it. Note that you have to comment out the constructor argument for the myHttpFetcherFactory-bean in your applicationContext-security.xml, because you're no longer in a GAE environment!

10 comments:

  1. Fantastic tutorial! With this configuration I was able to get OpenID up and running in my Google App Engine web application. Thank you so much!

    ReplyDelete
  2. Juan Carlos GonzálezNovember 5, 2010 at 12:22 PM

    Hi,

    Great tutorial. I'm really interested in testing this approach, but I can't find the snapshot you mention for openid4java. Please could you help me with this?

    ReplyDelete
  3. Hi Mattias,

    Yes, it worked.

    K.R.
    Juan Carlos

    ReplyDelete
  4. This post looks quite interesting. I'm wondering if you've had more luck recently running this locally.

    Also openid4java 0.9.6 has been released, so a SNAPSHOT version is no longer required.

    Cheers,
    Peter

    ReplyDelete
  5. Haven't touched this since my initial post, so no luck there. Please go ahead and try it and update us if you have any success!

    ReplyDelete
  6. Hmm, well it runs fine locally. But when I deploy I get

    org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'myOpenIDAuthenticationFilter' defined in class path resource [dynamictools-security-context.xml]: Initialization of bean failed; nested exception is java.lang.IllegalArgumentException: Cannot find class [javax.net.ssl.SSLContext]

    So now I'm digging to try and find what actually tries to use that class.

    ReplyDelete
  7. The HttpFetcherFactory was the culprit. It was broken (as far as AppEngine is concerned) in r658.

    I downloaded the source code and reverted that file to r631 and now I've got spring security with open id that works both on my dev box and when deployed! =)

    Couldn't have done it without your post. Thanks!

    ReplyDelete
  8. I'm trying to implement on tomcat server on AWS using Spring framework, can any one share experience of implementing OpenID on tomcat server?

    ReplyDelete
  9. Hi,
    i have implemented the same code but when
    UserDetails userDetails = userDetailsService.loadUserDetails(response); this code execute it call the loadUserByUsername(String username) from custom userDetailService but the parameter username contains the value like this
    "https://www.google.com/accounts/o8/id=SDSDSD2323SFS3FFDF" ,this is not the proper value hence user data is not retrieved from the database.
    Can u plz help me?
    Thanks in advance

    ReplyDelete