Initial commit of Google Authn Adaptor code

Authentication and group resolution is working. Authentication prompts
user if they would like to permit the login.
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..9f309ff
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "lib/plexi"]
+	path = lib/plexi
+	url = https://code.google.com/p/plexi/
diff --git a/build.xml b/build.xml
new file mode 100644
index 0000000..74198a1
--- /dev/null
+++ b/build.xml
@@ -0,0 +1,287 @@
+<project name="plexi.googleauthn" default="build" basedir=".">
+  <description>Google Authentication Adaptor</description>
+  <property name="src.dir" location="src"/>
+  <property name="test.dir" location="test"/>
+  <property name="test.class" value="*Test"/>
+  <property name="build.dir" location="build"/>
+  <property name="build-src.dir" location="${build.dir}/src"/>
+  <property name="build-test.dir" location="${build.dir}/test"/>
+  <property name="build-instrument.dir" location="${build.dir}/instrument"/>
+  <property name="adaptor.clone.dir" location="lib/plexi"/>
+  <!-- Used for checking if adaptor.jar has been changed. -->
+  <property name="adaptor.jar.default"
+    value="${adaptor.clone.dir}/build/dist/adaptor/adaptor-withlib.jar"/>
+  <property name="adaptor.jar" value="${adaptor.jar.default}"/>
+  <property name="dist.dir" location="dist"/>
+  <property name="javadoc.dir" location="${build.dir}/javadoc"/>
+  <property name="resource.dir" location="resources"/>
+  <property name="lib.dir" location="lib"/>
+  <property name="junit.jar" location="${lib.dir}/plexi/lib/junit-4.8.2.jar"/>
+  <property name="adaptor.class"
+    value="com.google.enterprise.adaptor.googleauthn.GoogleAuthnAdaptor"/>
+  <property name="adaptor.args" value=""/>
+  <property name="cobertura.dir" value="${basedir}/../cobertura/"/>
+  <!-- Adaptor suffix for distribution files. Useful for placing version numbers
+       on our jars. -->
+  <property name="adaptor.suffix" value=""/>
+  <property name="mail.jar" value="${lib.dir}/mailapi.jar"/>
+
+  <path id="adaptor.build.classpath">
+    <fileset dir="${lib.dir}">
+      <include name="gdata-client-1.0.jar"/>
+      <include name="gdata-client-meta-1.0.jar"/>
+      <include name="gdata-core-1.0.jar"/>
+      <include name="gdata-appsforyourdomain-1.0.jar"/>
+      <include name="gdata-appsforyourdomain-meta-1.0.jar"/>
+      <include name="openid4java-0.9.7.jar"/>
+      <include name="commons-codec-1.3.jar"/>
+      <include name="guice-2.0.jar"/>
+      <include name="httpclient-4.2.2.jar"/>
+      <include name="httpcore-4.2.2.jar"/>
+      <include name="nekohtml-1.9.14.jar"/>
+    </fileset>
+    <pathelement location="${mail.jar}"/>
+    <pathelement location="${adaptor.jar}"/>
+  </path>
+
+  <path id="adaptor.run.classpath">
+    <path refid="adaptor.build.classpath"/>
+  </path>
+
+  <path id="cobertura.classpath">
+    <fileset dir="${cobertura.dir}" erroronmissingdir="false">
+      <include name="cobertura.jar"/>
+      <include name="lib/**/*.jar"/>
+    </fileset>
+  </path>
+
+  <target name="-check-instrument-uptodate">
+    <uptodate property="instrument.uptodate"
+      targetfile="${build-instrument.dir}/cobertura.ser">
+      <srcfiles dir="${build-src.dir}"/>
+    </uptodate>
+  </target>
+
+  <target name="build"
+    depends="-real-build,-check-instrument-uptodate,clean-instrument"
+    description="Build source"/>
+
+  <target name="-plexi-check-submodule">
+    <condition property="plexi.is-not-submodule">
+      <not>
+        <equals arg1="${adaptor.jar.default}" arg2="${adaptor.jar}"/>
+      </not>
+    </condition>
+    <condition property="tmp.plexi.valid-setup">
+      <or>
+        <isset property="plexi.is-not-submodule"/>
+        <available file="${adaptor.clone.dir}/.git"/>
+      </or>
+    </condition>
+    <fail unless="tmp.plexi.valid-setup">Invalid setup:
+No lib/plexi submodule and using default adaptor.jar property.
+
+You need to run "git submodule init; git submodule update" to initialize the
+lib/plexi submodule or add the the command line argument
+-Dadaptor.jar=path/to/adaptor-withlib.jar to point to the adaptor library.
+    </fail>
+  </target>
+
+  <target name="-plexi-test-uptodate" depends="-plexi-check-submodule">
+    <condition property="plexi.uptodate">
+      <or>
+        <isset property="plexi.is-not-submodule"/>
+        <uptodate targetfile="${adaptor.jar}">
+          <srcfiles dir="${adaptor.clone.dir}"
+            excludes=".git/** build/** dist/** **.swp"/>
+        </uptodate>
+      </or>
+    </condition>
+  </target>
+
+  <target name="-javamail-available-test">
+    <available property="javamail.available" file="${mail.jar}"/>
+  </target>
+
+  <target name="-javamail-available-check" depends="-javamail-available-test"
+    unless="javamail.available">
+    <basename property="mail.jar.base" file="${mail.jar}"/>
+    <dirname property="mail.jar.dir" file="${mail.jar}"/>
+    <fail>Missing dependency: ${mail.jar.base} is unavailable.
+
+You must download JavaMail, extract the necessary jar, and copy it to the lib
+directory.
+
+Library available at: http://www.oracle.com/technetwork/java/index-138643.html
+Copy ${mail.jar.base} to: ${mail.jar.dir}</fail>
+  </target>
+
+  <target name="-plexi-build" depends="-plexi-test-uptodate"
+    unless="plexi.uptodate">
+    <echo message="Detected Plexi changes. Re-packaging Plexi..."/>
+    <ant antfile="${adaptor.clone.dir}/build.xml" dir="${adaptor.clone.dir}"
+      target="package" inheritAll="false">
+      <property name="adaptorlib.suffix" value=""/>
+    </ant>
+  </target>
+
+  <target name="-real-build" depends="-plexi-build,-javamail-available-check">
+    <mkdir dir="${build-src.dir}"/>
+
+    <javac srcdir="${src.dir}" destdir="${build-src.dir}" debug="true"
+      includeantruntime="false" encoding="utf-8">
+      <compilerarg value="-Xlint:unchecked"/>
+      <classpath refid="adaptor.build.classpath"/>
+    </javac>
+
+    <mkdir dir="${build-test.dir}"/>
+    <!-- Compile JUnit helper -->
+    <javac srcdir="${lib.dir}" destdir="${build-test.dir}" debug="true"
+      includeantruntime="true" encoding="utf-8">
+      <compilerarg value="-Xlint:unchecked"/>
+      <classpath location="${junit.jar}"/>
+      <include name="JUnitLogFixFormatter.java"/>
+    </javac>
+
+    <!-- Compile tests, excluding example tests. -->
+    <javac srcdir="${test.dir}" destdir="${build-test.dir}" debug="true"
+      includeantruntime="false" encoding="utf-8">
+      <compilerarg value="-Xlint:unchecked"/>
+      <classpath refid="adaptor.build.classpath"/>
+      <classpath location="${build-src.dir}"/>
+      <classpath location="${junit.jar}"/>
+    </javac>
+  </target>
+
+  <target name="dist" description="Generate distribution binaries"
+    depends="clean,test,package"/>
+
+  <target name="package" description="Generate binaries"
+    depends="build,javadoc">
+    <property name="dist.staging.dir" value="${build.dir}/dist/staging"/>
+
+    <delete dir="${build.dir}/dist"/>
+    <delete dir="${dist.dir}"/>
+
+    <mkdir dir="${build.dir}/dist"/>
+    <mkdir dir="${build.dir}/dist/staging"/>
+    <mkdir dir="${dist.dir}"/>
+
+    <!-- adaptor-googleauthn.jar -->
+    <!-- Concatenate dependent JARs together into a comma-delimited list. -->
+    <pathconvert pathsep=" " refid="adaptor.run.classpath"
+      property="tmp.adaptor.classpath">
+      <mapper type="flatten"/>
+      <map from="" to="lib/"/>
+    </pathconvert>
+    <jar destfile="${dist.staging.dir}/adaptor-googleauthn${adaptor.suffix}.jar"
+      basedir="${build-src.dir}">
+      <manifest>
+        <attribute name="Main-Class" value="${adaptor.class}"/>
+        <attribute name="Class-Path" value="${tmp.adaptor.classpath}"/>
+      </manifest>
+    </jar>
+
+    <!-- lib/ -->
+    <!-- Concatenate dependent JARs together into a comma-delimited list. -->
+    <pathconvert pathsep="," refid="adaptor.run.classpath"
+      property="tmp.adaptor.fileset">
+      <!-- We remove the lib.dir from the paths to prevent trouble with comma
+           and space in lib.dir. It also makes it nicer <echo>ing
+           tmp.adaptorlib.fileset. -->
+      <map from="${lib.dir}/" to=""/>
+    </pathconvert>
+    <copy todir="${dist.staging.dir}/lib" flatten="true">
+      <fileset dir="${lib.dir}" includes="${tmp.adaptor.fileset}"/>
+    </copy>
+
+    <!-- adaptor-googleauthn-withlib.jar -->
+    <jar filesetmanifest="mergewithoutmain"
+      destfile="${dist.staging.dir}/adaptor-googleauthn${adaptor.suffix}-withlib.jar">
+      <zipfileset
+        src="${dist.staging.dir}/adaptor-googleauthn${adaptor.suffix}.jar"/>
+      <zipgroupfileset dir="${dist.staging.dir}/lib"/>
+      <manifest>
+        <attribute name="Main-Class" value="${adaptor.class}"/>
+      </manifest>
+    </jar>
+
+    <!-- adaptor-bin.zip -->
+    <move file="${dist.staging.dir}"
+      tofile="${build.dir}/dist/adaptor-googleauthn${adaptor.suffix}"/>
+    <zip destfile="${dist.dir}/adaptor-googleauthn${adaptor.suffix}-bin.zip"
+      basedir="${build.dir}/dist/adaptor-googleauthn${adaptor.suffix}"/>
+  </target>
+
+  <target name="clean" description="Remove build output">
+    <delete dir="${build.dir}"/>
+    <delete dir="${dist.dir}"/>
+  </target>
+
+  <target name="javadoc" description="Build JavaDocs">
+    <javadoc sourcepath="${src.dir}" destdir="${javadoc.dir}"
+        overview="${src.dir}/overview.html">
+      <classpath refid="adaptor.build.classpath"/>
+      <arg value="-quiet"/>
+      <arg value="-notimestamp"/>
+    </javadoc>
+  </target>
+
+  <target name="run" depends="build" description="Run adaptor">
+    <java classpath="${build-src.dir}" fork="true" classname="${adaptor.class}">
+      <classpath refid="adaptor.run.classpath"/>
+      <sysproperty key="java.util.logging.config.file"
+        value="logging.properties"/>
+      <sysproperty key="javax.net.ssl.keyStore" file="keys.jks"/>
+      <sysproperty key="javax.net.ssl.keyStoreType" value="jks"/>
+      <sysproperty key="javax.net.ssl.keyStorePassword" value="changeit"/>
+      <sysproperty key="javax.net.ssl.trustStore" file="cacerts.jks"/>
+      <sysproperty key="javax.net.ssl.trustStoreType" value="jks"/>
+      <sysproperty key="javax.net.ssl.trustStorePassword" value="changeit"/>
+      <arg line="${adaptor.args}"/>
+    </java>
+  </target>
+
+  <target name="coverage" depends="instrument,test,coverage-report"
+    description="Run instrumented tests and generate coverage report"/>
+
+  <target name="test" depends="build" description="Run JUnit tests">
+    <junit printsummary="yes" haltonfailure="yes" forkmode="once" fork="true"
+      dir="${basedir}" maxmemory="512m">
+      <sysproperty key="net.sourceforge.cobertura.datafile"
+        file="${build-instrument.dir}/cobertura.ser"/>
+      <classpath refid="adaptor.run.classpath"/>
+      <classpath refid="cobertura.classpath"/>
+      <classpath location="${junit.jar}"/>
+      <classpath location="${build-instrument.dir}"/>
+      <classpath location="${build-src.dir}"/>
+      <classpath location="${build-test.dir}"/>
+      <formatter type="plain" usefile="false"/>
+      <formatter classname="JUnitLogFixFormatter" usefile="false"/>
+      <batchtest>
+        <fileset dir="${test.dir}">
+          <include name="**/${test.class}.java"/>
+        </fileset>
+      </batchtest>
+    </junit>
+  </target>
+
+  <target name="instrument" depends="build" description="Instrument classes">
+    <taskdef classpathref="cobertura.classpath" resource="tasks.properties"/>
+    <cobertura-instrument datafile="${build-instrument.dir}/cobertura.ser"
+      todir="${build-instrument.dir}">
+      <fileset dir="${build-src.dir}"/>
+    </cobertura-instrument>
+  </target>
+
+  <target name="clean-instrument" unless="instrument.uptodate"
+    description="Delete instrumented classes">
+    <delete dir="${build-instrument.dir}"/>
+  </target>
+
+  <target name="coverage-report" description="Generates code coverage report">
+    <taskdef classpathref="cobertura.classpath" resource="tasks.properties"/>
+    <cobertura-report datafile="${build-instrument.dir}/cobertura.ser"
+      srcdir="${src.dir}" destdir="${build.dir}/coverage"/>
+  </target>
+</project>
diff --git a/lib/JUnitLogFixFormatter.java b/lib/JUnitLogFixFormatter.java
new file mode 100644
index 0000000..9920f3b
--- /dev/null
+++ b/lib/JUnitLogFixFormatter.java
@@ -0,0 +1,109 @@
+// Copyright 2011 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import junit.framework.AssertionFailedError;
+import junit.framework.Test;
+
+import org.apache.tools.ant.BuildException;
+import org.apache.tools.ant.taskdefs.optional.junit.JUnitResultFormatter;
+import org.apache.tools.ant.taskdefs.optional.junit.JUnitTest;
+
+import java.io.*;
+import java.util.*;
+import java.util.logging.*;
+
+/**
+ * Ant formatter for fixing logging output when runing multiple JUnit tests in
+ * the same JVM.
+ *
+ * <p>The default {@link ConsoleHandler} caches the value of {@code System.err}.
+ * The Ant task changes {@code System.err} after every test. Thus, only the
+ * first test gets logging logged when running JUnit tests from Ant. This
+ * formatter changes swaps out any ConsoleHandlers with new console handlers
+ * that have the correct {@code System.err} reference, and the swaps them back
+ * after the test is over.
+ */
+public class JUnitLogFixFormatter implements JUnitResultFormatter {
+  private List<ConsoleHandler> removedHandlers
+      = new ArrayList<ConsoleHandler>();
+  private List<ConsoleHandler> addedHandlers = new ArrayList<ConsoleHandler>();
+
+  @Override
+  public void startTestSuite(JUnitTest suite) throws BuildException {
+    if (removedHandlers.size() != 0) {
+      throw new IllegalStateException("removedHandlers must be empty");
+    }
+    if (addedHandlers.size() != 0) {
+      throw new IllegalStateException("addedHandlers must be empty");
+    }
+
+    Logger root = Logger.getLogger("");
+    for (Handler handler : root.getHandlers()) {
+      if (handler instanceof ConsoleHandler) {
+        removedHandlers.add((ConsoleHandler) handler);
+      }
+    }
+
+    for (ConsoleHandler oldHandler : removedHandlers) {
+      ConsoleHandler newHandler = new ConsoleHandler();
+      newHandler.setLevel(oldHandler.getLevel());
+      newHandler.setFilter(oldHandler.getFilter());
+      newHandler.setFormatter(oldHandler.getFormatter());
+      try {
+        newHandler.setEncoding(oldHandler.getEncoding());
+      } catch (UnsupportedEncodingException ex) {
+        throw new IllegalStateException(ex);
+      }
+      newHandler.setErrorManager(oldHandler.getErrorManager());
+      root.addHandler(newHandler);
+      addedHandlers.add(newHandler);
+      root.removeHandler(oldHandler);
+    }
+  }
+
+  @Override
+  public void endTestSuite(JUnitTest suite) throws BuildException {
+    Logger root = Logger.getLogger("");
+    for (ConsoleHandler handler : removedHandlers) {
+      root.addHandler(handler);
+    }
+    removedHandlers.clear();
+
+    for (ConsoleHandler handler : addedHandlers) {
+      root.removeHandler(handler);
+    }
+    addedHandlers.clear();
+  }
+
+  @Override
+  public void setOutput(OutputStream out) {}
+
+  @Override
+  public void setSystemError(String err) {}
+
+  @Override
+  public void setSystemOutput(String out) {}
+
+  @Override
+  public void addError(Test test, Throwable t) {}
+
+  @Override
+  public void addFailure(Test test, AssertionFailedError t) {}
+
+  @Override
+  public void endTest(Test test) {}
+
+  @Override
+  public void startTest(Test test) {}
+}
diff --git a/lib/commons-codec-1.3.jar b/lib/commons-codec-1.3.jar
new file mode 100644
index 0000000..957b675
--- /dev/null
+++ b/lib/commons-codec-1.3.jar
Binary files differ
diff --git a/lib/gdata-appsforyourdomain-1.0.jar b/lib/gdata-appsforyourdomain-1.0.jar
new file mode 100644
index 0000000..32a6702
--- /dev/null
+++ b/lib/gdata-appsforyourdomain-1.0.jar
Binary files differ
diff --git a/lib/gdata-appsforyourdomain-meta-1.0.jar b/lib/gdata-appsforyourdomain-meta-1.0.jar
new file mode 100644
index 0000000..081f982
--- /dev/null
+++ b/lib/gdata-appsforyourdomain-meta-1.0.jar
Binary files differ
diff --git a/lib/gdata-client-1.0.jar b/lib/gdata-client-1.0.jar
new file mode 100644
index 0000000..99d073c
--- /dev/null
+++ b/lib/gdata-client-1.0.jar
Binary files differ
diff --git a/lib/gdata-client-meta-1.0.jar b/lib/gdata-client-meta-1.0.jar
new file mode 100644
index 0000000..362bca1
--- /dev/null
+++ b/lib/gdata-client-meta-1.0.jar
Binary files differ
diff --git a/lib/gdata-core-1.0.jar b/lib/gdata-core-1.0.jar
new file mode 100644
index 0000000..8c493c6
--- /dev/null
+++ b/lib/gdata-core-1.0.jar
Binary files differ
diff --git a/lib/guice-2.0.jar b/lib/guice-2.0.jar
new file mode 100644
index 0000000..ef058df
--- /dev/null
+++ b/lib/guice-2.0.jar
Binary files differ
diff --git a/lib/httpclient-4.2.2.jar b/lib/httpclient-4.2.2.jar
new file mode 100644
index 0000000..5f768c4
--- /dev/null
+++ b/lib/httpclient-4.2.2.jar
Binary files differ
diff --git a/lib/httpcore-4.2.2.jar b/lib/httpcore-4.2.2.jar
new file mode 100644
index 0000000..a64cd2f
--- /dev/null
+++ b/lib/httpcore-4.2.2.jar
Binary files differ
diff --git a/lib/nekohtml-1.9.14.jar b/lib/nekohtml-1.9.14.jar
new file mode 100644
index 0000000..ba65542
--- /dev/null
+++ b/lib/nekohtml-1.9.14.jar
Binary files differ
diff --git a/lib/openid4java-0.9.7.jar b/lib/openid4java-0.9.7.jar
new file mode 100644
index 0000000..c9590d0
--- /dev/null
+++ b/lib/openid4java-0.9.7.jar
Binary files differ
diff --git a/lib/plexi b/lib/plexi
new file mode 160000
index 0000000..14457e1
--- /dev/null
+++ b/lib/plexi
@@ -0,0 +1 @@
+Subproject commit 14457e17b1489b0ddb1f399ec360806279ba2352
diff --git a/logging.properties b/logging.properties
new file mode 100644
index 0000000..7769538
--- /dev/null
+++ b/logging.properties
@@ -0,0 +1,11 @@
+handlers = java.util.logging.ConsoleHandler
+.level = INFO
+com.google.enterprise.adaptor.googleauthn.level = FINER
+com.google.enterprise.adaptor.level = FINER
+java.util.logging.ConsoleHandler.level = FINEST
+#com.google.gdata.client.http.HttpGDataRequest.level = FINEST
+#com.google.gdata.util.XmlParser = FINEST
+java.util.logging.ConsoleHandler.formatter = com.google.enterprise.adaptor.CustomFormatter
+# Uncomment if your terminal can't handle colors and the auto-detection
+# is incorrect.
+# com.google.enterprise.adaptor.CustomFormatter.useColor = false
diff --git a/src/com/google/enterprise/adaptor/googleauthn/GoogleAuthnAdaptor.java b/src/com/google/enterprise/adaptor/googleauthn/GoogleAuthnAdaptor.java
new file mode 100644
index 0000000..f726f71
--- /dev/null
+++ b/src/com/google/enterprise/adaptor/googleauthn/GoogleAuthnAdaptor.java
@@ -0,0 +1,340 @@
+// Copyright 2013 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.enterprise.adaptor.googleauthn;
+
+import com.sun.net.httpserver.HttpContext;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+
+import com.google.enterprise.adaptor.AbstractAdaptor;
+import com.google.enterprise.adaptor.AdaptorContext;
+import com.google.enterprise.adaptor.AuthnAdaptor;
+import com.google.enterprise.adaptor.AuthnIdentity;
+import com.google.enterprise.adaptor.Config;
+import com.google.enterprise.adaptor.DocIdPusher;
+import com.google.enterprise.adaptor.HttpExchanges;
+import com.google.enterprise.adaptor.Request;
+import com.google.enterprise.adaptor.Response;
+import com.google.enterprise.adaptor.Session;
+
+import com.google.gdata.client.authn.oauth.GoogleOAuthParameters;
+import com.google.gdata.client.authn.oauth.OAuthException;
+import com.google.gdata.client.authn.oauth.OAuthHmacSha1Signer;
+import com.google.gdata.client.authn.oauth.OAuthParameters;
+import com.google.gdata.client.authn.oauth.OAuthSigner;
+import com.google.gdata.data.Link;
+import com.google.gdata.util.AuthenticationException;
+import com.google.gdata.util.ServiceException;
+import com.google.gdata.client.appsforyourdomain.AppsGroupsService;
+import com.google.gdata.data.appsforyourdomain.generic.GenericEntry;
+import com.google.gdata.data.appsforyourdomain.generic.GenericFeed;
+
+import org.openid4java.OpenIDException;
+import org.openid4java.consumer.ConsumerManager;
+import org.openid4java.consumer.VerificationResult;
+import org.openid4java.discovery.DiscoveryException;
+import org.openid4java.discovery.DiscoveryInformation;
+import org.openid4java.message.AuthRequest;
+import org.openid4java.message.Message;
+import org.openid4java.message.MessageException;
+import org.openid4java.message.ParameterList;
+import org.openid4java.message.ax.AxMessage;
+import org.openid4java.message.ax.FetchRequest;
+import org.openid4java.message.ax.FetchResponse;
+
+import java.io.IOException;
+import java.net.*;
+import java.nio.charset.Charset;
+import java.util.*;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/** Adaptor that authenticates users with Google. */
+public class GoogleAuthnAdaptor extends AbstractAdaptor
+    implements AuthnAdaptor {
+  private static final String PROGRAM_NAME = "GoogleAuthnAdaptor/v0.1";
+  private static final String SESSION_DATA = "authndata";
+
+  private static final Logger log
+      = Logger.getLogger(GoogleAuthnAdaptor.class.getName());
+
+  private AdaptorContext context;
+  private HttpContext responseContext;
+  private List<DiscoveryInformation> discoveries;
+  private String consumerKey;
+  private String consumerSecret;
+  private String domain;
+
+  @Override
+  public void initConfig(Config config) {
+    config.addKey("google-authn.consumerKey", null);
+    config.addKey("google-authn.consumerSecret", null);
+    config.addKey("google-authn.domain", null);
+  }
+
+  @Override
+  public void init(AdaptorContext context) throws IOException {
+    this.context = context;
+    Config config = context.getConfig();
+    consumerKey = config.getValue("google-authn.consumerKey");
+    consumerSecret = context.getSensitiveValueDecoder().decodeValue(
+        config.getValue("google-authn.consumerSecret"));
+    domain = config.getValue("google-authn.domain");
+
+    log.log(Level.CONFIG, "google-authn.consumerKey: {0}", consumerKey);
+    log.log(Level.CONFIG, "google-authn.domain: {0}", domain);
+
+    try {
+      @SuppressWarnings("unchecked")
+      List<DiscoveryInformation> discoveries = new ConsumerManager()
+          .discover("https://www.google.com/accounts/o8/id");
+      this.discoveries = discoveries;
+    } catch (DiscoveryException ex) {
+      throw new IOException(ex);
+    }
+
+    if (discoveries.isEmpty()) {
+      throw new RuntimeException("Could not discover openid endpoint");
+    }
+
+    responseContext = context.createHttpContext(
+        "/google-response", new ResponseHandler());
+  }
+
+  @Override
+  public void destroy() {
+    discoveries = null;
+    responseContext.getServer().removeContext(responseContext);
+    responseContext = null;
+  }
+
+  @Override
+  public void authenticateUser(HttpExchange ex, Callback callback)
+      throws IOException {
+    ConsumerManager manager = new ConsumerManager();
+    DiscoveryInformation discovered = manager.associate(discoveries);
+    URI requestUri = HttpExchanges.getRequestUri(ex);
+    URI returnUri = requestUri.resolve(responseContext.getPath());
+    AuthRequest request;
+    try {
+      request = manager.authenticate(discovered, returnUri.toASCIIString());
+    } catch (OpenIDException e) {
+      log.log(Level.WARNING, "Authn failed: OpenIDException", e);
+      callback.userAuthenticated(ex, null);
+      return;
+    }
+    FetchRequest fetch = FetchRequest.createFetchRequest();
+    try {
+      fetch.addAttribute("email", "http://axschema.org/contact/email", true);
+      request.addExtension(fetch);
+    } catch (MessageException e) {
+      log.log(Level.WARNING, "Authn failed: MessageException", e);
+      callback.userAuthenticated(ex, null);
+      return;
+    }
+    Session session = context.getUserSession(ex, true);
+    session.setAttribute(SESSION_DATA,
+        new SessionData(manager, discovered, callback));
+    HttpExchanges.sendRedirect(ex, URI.create(request.getDestinationUrl(true)));
+  }
+
+  @Override
+  public void getDocIds(DocIdPusher pusher) {}
+
+  @Override
+  public void getDocContent(Request request, Response response)
+      throws IOException {
+    response.respondNotFound();
+  }
+
+  public static void main(String[] args) throws Exception {
+    AbstractAdaptor.main(new GoogleAuthnAdaptor(), args);
+  }
+
+  private static Map<String, String[]> convertParameterListsToArrays(
+      Map<String, List<String>> params) {
+    Map<String, String[]> newMap = new HashMap<String, String[]>();
+    String[] zeroArray = new String[0];
+    for (Map.Entry<String, List<String>> me : params.entrySet()) {
+      newMap.put(me.getKey(), me.getValue().toArray(zeroArray));
+    }
+    return newMap;
+  }
+
+  private List<String> getAllGroups(String username) throws IOException {
+    AppsGroupsService groupService;
+    try {
+      groupService = new AppsGroupsService(domain, PROGRAM_NAME);
+    } catch (AuthenticationException ex) {
+      throw new IOException("Failed to create groups service", ex);
+    }
+    GoogleOAuthParameters oauthParameters = getOAuthParameters();
+    try {
+      groupService.setOAuthCredentials(oauthParameters, getOAuthSigner());
+    } catch (OAuthException e) {
+      throw new IOException("Failed to set provisioning credentials", e);
+    }
+    try {
+      log.log(Level.FINE, "Getting group entries for {0}", username);
+      ArrayList<String> groups = new ArrayList<String>();
+      GenericFeed groupsFeed = groupService.retrieveGroups(username, true);
+      while (groupsFeed != null) {
+        for (GenericEntry entry : groupsFeed.getEntries()) {
+          groups.add(entry.getProperty("groupId"));
+        }
+        Link nextPage = groupsFeed.getNextLink();
+        if (nextPage == null) {
+          groupsFeed = null;
+        } else {
+          groupsFeed = groupService.getFeed(
+              new URL(nextPage.getHref()), GenericFeed.class);
+        }
+      }
+      log.log(Level.FINE, "group count: {0}", groups.size());
+      log.log(Level.FINER, "all groups: {0}", groups);
+      return groups;
+    } catch (ServiceException se) {
+      throw new IOException("failed to get groups", se);
+    }
+  }
+
+  private GoogleOAuthParameters getOAuthParameters() {
+    GoogleOAuthParameters oauthParameters = new GoogleOAuthParameters();
+    oauthParameters.setOAuthConsumerKey(consumerKey);
+    oauthParameters.setOAuthConsumerSecret(consumerSecret);
+    oauthParameters.setOAuthType(OAuthParameters.OAuthType.TWO_LEGGED_OAUTH);
+    return oauthParameters;
+  }
+
+  private static OAuthSigner getOAuthSigner() {
+    return new OAuthHmacSha1Signer();
+  }
+
+  private static class SessionData {
+    private final ConsumerManager manager;
+    private final DiscoveryInformation discovered;
+    private final Callback callback;
+
+    public SessionData(ConsumerManager manager, DiscoveryInformation discovered,
+        Callback callback) {
+      this.manager = manager;
+      this.discovered = discovered;
+      this.callback = callback;
+    }
+  }
+
+  private class ResponseHandler implements HttpHandler {
+    @Override
+    public void handle(HttpExchange ex) throws IOException {
+      Session session = context.getUserSession(ex, false);
+      if (session == null) {
+        log.log(Level.WARNING, "Authn failed: Could not find user's session");
+        // TODO(ejona): Translate.
+        HttpExchanges.respond(ex, HttpURLConnection.HTTP_INTERNAL_ERROR,
+            "text/plain",
+            "Could not find user's session".getBytes(Charset.forName("UTF-8")));
+        return;
+      }
+      SessionData sessionData
+          = (SessionData) session.removeAttribute(SESSION_DATA);
+      if (sessionData == null) {
+        log.log(Level.WARNING, "Authn failed: Could not find session data");
+        // TODO(ejona): Translate.
+        HttpExchanges.respond(ex, HttpURLConnection.HTTP_INTERNAL_ERROR,
+            "text/plain",
+            "Could not find session data".getBytes(Charset.forName("UTF-8")));
+        return;
+      }
+      ConsumerManager manager = sessionData.manager;
+      DiscoveryInformation discovered = sessionData.discovered;
+      Callback callback = sessionData.callback;
+      URI requestUri = HttpExchanges.getRequestUri(ex);
+      @SuppressWarnings("unchecked")
+      Map<String, List<String>> params
+          = manager.extractQueryParams(requestUri.toURL());
+      ParameterList openidResp = new ParameterList(
+          convertParameterListsToArrays(params));
+      // TODO(ejona): compute requestUri directly from the exchange
+      VerificationResult verification;
+      try {
+        verification = manager.verify(
+            requestUri.toASCIIString(), openidResp, discovered);
+      } catch (OpenIDException e) {
+        log.log(Level.WARNING, "Authn failed: OpenIDException", e);
+        callback.userAuthenticated(ex, null);
+        return;
+      }
+      if (verification.getVerifiedId() == null) {
+        if (Message.OPENID2_NS.equals(verification.getAuthResponse()
+              .getParameterValue("openid.ns"))
+            && Message.MODE_CANCEL.equals(verification.getAuthResponse()
+              .getParameterValue("openid.mode"))) {
+          log.log(Level.WARNING, "Authn failed: user canceled");
+        } else {
+          log.log(Level.WARNING, "Authn failed: verification failed");
+        }
+        callback.userAuthenticated(ex, null);
+        return;
+      }
+      Message response = verification.getAuthResponse();
+      FetchResponse ax;
+      try {
+        ax = (FetchResponse) response.getExtension(AxMessage.OPENID_NS_AX);
+      } catch (MessageException e) {
+        log.log(Level.WARNING, "Authn failed: MessageException", e);
+        callback.userAuthenticated(ex, null);
+        return;
+      }
+      if (ax == null) {
+        log.log(Level.WARNING, "Authn failed: No ax extension");
+        callback.userAuthenticated(ex, null);
+        return;
+      }
+      final String email = ax.getAttributeValue("email");
+      if (email == null) {
+        log.log(Level.WARNING, "Authn failed: No email attribute");
+        callback.userAuthenticated(ex, null);
+        return;
+      }
+      log.log(Level.FINE, "User {0} authenticated", email);
+      final Set<String> groups;
+      try {
+        groups = Collections.unmodifiableSet(
+            new HashSet<String>(getAllGroups(email)));
+      } catch (IOException e) {
+        log.log(Level.WARNING, "Authn failed: Error getting groups", e);
+        callback.userAuthenticated(ex, null);
+        return;
+      }
+      AuthnIdentity identity = new AuthnIdentity() {
+        @Override
+        public String getUsername() {
+          return email;
+        }
+
+        @Override
+        public String getPassword() {
+          return null;
+        }
+
+        @Override
+        public Set<String> getGroups() {
+          return groups;
+        }
+      };
+      callback.userAuthenticated(ex, identity);
+    }
+  }
+}
diff --git a/src/overview.html b/src/overview.html
new file mode 100644
index 0000000..083d88b
--- /dev/null
+++ b/src/overview.html
@@ -0,0 +1,111 @@
+<body>
+
+<center>
+Google Authentication Adaptor Deployment
+</center>
+
+Starting point (aka .Requirements.):<br><ul>
+<li> Have a Google domain that you can administer
+<li> Have Java JRE 1.6u27 or higher on computer that runs adaptor
+<li> Get binary (Java jar file)
+</ul>
+
+<br>
+
+4 steps to deployment:<ol>
+<li> Get your instance of the application a .key. from Google
+<li> Administer domain, adding application.s key and giving application permissions
+<li> Configure your adaptor by creating an adaptor-config.properties file
+<li> Configure your GSA -- including LDAP
+</ol>
+
+<br>
+
+Step 1 Alternative A -- Get your instance of the application a .key. from Google.<br>
+<ul>
+    <li>Create an API project in the Google APIs Console (https://code.google.com/apis/console/)
+    <li>Select the API Access tab in your API Project, click Create an OAuth 2.0 client ID, and follow the prompts to create an Installed application Application Type with required access credentials.  At the end of this creation process you should have a Client ID and Client secret .
+</ul>
+
+<br>
+
+Step 1 Alternative B -- Use your Google Apps Domain.s Client ID and Client secret.<br>
+<ul>
+   <li> Login to your domain. Goto Advanced tools > Authentication > Manage OAuth domain key
+   <li> Record your OAuth consumer key and OAuth consumer secret. These are equivalent to the Client ID and Client secret of step 1A.
+   <li> Make sure that the Enable this consumer key checkbox on Oauth consumer key is checked.
+
+   Note: Alternative B may is simpler than Alternative A, but has been flaky.
+   Note: Allow access to all APIs checkbox doesn.t work as expected.
+</ul>
+
+<br>
+
+Step 2 -- Administer domain, adding application.s key and giving application permission<br>
+<ul>
+   <li> Login to your domain.  Goto Advanced tools > Authentication > Manage third party OAuth Client access
+   <li> Add your new service account as an Authorized API client. Put your Client ID in the Client Name column and put this value for the One or More API Scopes:
+
+https://apps-apis.google.com/a/feeds/group/#readonly
+
+   <li>If successful, then each scope will have a user-readable name (like .Group Provisioning.).
+</ul>
+
+<br>
+
+Step 3 -- Configure your adaptor by creating an adaptor-config.properties file.<br>
+<ul>
+    <li>Make a text file, that is named adaptor-config.properties, in the directory that has binary.
+    <li>Here is a model for its contents:
+<pre>
+gsa.hostname=sgsa39
+server.secure=true
+google-authn.domain=amazingballoons.com
+google-authn.consumerKey=Client ID
+google-authn.consumerSecret=Client secret
+</pre>
+</ul>
+
+<br>
+
+Step 4 -- Configure your GSA<br>
+<ul>
+    <li>In the GSA's Admin Console, go to Serving > Universal Login Auth
+    Mechanisms > SAML. Add a new mechanism where:
+    <dl>
+      <dt>IDP Entity ID</dt>
+      <dd>http://google.com/enterprise/gsa/adaptor</dd>
+      <dt>Login URL<dt>
+      <dd>https://bruteforce.mtv.corp.google.com:5678/samlip</dd>
+      <dt>Artifact Resolver URL</dt>
+      <dd>https://bruteforce.mtv.corp.google.com:5678/artifact</dd>
+    </dl>
+</ul>
+
+Step 5 -- Set up Security<br>
+See the adaptor documentation for setting up an adaptor in "secure mode".
+
+<br>
+in command line: java -jar adaptor-googleauthn-xxxxxxxx.jar
+
+
+<hr>
+
+When running, to control logging, use the following logging.properties file:
+<pre>
+.level=FINER
+com.google.gdata.level=INFO
+com.google.enterprise.adaptor.level=FINER
+com.google.enterprise.adaptor.googleauthn.level=FINER
+handlers=java.util.logging.FileHandler,java.util.logging.ConsoleHandler
+
+java.util.logging.FileHandler.formatter=com.google.enterprise.adaptor.CustomFormatter
+java.util.logging.FileHandler.pattern=adaptor.%g.log
+java.util.logging.FileHandler.limit=10485760
+java.util.logging.FileHandler.count=5
+
+java.util.logging.ConsoleHandler.formatter=com.google.enterprise.adaptor.CustomFormatter
+java.util.logging.ConsoleHandler.level=INFO
+</pre>
+
+</body>
diff --git a/test/com/google/enterprise/adaptor/googleauthn/GoogleAuthnAdaptorTest.java b/test/com/google/enterprise/adaptor/googleauthn/GoogleAuthnAdaptorTest.java
new file mode 100644
index 0000000..be69df5
--- /dev/null
+++ b/test/com/google/enterprise/adaptor/googleauthn/GoogleAuthnAdaptorTest.java
@@ -0,0 +1,27 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.enterprise.adaptor.googleauthn;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+/**
+ * Test cases for {@link GoogleAuthnAdaptor}.
+ */
+public class GoogleAuthnAdaptorTest {
+  @Test
+  public void testNoop() {}
+}