Use OpenID PAPE extension to force reauthentication

Site administrators relying on OpenID can now enable the PAPE
extension, requiring users to reauthenticate with their provider
before establishing a new session with the Gerrit server.

This resolves issue 521 by allowing a site administrator to
set auth.maxOpenIdSessionAge to 0.  In this configuration the
Google Accounts provider will always prompt for a password,
which gives the user a chance to sign-out of Google's account
system and sign-in as a different user before they return to
the Gerrit installation.

Bug: issue 521
Change-Id: I656d6fd31831a71edf15319b6d94503ac93f6f36
Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 62de16a..65aa526 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -112,6 +112,30 @@
 By default, the list contains two values, `http://` and `https://`,
 allowing Gerrit to trust any OpenID it receives.
 
+[[auth.maxOpenIdSessionAge]]auth.maxOpenIdSessionAge::
++
+Time in seconds before an OpenID provider must force the user
+to authenticate themselves again before authentication to this
+Gerrit server.  Currently this is only a polite request, and users
+coming from providers that don't support the PAPE extension will
+be accepted anyway.  In the future it may be enforced, rejecting
+users coming from providers that don't honor the max session age.
++
+If set to 0, the provider will always force the user to authenticate
+(e.g. supply their password).  Values should use common unit suffixes
+to express their setting:
++
+* s, sec, second, seconds
+* m, min, minute, minutes
+* h, hr, hour, hours
+* d, day, days
+* w, week, weeks (`1 week` is treated as `7 days`)
+* mon, month, months (`1 month` is treated as `30 days`)
+* y, year, years (`1 year` is treated as `365 days`)
+
++
+Default is -1, permitting infinite time between authentications.
+
 [[auth.httpHeader]]auth.httpHeader::
 +
 HTTP header to trust the username from, or unset to select HTTP basic
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
index e7aa98f..2fdd802 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.SelfPopulatingCache;
 import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwtorm.client.KeyUtil;
@@ -50,6 +51,9 @@
 import org.openid4java.message.ax.AxMessage;
 import org.openid4java.message.ax.FetchRequest;
 import org.openid4java.message.ax.FetchResponse;
+import org.openid4java.message.pape.PapeMessage;
+import org.openid4java.message.pape.PapeRequest;
+import org.openid4java.message.pape.PapeResponse;
 import org.openid4java.message.sreg.SRegMessage;
 import org.openid4java.message.sreg.SRegRequest;
 import org.openid4java.message.sreg.SRegResponse;
@@ -62,6 +66,7 @@
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.List;
+import java.util.concurrent.TimeUnit;
 
 import javax.annotation.Nullable;
 import javax.servlet.http.Cookie;
@@ -98,6 +103,9 @@
   private final ConsumerManager manager;
   private final SelfPopulatingCache<String, List> discoveryCache;
 
+  /** Maximum age, in seconds, before forcing re-authentication of account. */
+  private final int papeMaxAuthAge;
+
   @Inject
   OpenIdServiceImpl(final Provider<WebSession> cf,
       final Provider<IdentifiedUser> iu,
@@ -135,6 +143,8 @@
     urlProvider = up;
     accountManager = am;
     manager = new ConsumerManager();
+    papeMaxAuthAge = (int) ConfigUtil.getTimeUnit(config, //
+        "auth", null, "maxOpenIdSessionAge", -1, TimeUnit.SECONDS);
 
     discoveryCache = new SelfPopulatingCache<String, List>(openidCache) {
       @Override
@@ -177,6 +187,12 @@
         fetch.addAttribute("Email", SCHEMA_EMAIL, true);
         aReq.addExtension(fetch);
       }
+
+      if (0 <= papeMaxAuthAge) {
+        final PapeRequest pape = PapeRequest.createPapeRequest();
+        pape.setMaxAuthAge(papeMaxAuthAge);
+        aReq.addExtension(pape);
+      }
     } catch (MessageException e) {
       callback.onSuccess(new DiscoveryResult(false));
       return;
@@ -277,6 +293,28 @@
     SRegResponse sregRsp = null;
     FetchResponse fetchRsp = null;
 
+    if (0 <= papeMaxAuthAge) {
+      PapeResponse ext;
+      boolean unsupported = false;
+
+      try {
+        ext = (PapeResponse) authRsp.getExtension(PapeMessage.OPENID_NS_PAPE);
+      } catch (MessageException err) {
+        // Far too many providers are unable to provide PAPE extensions
+        // right now. Instead of blocking all of them log the error and
+        // let the authentication complete anyway.
+        //
+        log.error("Invalid PAPE response " + openidIdentifier + ": " + err);
+        unsupported = true;
+        ext = null;
+      }
+      if (!unsupported && ext == null) {
+        log.error("No PAPE extension response from " + openidIdentifier);
+        cancelWithError(req, rsp, "OpenID provider does not support PAPE.");
+        return;
+      }
+    }
+
     if (authRsp.hasExtension(SRegMessage.OPENID_NS_SREG)) {
       final MessageExtension ext =
           authRsp.getExtension(SRegMessage.OPENID_NS_SREG);