Skip to main content

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!

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
    Replies
    1. คาสิโน the popularity of taming connected to the internet more. The feedback is very good because the

      Delete

Post a Comment

Popular posts from this blog

GWT and Spring Security

Update! - Based on the post below, and my other post regarding Spring Security and OpenID, I have added Open-ID support to the sample application below. For those interested, here's the write-up of changes. I've spent quite some time digging into ways of integrating GWT and Spring Security. It all started by reading the following post in the GWT Forum - Best practices/ideas for GWT with Spring Security (or equivalent) , and then checking out this blog - GWT and Spring Security . To make matters worse, I started reading Security for GWT Applications and specifically about the "Cross-Site Request Forging"-attacks. Now, what could I do about it? Well, starting by setting up my own project (Maven-based) with all updated dependencies (GWT 2.0.3 etc) and started reading the Spring Security Reference Documentation (puh!). Instead of See Wah Cheng's approach of implementing a custom authentication service, I decided to rely on standard namespace configuration

Loading Google Maps API asynchronously with RequireJS

With Single Page Web Applications becoming more and more popular, I decided to understand the concepts of various Javascript frameworks a little better. There are literally hundreds of them, but I decided to start with a really nice tutorial written by Alex Young. Cornerstones in this tutorial are BackboneJS, Underscore, Bootstrap and RequireJS. After been through the tutorial I decided to roll my own project based on the same setup. I wanted to use Google Maps for this, and searched for a way to load the API using RequireJS. Turned out that there are a few different approaches, but the most common seems to be to use the async-plugin created by Miller Medeiros. Jason Wyatt has another interesting solution which caught my attention. Being new to all this, I really didn't feel like start involving plug-ins from remote repositories. It might be the most natural thing to do, but one step at a time is more my melody. Jason's solution had some drawbacks mentioned in the comm

Google Apps Script and ES Modules

Currently, Google Apps Script does not support ES modules - and any usage of export/import will fail. One way of handling this is to use rollup.js to bundle your project into one single JavaScript file. The trick here is to make sure not to export any functions in your entry point code, e.g. index.ts , and to prevent any generation of export statement in the final bundle (see the custom rollup plugin in the rollup.config.js below). import { babel } from "@rollup/plugin-babel"; import { nodeResolve } from "@rollup/plugin-node-resolve"; const extensions = [".ts", ".js"]; const preventThreeShakingPlugin = () => { return { name: 'no-threeshaking', resolveId(id, importer) { if (!importer) { // let's not theeshake entry points, as we're not exporting anything in Apps Script files return {id, moduleSideEffects: "no-treeshake" } } return null; }