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);