Merge "Expose extension point for generic OAuth providers" into stable-2.10
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index c2221df..4ad7d21 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -145,6 +145,16 @@
The configured <<ldap.username,ldap.username>> identity is not used to obtain
account information.
+
+* OAUTH
++
+OAuth is a protocol that lets external apps request authorization to private
+details in a user's account without getting their password. This is
+preferred over Basic Authentication because tokens can be limited to specific
+types of data, and can be revoked by users at any time.
++
+Site owners have to register their application before getting started. Note
+that provider specific plugins must be used with this authentication scheme.
++
* `DEVELOPMENT_BECOME_ANY_ACCOUNT`
+
*DO NOT USE*. Only for use in a development environment.
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthServiceProvider.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthServiceProvider.java
new file mode 100644
index 0000000..8375e31
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthServiceProvider.java
@@ -0,0 +1,74 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// 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.gerrit.extensions.auth.oauth;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+import java.io.IOException;
+
+/* Contract that OAuth provider must implement */
+@ExtensionPoint
+public interface OAuthServiceProvider {
+
+ /**
+ * Retrieve the request token.
+ *
+ * @return request token
+ */
+ OAuthToken getRequestToken();
+
+ /**
+ * Returns the URL where you should redirect your users to authenticate
+ * your application.
+ *
+ * @param requestToken the request token you need to authorize
+ * @return the URL where you should redirect your users
+ */
+ String getAuthorizationUrl(OAuthToken requestToken);
+
+ /**
+ * Retrieve the access token
+ *
+ * @param requestToken request token (obtained previously)
+ * @param verifier verifier code
+ * @return access token
+ */
+ OAuthToken getAccessToken(OAuthToken requestToken, OAuthVerifier verifier);
+
+ /**
+ * After establishing of secure communication channel, this method supossed to
+ * access the protected resoure and retrieve the username.
+ *
+ * @param token
+ * @return OAuth user information
+ * @throws IOException
+ */
+ OAuthUserInfo getUserInfo(OAuthToken token) throws IOException;
+
+ /**
+ * Returns the OAuth version of the service.
+ *
+ * @return oauth version as string
+ */
+ String getVersion();
+
+ /**
+ * Returns the name of this service. This name is resented the user to choose
+ * between multiple service providers
+ *
+ * @return name of the service
+ */
+ String getName();
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthToken.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthToken.java
new file mode 100644
index 0000000..901951e
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthToken.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// 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.gerrit.extensions.auth.oauth;
+
+/* OAuth token */
+public class OAuthToken {
+
+ private final String token;
+ private final String secret;
+ private final String raw;
+
+ public OAuthToken(String token, String secret, String raw) {
+ this.token = token;
+ this.secret = secret;
+ this.raw = raw;
+ }
+
+ public String getToken() {
+ return token;
+ }
+
+ public String getSecret() {
+ return secret;
+ }
+
+ public String getRaw() {
+ return raw;
+ }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthUserInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthUserInfo.java
new file mode 100644
index 0000000..23a7bec
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthUserInfo.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// 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.gerrit.extensions.auth.oauth;
+
+public class OAuthUserInfo {
+
+ private final String externalId;
+ private final String userName;
+ private final String emailAddress;
+ private final String displayName;
+
+ public OAuthUserInfo(String externalId,
+ String userName,
+ String emailAddress,
+ String displayName) {
+ this.externalId = externalId;
+ this.userName = userName;
+ this.emailAddress = emailAddress;
+ this.displayName = displayName;
+ }
+
+ public String getExternalId() {
+ return externalId;
+ }
+
+ public String getUserName() {
+ return userName;
+ }
+
+ public String getEmailAddress() {
+ return emailAddress;
+ }
+
+ public String getDisplayName() {
+ return displayName;
+ }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthVerifier.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthVerifier.java
new file mode 100644
index 0000000..33c45c5
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthVerifier.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// 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.gerrit.extensions.auth.oauth;
+
+/* OAuth verifier */
+public class OAuthVerifier {
+
+ private final String value;
+
+ public OAuthVerifier(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
index d81827c..27a4b53 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
@@ -718,6 +718,15 @@
});
break;
+ case OAUTH:
+ menuRight.addItem(C.menuSignIn(), new Command() {
+ @Override
+ public void execute() {
+ doSignIn(History.getToken());
+ }
+ });
+ break;
+
case OPENID_SSO:
menuRight.addItem(C.menuSignIn(), new Command() {
public void execute() {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java
index bbe6972..f41f67c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java
@@ -112,6 +112,7 @@
case CLIENT_SSL_CERT_LDAP:
case DEVELOPMENT_BECOME_ANY_ACCOUNT:
+ case OAUTH:
case OPENID:
case OPENID_SSO:
break;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpLogoutServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpLogoutServlet.java
index 7e1aa28..b2228a9 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpLogoutServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpLogoutServlet.java
@@ -35,7 +35,7 @@
import javax.servlet.http.HttpServletResponse;
@Singleton
-class HttpLogoutServlet extends HttpServlet {
+public class HttpLogoutServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private final DynamicItem<WebSession> webSession;
@@ -44,7 +44,7 @@
private final AuditService audit;
@Inject
- HttpLogoutServlet(final AuthConfig authConfig,
+ protected HttpLogoutServlet(final AuthConfig authConfig,
final DynamicItem<WebSession> webSession,
@CanonicalWebUrl @Nullable final Provider<String> urlProvider,
final AccountManager accountManager,
@@ -55,7 +55,7 @@
this.audit = audit;
}
- private void doLogout(final HttpServletRequest req,
+ protected void doLogout(final HttpServletRequest req,
final HttpServletResponse rsp) throws IOException {
webSession.get().logout();
if (logoutUrl != null) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
index 3c4dfc5..4c36e4d 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
@@ -33,8 +33,10 @@
import com.google.gerrit.httpd.rpc.doc.QueryDocumentationFilter;
import com.google.gerrit.httpd.rpc.group.GroupsRestApiServlet;
import com.google.gerrit.httpd.rpc.project.ProjectsRestApiServlet;
+import com.google.gerrit.reviewdb.client.AuthType;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gwtexpui.server.CacheControlFilter;
import com.google.inject.Inject;
@@ -64,10 +66,12 @@
private final UrlConfig cfg;
private GerritUiOptions uiOptions;
+ private AuthConfig authConfig;
- UrlModule(UrlConfig cfg, GerritUiOptions uiOptions) {
+ UrlModule(UrlConfig cfg, GerritUiOptions uiOptions, AuthConfig authConfig) {
this.cfg = cfg;
this.uiOptions = uiOptions;
+ this.authConfig = authConfig;
}
@Override
@@ -81,8 +85,11 @@
serve("/Gerrit/*").with(legacyGerritScreen());
}
serve("/cat/*").with(CatServlet.class);
- serve("/logout").with(HttpLogoutServlet.class);
- serve("/signout").with(HttpLogoutServlet.class);
+
+ if (authConfig.getAuthType() != AuthType.OAUTH) {
+ serve("/logout").with(HttpLogoutServlet.class);
+ serve("/signout").with(HttpLogoutServlet.class);
+ }
serve("/ssh_info").with(SshInfoServlet.class);
serve("/static/*").with(StaticServlet.class);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
index 3443968..76e1e41 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
@@ -101,6 +101,8 @@
install(new BecomeAnyAccountModule());
break;
+ case OAUTH:
+ // OAuth support is bound in WebAppInitializer and Daemon.
case OPENID:
case OPENID_SSO:
// OpenID support is bound in WebAppInitializer and Daemon.
@@ -110,7 +112,7 @@
throw new ProvisionException("Unsupported loginType: " + authConfig.getAuthType());
}
- install(new UrlModule(urlConfig, uiOptions));
+ install(new UrlModule(urlConfig, uiOptions, authConfig));
install(new UiRpcModule());
install(new GerritRequestModule());
install(new GitOverHttpServlet.Module());
diff --git a/gerrit-oauth/BUCK b/gerrit-oauth/BUCK
new file mode 100644
index 0000000..4641e81
--- /dev/null
+++ b/gerrit-oauth/BUCK
@@ -0,0 +1,24 @@
+SRCS = glob(
+ ['src/main/java/**/*.java'],
+)
+RESOURCES = glob(['src/main/resources/**/*'])
+
+java_library(
+ name = 'oauth',
+ srcs = SRCS,
+ resources = RESOURCES,
+ deps = [
+ '//gerrit-common:annotations',
+ '//gerrit-extension-api:api',
+ '//gerrit-httpd:httpd',
+ '//gerrit-server:server',
+ '//lib:gson',
+ '//lib:guava',
+ '//lib/commons:codec',
+ '//lib/guice:guice',
+ '//lib/guice:guice-servlet',
+ '//lib/log:api',
+ ],
+ provided_deps = ['//lib:servlet-api-3_1'],
+ visibility = ['PUBLIC'],
+)
diff --git a/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthLogoutServlet.java b/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthLogoutServlet.java
new file mode 100644
index 0000000..43b85bd
--- /dev/null
+++ b/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthLogoutServlet.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// 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.gerrit.httpd.auth.oauth;
+
+import com.google.gerrit.audit.AuditService;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.httpd.HttpLogoutServlet;
+import com.google.gerrit.httpd.WebSession;
+import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.io.IOException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@Singleton
+class OAuthLogoutServlet extends HttpLogoutServlet {
+ private static final long serialVersionUID = 1L;
+
+ private final Provider<OAuthSession> oauthSession;
+
+ @Inject
+ OAuthLogoutServlet(AuthConfig authConfig,
+ DynamicItem<WebSession> webSession,
+ @CanonicalWebUrl @Nullable Provider<String> urlProvider,
+ AccountManager accountManager,
+ AuditService audit,
+ Provider<OAuthSession> oauthSession) {
+ super(authConfig, webSession, urlProvider, accountManager, audit);
+ this.oauthSession = oauthSession;
+ }
+
+ @Override
+ protected void doLogout(HttpServletRequest req, HttpServletResponse rsp)
+ throws IOException {
+ super.doLogout(req, rsp);
+ oauthSession.get().logout();
+ }
+}
diff --git a/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthModule.java b/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthModule.java
new file mode 100644
index 0000000..f74e005
--- /dev/null
+++ b/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthModule.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// 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.gerrit.httpd.auth.oauth;
+
+import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.inject.servlet.ServletModule;
+
+/** Servlets and support related to OAuth authentication. */
+public class OAuthModule extends ServletModule {
+
+ @Override
+ protected void configureServlets() {
+ filter("/login", "/login/*", "/oauth").through(OAuthWebFilter.class);
+ // This is needed to invalidate OAuth session during logout
+ serve("/logout").with(OAuthLogoutServlet.class);
+ DynamicMap.mapOf(binder(), OAuthServiceProvider.class);
+ }
+}
diff --git a/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java b/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java
new file mode 100644
index 0000000..d625e02
--- /dev/null
+++ b/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java
@@ -0,0 +1,178 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// 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.gerrit.httpd.auth.oauth;
+
+import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
+import com.google.gerrit.extensions.auth.oauth.OAuthToken;
+import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
+import com.google.gerrit.extensions.auth.oauth.OAuthVerifier;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.httpd.WebSession;
+import com.google.gerrit.server.account.AccountException;
+import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.AuthResult;
+import com.google.inject.Inject;
+import com.google.inject.servlet.SessionScoped;
+
+import org.apache.commons.codec.binary.Base64;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+
+import javax.servlet.ServletRequest;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@SessionScoped
+/* OAuth protocol implementation */
+class OAuthSession {
+ private static final Logger log = LoggerFactory.getLogger(OAuthSession.class);
+ private static final SecureRandom randomState = newRandomGenerator();
+ private final String state;
+ private final DynamicItem<WebSession> webSession;
+ private final AccountManager accountManager;
+ private OAuthServiceProvider serviceProvider;
+ private OAuthToken token;
+ private OAuthUserInfo user;
+ private String redirectUrl;
+
+ @Inject
+ OAuthSession(DynamicItem<WebSession> webSession,
+ AccountManager accountManager) {
+ this.state = generateRandomState();
+ this.webSession = webSession;
+ this.accountManager = accountManager;
+ }
+
+ boolean isLoggedIn() {
+ return token != null && user != null;
+ }
+
+ boolean isOAuthFinal(HttpServletRequest request) {
+ return Strings.emptyToNull(request.getParameter("code")) != null;
+ }
+
+ boolean login(HttpServletRequest request, HttpServletResponse response,
+ OAuthServiceProvider oauth) throws IOException {
+ if (isLoggedIn()) {
+ return true;
+ }
+
+ log.debug("Login " + this);
+
+ if (isOAuthFinal(request)) {
+ if (!checkState(request)) {
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ return false;
+ }
+
+ log.debug("Login-Retrieve-User " + this);
+ token = oauth.getAccessToken(null,
+ new OAuthVerifier(request.getParameter("code")));
+
+ user = oauth.getUserInfo(token);
+
+ if (isLoggedIn()) {
+ log.debug("Login-SUCCESS " + this);
+ authenticateAndRedirect(response);
+ return true;
+ } else {
+ response.sendError(SC_UNAUTHORIZED);
+ return false;
+ }
+ } else {
+ log.debug("Login-PHASE1 " + this);
+ redirectUrl = request.getRequestURI();
+ response.sendRedirect(oauth.getAuthorizationUrl(null) +
+ "&state=" + state);
+ return false;
+ }
+ }
+
+ private void authenticateAndRedirect(HttpServletResponse rsp)
+ throws IOException {
+ com.google.gerrit.server.account.AuthRequest areq =
+ new com.google.gerrit.server.account.AuthRequest(user.getExternalId());
+ areq.setUserName(user.getUserName());
+ areq.setEmailAddress(user.getEmailAddress());
+ areq.setDisplayName(user.getDisplayName());
+ AuthResult arsp;
+ try {
+ arsp = accountManager.authenticate(areq);
+ } catch (AccountException e) {
+ log.error("Unable to authenticate user \"" + user + "\"", e);
+ rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
+ return;
+ }
+
+ webSession.get().login(arsp, true);
+ String suffix = redirectUrl.substring(
+ OAuthWebFilter.GERRIT_LOGIN.length() + 1);
+ suffix = URLDecoder.decode(suffix, StandardCharsets.UTF_8.name());
+ rsp.sendRedirect(suffix);
+ }
+
+ void logout() {
+ token = null;
+ user = null;
+ redirectUrl = null;
+ serviceProvider = null;
+ }
+
+ private boolean checkState(ServletRequest request) {
+ String s = Strings.nullToEmpty(request.getParameter("state"));
+ if (!s.equals(state)) {
+ log.error("Illegal request state '" + s + "' on OAuthProtocol " + this);
+ return false;
+ }
+ return true;
+ }
+
+ private static SecureRandom newRandomGenerator() {
+ try {
+ return SecureRandom.getInstance("SHA1PRNG");
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalArgumentException(
+ "No SecureRandom available for GitHub authentication", e);
+ }
+ }
+
+ private static String generateRandomState() {
+ byte[] state = new byte[32];
+ randomState.nextBytes(state);
+ return Base64.encodeBase64URLSafeString(state);
+ }
+
+ @Override
+ public String toString() {
+ return "OAuthSession [token=" + token + ", user=" + user + "]";
+ }
+
+ public void setServiceProvider(OAuthServiceProvider provider) {
+ this.serviceProvider = provider;
+ }
+
+ public OAuthServiceProvider getServiceProvider() {
+ return serviceProvider;
+ }
+}
diff --git a/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthWebFilter.java b/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthWebFilter.java
new file mode 100644
index 0000000..7f93437
--- /dev/null
+++ b/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthWebFilter.java
@@ -0,0 +1,223 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// 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.gerrit.httpd.auth.oauth;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.httpd.HtmlDomUtil;
+import com.google.gerrit.httpd.LoginUrlToken;
+import com.google.gerrit.httpd.template.SiteHeaderFooter;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.SortedSet;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+@Singleton
+/* OAuth web filter uses active OAuth session to perform OAuth requests */
+class OAuthWebFilter implements Filter {
+ static final String GERRIT_LOGIN = "/login";
+
+ private final Provider<String> urlProvider;
+ private final Provider<CurrentUser> currentUserProvider;
+ private final Provider<OAuthSession> oauthSessionProvider;
+ private final DynamicMap<OAuthServiceProvider> oauthServiceProviders;
+ private final SiteHeaderFooter header;
+ private OAuthServiceProvider ssoProvider;
+
+ @Inject
+ OAuthWebFilter(@CanonicalWebUrl @Nullable Provider<String> urlProvider,
+ Provider<CurrentUser> currentUserProvider,
+ DynamicMap<OAuthServiceProvider> oauthServiceProviders,
+ Provider<OAuthSession> oauthSessionProvider,
+ SiteHeaderFooter header) {
+ this.urlProvider = urlProvider;
+ this.currentUserProvider = currentUserProvider;
+ this.oauthServiceProviders = oauthServiceProviders;
+ this.oauthSessionProvider = oauthSessionProvider;
+ this.header = header;
+ }
+
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException {
+ pickSSOServiceProvider();
+ }
+
+ @Override
+ public void destroy() {
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response,
+ FilterChain chain) throws IOException, ServletException {
+ HttpServletRequest httpRequest = (HttpServletRequest) request;
+ HttpSession httpSession = ((HttpServletRequest) request).getSession(false);
+ if (currentUserProvider.get().isIdentifiedUser()) {
+ if (httpSession != null) {
+ httpSession.invalidate();
+ }
+ chain.doFilter(request, response);
+ return;
+ }
+
+ HttpServletResponse httpResponse = (HttpServletResponse) response;
+
+ String provider = httpRequest.getParameter("provider");
+ OAuthSession oauthSession = oauthSessionProvider.get();
+ OAuthServiceProvider service = ssoProvider == null
+ ? oauthSession.getServiceProvider()
+ : ssoProvider;
+
+ if ((isGerritLogin(httpRequest)
+ || oauthSession.isOAuthFinal(httpRequest))
+ && !oauthSession.isLoggedIn()) {
+ if (service == null && Strings.isNullOrEmpty(provider)) {
+ selectProvider(httpRequest, httpResponse, null);
+ return;
+ } else {
+ if (service == null) {
+ service = findService(provider);
+ }
+ oauthSession.setServiceProvider(service);
+ oauthSession.login(httpRequest, httpResponse, service);
+ }
+ } else {
+ chain.doFilter(httpRequest, response);
+ }
+ }
+
+ private OAuthServiceProvider findService(String providerId)
+ throws ServletException {
+ Set<String> plugins = oauthServiceProviders.plugins();
+ for (String pluginName : plugins) {
+ Map<String, Provider<OAuthServiceProvider>> m =
+ oauthServiceProviders.byPlugin(pluginName);
+ for (Map.Entry<String, Provider<OAuthServiceProvider>> e
+ : m.entrySet()) {
+ if (providerId.equals(
+ String.format("%s_%s", pluginName, e.getKey()))) {
+ return e.getValue().get();
+ }
+ }
+ }
+ throw new ServletException("No provider found for: " + providerId);
+ }
+
+ private void selectProvider(HttpServletRequest req, HttpServletResponse res,
+ @Nullable String errorMessage)
+ throws IOException {
+ String self = req.getRequestURI();
+ String cancel = Objects.firstNonNull(
+ urlProvider != null ? urlProvider.get() : "/", "/");
+ cancel += LoginUrlToken.getToken(req);
+
+ Document doc = header.parse(OAuthWebFilter.class, "LoginForm.html");
+ HtmlDomUtil.find(doc, "hostName").setTextContent(req.getServerName());
+ HtmlDomUtil.find(doc, "login_form").setAttribute("action", self);
+ HtmlDomUtil.find(doc, "cancel_link").setAttribute("href", cancel);
+
+ Element emsg = HtmlDomUtil.find(doc, "error_message");
+ if (Strings.isNullOrEmpty(errorMessage)) {
+ emsg.getParentNode().removeChild(emsg);
+ } else {
+ emsg.setTextContent(errorMessage);
+ }
+
+ Element providers = HtmlDomUtil.find(doc, "providers");
+
+ Set<String> plugins = oauthServiceProviders.plugins();
+ for (String pluginName : plugins) {
+ Map<String, Provider<OAuthServiceProvider>> m =
+ oauthServiceProviders.byPlugin(pluginName);
+ for (Map.Entry<String, Provider<OAuthServiceProvider>> e
+ : m.entrySet()) {
+ addProvider(providers, pluginName, e.getKey(),
+ e.getValue().get().getName());
+ }
+ }
+
+ sendHtml(res, doc);
+ }
+
+ private static void addProvider(Element form, String pluginName,
+ String id, String serviceName) {
+ Element div = form.getOwnerDocument().createElement("div");
+ div.setAttribute("id", id);
+ Element hyperlink = form.getOwnerDocument().createElement("a");
+ hyperlink.setAttribute("href", String.format("?provider=%s_%s",
+ pluginName, id));
+ hyperlink.setTextContent(serviceName +
+ " (" + pluginName + " plugin)");
+ div.appendChild(hyperlink);
+ form.appendChild(div);
+ }
+
+ private static void sendHtml(HttpServletResponse res, Document doc)
+ throws IOException {
+ byte[] bin = HtmlDomUtil.toUTF8(doc);
+ res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+ res.setContentType("text/html");
+ res.setCharacterEncoding(StandardCharsets.UTF_8.name());
+ res.setContentLength(bin.length);
+ try (ServletOutputStream out = res.getOutputStream()) {
+ out.write(bin);
+ }
+ }
+
+ private void pickSSOServiceProvider()
+ throws ServletException {
+ SortedSet<String> plugins = oauthServiceProviders.plugins();
+ if (plugins.isEmpty()) {
+ throw new ServletException(
+ "OAuth service provider wasn't installed");
+ }
+ if (plugins.size() == 1) {
+ SortedMap<String, Provider<OAuthServiceProvider>> services =
+ oauthServiceProviders.byPlugin(Iterables.getOnlyElement(plugins));
+ if (services.size() == 1) {
+ ssoProvider = Iterables.getOnlyElement(services.values()).get();
+ }
+ }
+ }
+
+ private static boolean isGerritLogin(HttpServletRequest request) {
+ return request.getRequestURI().indexOf(GERRIT_LOGIN) >= 0;
+ }
+}
diff --git a/gerrit-oauth/src/main/resources/com/google/gerrit/httpd/auth/oauth/LoginForm.html b/gerrit-oauth/src/main/resources/com/google/gerrit/httpd/auth/oauth/LoginForm.html
new file mode 100644
index 0000000..f7814c0
--- /dev/null
+++ b/gerrit-oauth/src/main/resources/com/google/gerrit/httpd/auth/oauth/LoginForm.html
@@ -0,0 +1,58 @@
+<html>
+ <head>
+ <title>Gerrit Code Review - Sign In</title>
+ <style>
+ #error_message {
+ padding: 5px;
+ margin-left: 5px;
+ margin-bottom: 5px;
+ width: 20em;
+ background-color: rgb(255, 255, 116);
+ font-weight: bold;
+ }
+ #cancel_link {
+ margin-left: 45px;
+ }
+ #logo_box {
+ padding-left: 160px;
+ }
+ #logo_img {
+ width: 239px;
+ height: 240px;
+ background: url('data:image/png;base64,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') no-repeat 0px 0px;
+ }
+ </style>
+ <style id="gerrit_sitecss" type="text/css"></style>
+ </head>
+ <body>
+ <div id="gerrit_topmenu" style="height:45px;" class="gerritTopMenu"></div>
+ <div id="gerrit_header"></div>
+ <div id="gerrit_body" class="gerritBody">
+ <h1>Sign In to Gerrit Code Review at <span id="hostName">example.com</span></h1>
+ <form method="POST" action="#" id="login_form">
+ <input type="hidden" name="link" id="f_link" value="1" />
+ <div id="logo_box"><div id="logo_img"></div></div>
+ <div id="error_message">Invalid OAuth provider.</div>
+
+ <div>Available OAuth providers:</div>
+
+ <div id="providers">
+ </div>
+
+ <div>
+ <a href="../" id="cancel_link">Cancel</a>
+ </div>
+
+ <div style="margin-top: 25px;">
+ <h2>What is OAuth protocol?</h2>
+ <p>OAuth is an open standard for authorization. OAuth provides client applications a 'secure delegated access'</p>
+ <p>to server resources on behalf of a resource owner. It specifies a process for resource owners to authorize</p>
+ <p>third-party access to their server resources without sharing their credentials.</p>
+ </div>
+ </form>
+ </div>
+ <div style="clear: both; margin-top: 15px; padding-top: 2px; margin-bottom: 15px;">
+ <div id="gerrit_footer"></div>
+ </div>
+ </body>
+</html>
diff --git a/gerrit-pgm/BUCK b/gerrit-pgm/BUCK
index 3a89cd0..8b5fdc1 100644
--- a/gerrit-pgm/BUCK
+++ b/gerrit-pgm/BUCK
@@ -99,6 +99,7 @@
'//gerrit-gwtexpui:server',
'//gerrit-httpd:httpd',
'//gerrit-lucene:lucene',
+ '//gerrit-oauth:oauth',
'//gerrit-openid:openid',
'//gerrit-reviewdb:server',
'//gerrit-server:server',
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
index 88a16fb..e28b5ba 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
@@ -27,6 +27,7 @@
import com.google.gerrit.httpd.RequestContextFilter;
import com.google.gerrit.httpd.WebModule;
import com.google.gerrit.httpd.WebSshGlueModule;
+import com.google.gerrit.httpd.auth.oauth.OAuthModule;
import com.google.gerrit.httpd.auth.openid.OpenIdModule;
import com.google.gerrit.httpd.plugins.HttpPluginModule;
import com.google.gerrit.lifecycle.LifecycleManager;
@@ -430,6 +431,8 @@
if (authConfig.getAuthType() == AuthType.OPENID ||
authConfig.getAuthType() == AuthType.OPENID_SSO) {
modules.add(new OpenIdModule());
+ } else if (authConfig.getAuthType() == AuthType.OAUTH) {
+ modules.add(new OAuthModule());
}
modules.add(sysInjector.getInstance(GetUserFilter.Module.class));
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java
index a2037b5..74884a4 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java
@@ -60,6 +60,7 @@
case DEVELOPMENT_BECOME_ANY_ACCOUNT:
case LDAP:
case LDAP_BIND:
+ case OAUTH:
case OPENID:
case OPENID_SSO:
break;
@@ -94,6 +95,7 @@
case CUSTOM_EXTENSION:
case DEVELOPMENT_BECOME_ANY_ACCOUNT:
case HTTP:
+ case OAUTH:
case OPENID:
case OPENID_SSO:
break;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AuthType.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AuthType.java
index 6af9610..38a78ba 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AuthType.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AuthType.java
@@ -80,5 +80,8 @@
CUSTOM_EXTENSION,
/** Development mode to enable becoming anyone you want. */
- DEVELOPMENT_BECOME_ANY_ACCOUNT
+ DEVELOPMENT_BECOME_ANY_ACCOUNT,
+
+ /** Generic OAuth provider over HTTP. */
+ OAUTH
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
index 938d940..4b8f0b4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
@@ -40,7 +40,8 @@
@Override
public boolean allowsEdit(final Account.FieldName field) {
- if (authConfig.getAuthType() == AuthType.HTTP) {
+ if (authConfig.getAuthType() == AuthType.HTTP
+ || authConfig.getAuthType() == AuthType.OAUTH) {
switch (field) {
case USER_NAME:
return false;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
index b1ec6e4..c2cf95e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
@@ -197,7 +197,7 @@
case LDAP_BIND:
case CLIENT_SSL_CERT_LDAP:
case CUSTOM_EXTENSION:
- // Its safe to assume yes for an HTTP authentication type, as the
+ case OAUTH:
// only way in is through some external system that the admin trusts
//
return true;
diff --git a/gerrit-war/BUCK b/gerrit-war/BUCK
index fc73973..f557edc 100644
--- a/gerrit-war/BUCK
+++ b/gerrit-war/BUCK
@@ -8,6 +8,7 @@
'//gerrit-extension-api:api',
'//gerrit-httpd:httpd',
'//gerrit-lucene:lucene',
+ '//gerrit-oauth:oauth',
'//gerrit-openid:openid',
'//gerrit-pgm:init-api',
'//gerrit-pgm:init-base',
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
index 56b092e..e9bd296 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
@@ -19,6 +19,7 @@
import com.google.common.base.Splitter;
import com.google.gerrit.common.ChangeHookRunner;
+import com.google.gerrit.httpd.auth.oauth.OAuthModule;
import com.google.gerrit.httpd.auth.openid.OpenIdModule;
import com.google.gerrit.httpd.plugins.HttpPluginModule;
import com.google.gerrit.lifecycle.LifecycleManager;
@@ -343,6 +344,8 @@
AuthConfig authConfig = cfgInjector.getInstance(AuthConfig.class);
if (authConfig.getAuthType() == AuthType.OPENID) {
modules.add(new OpenIdModule());
+ } else if (authConfig.getAuthType() == AuthType.OAUTH) {
+ modules.add(new OAuthModule());
}
return sysInjector.createChildInjector(modules);