Use auth.allowedOpenID to limit which providers can be used

Setting allowedOpenID permits an administrator to require that only
recognized OpenID providers be used to authenticate to the server.
Unlike trustedOpenID, users with providers not on the list are
simply not permitted to login.

Change-Id: I56106f2d92d100a3085b8738d556717da03ae5d7
Signed-off-by: Shawn O. Pearce <sop@google.com>
This commit is contained in:
Shawn O. Pearce
2010-05-11 16:05:27 -07:00
parent 649c6205e7
commit 533cafc64c
9 changed files with 149 additions and 41 deletions

View File

@@ -96,6 +96,18 @@ authentication is not possible.
+
By default, OpenID.
[[auth.allowedOpenID]]auth.allowedOpenID::
+
List of permitted OpenID providers. A user may only authenticate
with an OpenID that matches this list. Only used if `auth.type`
was set to OpenID (the default).
+
Patterns may be either a regular expression (start with `^` and
end with `$`) or be a simple prefix (any other string).
+
By default, the list contains two values, `http://` and `https://`,
allowing users to authenticate with any OpenID provider.
[[auth.trustedOpenID]]auth.trustedOpenID::
+
List of trusted OpenID providers. Only used if `auth.type` was

View File

@@ -17,21 +17,34 @@ package com.google.gerrit.common.auth.openid;
import java.util.Map;
public final class DiscoveryResult {
public boolean validProvider;
public static enum Status {
/** Provider was discovered and {@code providerUrl} is valid. */
VALID,
/** The identifier is not allowed to be used, by site configuration. */
NOT_ALLOWED,
/** Identifier isn't for an OpenID provider. */
NO_PROVIDER,
/** The provider was discovered, but something else failed. */
ERROR;
}
public Status status;
public String providerUrl;
public Map<String, String> providerArgs;
protected DiscoveryResult() {
}
public DiscoveryResult(final boolean valid, final String redirect,
final Map<String, String> args) {
validProvider = valid;
public DiscoveryResult(final String redirect, final Map<String, String> args) {
status = Status.VALID;
providerUrl = redirect;
providerArgs = args;
}
public DiscoveryResult(final boolean fail) {
this(false, null, null);
public DiscoveryResult(final Status s) {
status = s;
}
}

View File

@@ -14,6 +14,7 @@
package com.google.gerrit.common.data;
import com.google.gerrit.common.auth.openid.OpenIdProviderPattern;
import com.google.gerrit.reviewdb.Account;
import com.google.gerrit.reviewdb.AuthType;
import com.google.gerrit.reviewdb.Project;
@@ -24,6 +25,8 @@ import java.util.Set;
public class GerritConfig implements Cloneable {
protected String registerUrl;
protected List<OpenIdProviderPattern> allowedOpenIDs;
protected GitwebLink gitweb;
protected boolean useContributorAgreements;
protected boolean useContactInfo;
@@ -52,6 +55,14 @@ public class GerritConfig implements Cloneable {
registerUrl = u;
}
public List<OpenIdProviderPattern> getAllowedOpenIDs() {
return allowedOpenIDs;
}
public void setAllowedOpenIDs(List<OpenIdProviderPattern> l) {
allowedOpenIDs = l;
}
public AuthType getAuthType() {
return authType;
}

View File

@@ -22,7 +22,10 @@ public interface OpenIdConstants extends Constants {
String buttonLinkId();
String rememberMe();
String notSupported();
String notAllowed();
String noProvider();
String error();
String nameGoogle();
String nameYahoo();

View File

@@ -3,7 +3,10 @@ buttonRegister = Register
buttonLinkId = Link Identity
rememberMe = Remember Me
notSupported = Provider is not supported, or was incorrectly entered.
notAllowed = Provider is not allowed.
noProvider = Provider is not supported, or was incorrectly entered.
error = Unable to connect with OpenID provider.
nameGoogle = Google Account
nameYahoo = Yahoo! ID

View File

@@ -14,11 +14,13 @@
package com.google.gerrit.client.auth.openid;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.SignInDialog;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.ui.SmallHeading;
import com.google.gerrit.common.auth.SignInMode;
import com.google.gerrit.common.auth.openid.DiscoveryResult;
import com.google.gerrit.common.auth.openid.OpenIdProviderPattern;
import com.google.gerrit.common.auth.openid.OpenIdUrls;
import com.google.gwt.dom.client.FormElement;
import com.google.gwt.event.dom.client.ClickEvent;
@@ -230,6 +232,10 @@ public class OpenIdSignInDialog extends SignInDialog implements
private void link(final String identUrl, final String who,
final ImageResource icon) {
if (!isAllowedProvider(identUrl)) {
return;
}
final ClickHandler i = new ClickHandler() {
@Override
public void onClick(final ClickEvent event) {
@@ -266,6 +272,15 @@ public class OpenIdSignInDialog extends SignInDialog implements
formBody.add(line);
}
private static boolean isAllowedProvider(final String identUrl) {
for (OpenIdProviderPattern p : Gerrit.getConfig().getAllowedOpenIDs()) {
if (p.matches(identUrl)) {
return true;
}
}
return false;
}
private void enable(final boolean on) {
providerId.setEnabled(on);
login.setEnabled(on);
@@ -274,33 +289,40 @@ public class OpenIdSignInDialog extends SignInDialog implements
private void onDiscovery(final DiscoveryResult result) {
discovering = false;
if (result.validProvider) {
switch (result.status) {
case VALID:
// The provider won't support operation inside an IFRAME,
// so we replace our entire application.
//
redirectForm.setMethod(FormPanel.METHOD_POST);
redirectForm.setAction(result.providerUrl);
redirectBody.clear();
for (final Map.Entry<String, String> e : result.providerArgs.entrySet()) {
redirectBody.add(new Hidden(e.getKey(), e.getValue()));
}
// The provider won't support operation inside an IFRAME, so we
// replace our entire application. No fancy waits are needed,
// the browser won't update anything until its started to load
// the provider's page.
//
FormElement.as(redirectForm.getElement()).setTarget("_top");
redirectForm.submit();
break;
} else {
// We failed discovery. We have to use a deferred command here
// as we are being called from within an invisible IFRAME. Jump
// back to the main event loop in the parent window.
//
onDiscoveryFailure();
case NOT_ALLOWED:
showError(OpenIdUtil.C.notAllowed());
enableRetryDiscovery();
break;
case NO_PROVIDER:
showError(OpenIdUtil.C.noProvider());
enableRetryDiscovery();
break;
case ERROR:
default:
showError(OpenIdUtil.C.error());
enableRetryDiscovery();
break;
}
}
private void onDiscoveryFailure() {
showError(OpenIdUtil.C.notSupported());
private void enableRetryDiscovery() {
enable(true);
providerId.selectAll();
providerId.setFocus(true);
@@ -316,6 +338,12 @@ public class OpenIdSignInDialog extends SignInDialog implements
return;
}
if (!isAllowedProvider(openidIdentifier)) {
showError(OpenIdUtil.C.notAllowed());
enableRetryDiscovery();
return;
}
discovering = true;
enable(false);
hideError();
@@ -330,7 +358,7 @@ public class OpenIdSignInDialog extends SignInDialog implements
@Override
public void onFailure(final Throwable caught) {
super.onFailure(caught);
onDiscoveryFailure();
enableRetryDiscovery();
}
});
}

View File

@@ -80,6 +80,10 @@ class GerritConfigProvider implements Provider<GerritConfig> {
private GerritConfig create() throws MalformedURLException {
final GerritConfig config = new GerritConfig();
switch (authConfig.getAuthType()) {
case OPENID:
config.setAllowedOpenIDs(authConfig.getAllowedOpenIDs());
break;
case LDAP:
case LDAP_BIND:
config.setRegisterUrl(cfg.getString("auth", null, "registerurl"));

View File

@@ -17,6 +17,7 @@ package com.google.gerrit.httpd.auth.openid;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.auth.SignInMode;
import com.google.gerrit.common.auth.openid.DiscoveryResult;
import com.google.gerrit.common.auth.openid.OpenIdProviderPattern;
import com.google.gerrit.common.auth.openid.OpenIdService;
import com.google.gerrit.common.auth.openid.OpenIdUrls;
import com.google.gerrit.httpd.WebSession;
@@ -27,6 +28,7 @@ import com.google.gerrit.server.account.AccountException;
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.cache.Cache;
import com.google.gerrit.server.cache.SelfPopulatingCache;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.GerritServerConfig;
@@ -101,6 +103,7 @@ class OpenIdServiceImpl implements OpenIdService {
private final Provider<String> urlProvider;
private final AccountManager accountManager;
private final ConsumerManager manager;
private final List<OpenIdProviderPattern> allowedOpenIDs;
private final SelfPopulatingCache<String, List> discoveryCache;
/** Maximum age, in seconds, before forcing re-authentication of account. */
@@ -111,8 +114,8 @@ class OpenIdServiceImpl implements OpenIdService {
final Provider<IdentifiedUser> iu,
@CanonicalWebUrl @Nullable final Provider<String> up,
@Named("openid") final Cache<String, List> openidCache,
@GerritServerConfig final Config config, final AccountManager am)
throws ConsumerException, MalformedURLException {
@GerritServerConfig final Config config, final AuthConfig ac,
final AccountManager am) throws ConsumerException, MalformedURLException {
if (config.getString("http", null, "proxy") != null) {
final URL proxyUrl = new URL(config.getString("http", null, "proxy"));
@@ -143,6 +146,7 @@ class OpenIdServiceImpl implements OpenIdService {
urlProvider = up;
accountManager = am;
manager = new ConsumerManager();
allowedOpenIDs = ac.getAllowedOpenIDs();
papeMaxAuthAge = (int) ConfigUtil.getTimeUnit(config, //
"auth", null, "maxOpenIdSessionAge", -1, TimeUnit.SECONDS);
@@ -162,11 +166,16 @@ class OpenIdServiceImpl implements OpenIdService {
public void discover(final String openidIdentifier, final SignInMode mode,
final boolean remember, final String returnToken,
final AsyncCallback<DiscoveryResult> callback) {
final AsyncCallback<DiscoveryResult> cb) {
if (!isAllowedOpenID(openidIdentifier)) {
cb.onSuccess(new DiscoveryResult(DiscoveryResult.Status.NOT_ALLOWED));
return;
}
final State state;
state = init(openidIdentifier, mode, remember, returnToken);
if (state == null) {
callback.onSuccess(new DiscoveryResult(false));
cb.onSuccess(new DiscoveryResult(DiscoveryResult.Status.NO_PROVIDER));
return;
}
@@ -194,14 +203,16 @@ class OpenIdServiceImpl implements OpenIdService {
aReq.addExtension(pape);
}
} catch (MessageException e) {
callback.onSuccess(new DiscoveryResult(false));
log.error("Cannot create OpenID redirect for " + openidIdentifier, e);
cb.onSuccess(new DiscoveryResult(DiscoveryResult.Status.ERROR));
return;
} catch (ConsumerException e) {
callback.onSuccess(new DiscoveryResult(false));
log.error("Cannot create OpenID redirect for " + openidIdentifier, e);
cb.onSuccess(new DiscoveryResult(DiscoveryResult.Status.ERROR));
return;
}
callback.onSuccess(new DiscoveryResult(true, aReq.getDestinationUrl(false),
cb.onSuccess(new DiscoveryResult(aReq.getDestinationUrl(false), //
aReq.getParameterMap()));
}
@@ -245,6 +256,13 @@ class OpenIdServiceImpl implements OpenIdService {
claimedIdentifier != null ? claimedIdentifier : openidIdentifier;
final State state;
if (!isAllowedOpenID(rediscoverIdentifier)
|| !isAllowedOpenID(openidIdentifier)
|| (claimedIdentifier != null && !isAllowedOpenID(claimedIdentifier))) {
cancelWithError(req, rsp, "Provider not allowed");
return;
}
state = init(rediscoverIdentifier, mode, remember, returnToken);
if (state == null) {
// Re-discovery must have failed, we can't run a login.
@@ -525,6 +543,15 @@ class OpenIdServiceImpl implements OpenIdService {
return new State(discovered, retTo, contextUrl);
}
private boolean isAllowedOpenID(final String id) {
for (final OpenIdProviderPattern pattern : allowedOpenIDs) {
if (pattern.matches(id)) {
return true;
}
}
return false;
}
private static class State {
final DiscoveryInformation discovered;
final UrlEncoded retTo;

View File

@@ -39,7 +39,8 @@ public class AuthConfig {
private final AuthType authType;
private final String httpHeader;
private final String logoutUrl;
private final List<OpenIdProviderPattern> trusted;
private final List<OpenIdProviderPattern> trustedOpenIDs;
private final List<OpenIdProviderPattern> allowedOpenIDs;
private final SignedToken emailReg;
private final AccountGroup.Id administratorGroup;
@@ -54,7 +55,8 @@ public class AuthConfig {
authType = toType(cfg);
httpHeader = cfg.getString("auth", null, "httpheader");
logoutUrl = cfg.getString("auth", null, "logouturl");
trusted = toPatterns(cfg, "trustedopenid");
trustedOpenIDs = toPatterns(cfg, "trustedOpenID");
allowedOpenIDs = toPatterns(cfg, "allowedOpenID");
emailReg = new SignedToken(5 * 24 * 60 * 60, s.registerEmailPrivateKey);
final HashSet<AccountGroup.Id> r = new HashSet<AccountGroup.Id>(2);
@@ -125,6 +127,11 @@ public class AuthConfig {
return registeredGroups;
}
/** OpenID identities which the server permits for authentication. */
public List<OpenIdProviderPattern> getAllowedOpenIDs() {
return allowedOpenIDs;
}
public boolean isIdentityTrustable(final Collection<AccountExternalId> ids) {
switch (getAuthType()) {
case DEVELOPMENT_BECOME_ANY_ACCOUNT:
@@ -184,7 +191,7 @@ public class AuthConfig {
return true;
}
for (final OpenIdProviderPattern p : trusted) {
for (final OpenIdProviderPattern p : trustedOpenIDs) {
if (p.matches(id)) {
return true;
}