diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt index a50c788284..f121e1f6f6 100644 --- a/Documentation/config-gerrit.txt +++ b/Documentation/config-gerrit.txt @@ -145,6 +145,16 @@ request is the exact string supplied in the dialog by the user. The configured <> 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 0000000000..8375e3179e --- /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 0000000000..901951ed67 --- /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 0000000000..23a7bec4b4 --- /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 0000000000..33c45c599e --- /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 d81827c7d2..27a4b53bf8 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 @@ public class Gerrit implements EntryPoint { }); 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 bbe6972611..f41f67cbe2 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 @@ class GerritConfigProvider implements Provider { 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 7e1aa28d7e..b2228a98f3 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.HttpServletRequest; 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; @@ -44,7 +44,7 @@ class HttpLogoutServlet extends HttpServlet { private final AuditService audit; @Inject - HttpLogoutServlet(final AuthConfig authConfig, + protected HttpLogoutServlet(final AuthConfig authConfig, final DynamicItem webSession, @CanonicalWebUrl @Nullable final Provider urlProvider, final AccountManager accountManager, @@ -55,7 +55,7 @@ class HttpLogoutServlet extends HttpServlet { 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 3c4dfc5d35..4c36e4d66d 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.config.ConfigRestApiServlet; 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 @@ class UrlModule extends ServletModule { 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 @@ class UrlModule extends ServletModule { 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 3443968785..76e1e414e7 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 @@ public class WebModule extends LifecycleModule { 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 @@ public class WebModule extends LifecycleModule { 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 0000000000..4641e8168e --- /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 0000000000..43b85bd29d --- /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; + + @Inject + OAuthLogoutServlet(AuthConfig authConfig, + DynamicItem webSession, + @CanonicalWebUrl @Nullable Provider urlProvider, + AccountManager accountManager, + AuditService audit, + Provider 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 0000000000..f74e00519d --- /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 0000000000..d625e02abd --- /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; + private final AccountManager accountManager; + private OAuthServiceProvider serviceProvider; + private OAuthToken token; + private OAuthUserInfo user; + private String redirectUrl; + + @Inject + OAuthSession(DynamicItem 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 0000000000..7f93437fbe --- /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 urlProvider; + private final Provider currentUserProvider; + private final Provider oauthSessionProvider; + private final DynamicMap oauthServiceProviders; + private final SiteHeaderFooter header; + private OAuthServiceProvider ssoProvider; + + @Inject + OAuthWebFilter(@CanonicalWebUrl @Nullable Provider urlProvider, + Provider currentUserProvider, + DynamicMap oauthServiceProviders, + Provider 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 plugins = oauthServiceProviders.plugins(); + for (String pluginName : plugins) { + Map> m = + oauthServiceProviders.byPlugin(pluginName); + for (Map.Entry> 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 plugins = oauthServiceProviders.plugins(); + for (String pluginName : plugins) { + Map> m = + oauthServiceProviders.byPlugin(pluginName); + for (Map.Entry> 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 plugins = oauthServiceProviders.plugins(); + if (plugins.isEmpty()) { + throw new ServletException( + "OAuth service provider wasn't installed"); + } + if (plugins.size() == 1) { + SortedMap> 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 0000000000..f7814c039b --- /dev/null +++ b/gerrit-oauth/src/main/resources/com/google/gerrit/httpd/auth/oauth/LoginForm.html @@ -0,0 +1,58 @@ + + + Gerrit Code Review - Sign In + + + + +
+
+
+

Sign In to Gerrit Code Review at example.com

+
+ +
+
Invalid OAuth provider.
+ +
Available OAuth providers:
+ +
+
+ +
+ Cancel +
+ +
+

What is OAuth protocol?

+

OAuth is an open standard for authorization. OAuth provides client applications a 'secure delegated access'

+

to server resources on behalf of a resource owner. It specifies a process for resource owners to authorize

+

third-party access to their server resources without sharing their credentials.

+
+
+
+
+ +
+ + diff --git a/gerrit-pgm/BUCK b/gerrit-pgm/BUCK index 3a89cd0d76..8b5fdc173a 100644 --- a/gerrit-pgm/BUCK +++ b/gerrit-pgm/BUCK @@ -99,6 +99,7 @@ java_library( '//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 88a16fb8bc..e28b5ba9ad 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.HttpCanonicalWebUrlProvider; 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 @@ public class Daemon extends SiteProgram { 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 a2037b52db..74884a443b 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 @@ class InitAuth implements InitStep { case DEVELOPMENT_BECOME_ANY_ACCOUNT: case LDAP: case LDAP_BIND: + case OAUTH: case OPENID: case OPENID_SSO: break; @@ -94,6 +95,7 @@ class InitAuth implements InitStep { 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 6af9610a9c..38a78baac9 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 @@ public enum AuthType { 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 938d940ac6..4b8f0b4b1e 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 @@ public class DefaultRealm implements Realm { @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 b1ec6e4941..c2cf95e6ca 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 @@ public class AuthConfig { 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 fc73973711..f557edc799 100644 --- a/gerrit-war/BUCK +++ b/gerrit-war/BUCK @@ -8,6 +8,7 @@ java_library( '//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 56b092e971..e9bd296ea9 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 static com.google.inject.Stage.PRODUCTION; 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 @@ public class WebAppInitializer extends GuiceServletContextListener 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);