From 8b5aa48f1da37ca35beddabcd85c306438be3fe3 Mon Sep 17 00:00:00 2001 From: David Ostrovsky Date: Mon, 16 Mar 2015 08:16:45 +0100 Subject: [PATCH] Support hybrid OpenID and OAuth2 authentication e9707d8f85 exposed OAuth authentication extension point. Using this extension point plugins can offer OAuth2 authentications. That is fine for new Gerrit sites, which can restrict the auth scheme to OAuth2 only. For the existing sites, that rely on non SSO OpenID auth scheme it doesn't work to migrate to OAuth2 because of diverse contributors base that use different OpenID providers. Not all OpenID providers offer OAuth2 protocol. Particularly, widespread OpenID providers among open source Gerrit communities are Launchpad/UbuntuOne and FedoraProject don't offer OAuth2 protocol. To not lock out those contributors from being able to contribute to open source Gerrit based projects OpenID must still be supported. With Google's shut down of their OpenID service in April 2015, big user base is locked out from contribution to Gerrit based projects that only support OpenID auth scheme. The only way to still support OpenID 2.0 providers and new OAuth2 based protocol is native support for hybrid authentication scheme in Gerrit. This change extends OpenID auth scheme by making it aware of optional OAuth plugin-based authentication. When no oauth-provider plugins are deployed, OpenID auth scheme works as usual. When OAuth provider plugins are deployed, OAuth2 providers are offered on the OpenID login form, in addition to hard coded Yahoo! and Launchpad OpenID providers: [1]. [1] http://imgur.com/IcCrChN Change-Id: I6d70212f4fea5443a6322c7da683e1e943d058eb --- .../com/google/gerrit/httpd/UrlModule.java | 3 +- gerrit-openid/BUCK | 1 + .../gerrit/httpd/auth/openid/LoginForm.java | 83 ++++++- .../openid/OAuthOverOpenIDLogoutServlet.java | 57 +++++ .../auth/openid/OAuthSessionOverOpenID.java | 216 ++++++++++++++++++ .../auth/openid/OAuthWebFilterOverOpenID.java | 116 ++++++++++ .../httpd/auth/openid/OpenIdModule.java | 5 + .../gerrit/httpd/auth/openid/LoginForm.html | 17 +- 8 files changed, 493 insertions(+), 5 deletions(-) create mode 100644 gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthOverOpenIDLogoutServlet.java create mode 100644 gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java create mode 100644 gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthWebFilterOverOpenID.java 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 4c36e4d66d..031e3a230f 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 @@ -86,7 +86,8 @@ class UrlModule extends ServletModule { } serve("/cat/*").with(CatServlet.class); - if (authConfig.getAuthType() != AuthType.OAUTH) { + if (authConfig.getAuthType() != AuthType.OAUTH && + authConfig.getAuthType() != AuthType.OPENID) { serve("/logout").with(HttpLogoutServlet.class); serve("/signout").with(HttpLogoutServlet.class); } diff --git a/gerrit-openid/BUCK b/gerrit-openid/BUCK index 8761d346af..78abce88ec 100644 --- a/gerrit-openid/BUCK +++ b/gerrit-openid/BUCK @@ -12,6 +12,7 @@ java_library( '//gerrit-server:server', '//lib:guava', '//lib:gwtorm', + '//lib/commons:codec', '//lib/guice:guice', '//lib/guice:guice-servlet', '//lib/jgit:jgit', diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java index fc2f0e0983..93031fef62 100644 --- a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java +++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java @@ -22,11 +22,14 @@ import com.google.common.collect.Sets; import com.google.gerrit.common.Nullable; import com.google.gerrit.common.PageLinks; import com.google.gerrit.common.auth.openid.OpenIdUrls; +import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider; +import com.google.gerrit.extensions.registration.DynamicMap; import com.google.gerrit.extensions.restapi.Url; import com.google.gerrit.httpd.HtmlDomUtil; import com.google.gerrit.httpd.LoginUrlToken; import com.google.gerrit.httpd.template.SiteHeaderFooter; import com.google.gerrit.reviewdb.client.AuthType; +import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.config.AuthConfig; import com.google.gerrit.server.config.CanonicalWebUrl; import com.google.gerrit.server.config.GerritServerConfig; @@ -61,10 +64,13 @@ class LoginForm extends HttpServlet { private final ImmutableSet suggestProviders; private final Provider urlProvider; + private final Provider oauthSessionProvider; private final OpenIdServiceImpl impl; private final int maxRedirectUrlLength; private final String ssoUrl; private final SiteHeaderFooter header; + private final Provider currentUserProvider; + private final DynamicMap oauthServiceProviders; @Inject LoginForm( @@ -72,13 +78,19 @@ class LoginForm extends HttpServlet { @GerritServerConfig Config config, AuthConfig authConfig, OpenIdServiceImpl impl, - SiteHeaderFooter header) { + SiteHeaderFooter header, + Provider oauthSessionProvider, + Provider currentUserProvider, + DynamicMap oauthServiceProviders) { this.urlProvider = urlProvider; this.impl = impl; this.header = header; this.maxRedirectUrlLength = config.getInt( "openid", "maxRedirectUrlLength", 10); + this.oauthSessionProvider = oauthSessionProvider; + this.currentUserProvider = currentUserProvider; + this.oauthServiceProviders = oauthServiceProviders; if (urlProvider == null || Strings.isNullOrEmpty(urlProvider.get())) { log.error("gerrit.canonicalWebUrl must be set in gerrit.config"); @@ -152,7 +164,23 @@ class LoginForm extends HttpServlet { mode = SignInMode.SIGN_IN; } - discover(req, res, link, id, remember, token, mode); + OAuthServiceProvider oauthProvider = lookupOAuthServiceProvider(id); + + if (oauthProvider == null) { + discover(req, res, link, id, remember, token, mode); + } else { + OAuthSessionOverOpenID oauthSession = oauthSessionProvider.get(); + if (!currentUserProvider.get().isIdentifiedUser() + && oauthSession.isLoggedIn()) { + oauthSession.logout(); + } + if ((isGerritLogin(req) + || oauthSession.isOAuthFinal(req)) + && !oauthSession.isLoggedIn()) { + oauthSession.setServiceProvider(oauthProvider); + oauthSession.login(req, res, oauthProvider); + } + } } private void discover(HttpServletRequest req, HttpServletResponse res, @@ -266,6 +294,20 @@ class LoginForm extends HttpServlet { } a.setAttribute("href", u.toString()); } + + // OAuth: Add plugin based providers + 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); } @@ -284,6 +326,38 @@ class LoginForm extends HttpServlet { } } + 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("?id=%s_%s", + pluginName, id)); + hyperlink.setTextContent(serviceName + + " (" + pluginName + " plugin)"); + div.appendChild(hyperlink); + form.appendChild(div); + } + + private OAuthServiceProvider lookupOAuthServiceProvider(String providerId) { + if (providerId.startsWith("http://")) { + providerId = providerId.substring("http://".length()); + } + 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(); + } + } + } + return null; + } + private static String getLastId(HttpServletRequest req) { Cookie[] cookies = req.getCookies(); if (cookies != null) { @@ -295,4 +369,9 @@ class LoginForm extends HttpServlet { } return null; } + + private static boolean isGerritLogin(HttpServletRequest request) { + return request.getRequestURI().indexOf( + OAuthSessionOverOpenID.GERRIT_LOGIN) >= 0; + } } diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthOverOpenIDLogoutServlet.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthOverOpenIDLogoutServlet.java new file mode 100644 index 0000000000..8ca71ff858 --- /dev/null +++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthOverOpenIDLogoutServlet.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.openid; + +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 OAuthOverOpenIDLogoutServlet extends HttpLogoutServlet { + private static final long serialVersionUID = 1L; + + private final Provider oauthSession; + + @Inject + OAuthOverOpenIDLogoutServlet(AuthConfig authConfig, + DynamicItem webSession, + AccountManager accountManager, + @CanonicalWebUrl @Nullable Provider urlProvider, + 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-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java new file mode 100644 index 0000000000..a02f52d5d0 --- /dev/null +++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java @@ -0,0 +1,216 @@ +// 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.openid; + +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.extensions.restapi.Url; +import com.google.gerrit.httpd.CanonicalWebUrl; +import com.google.gerrit.httpd.LoginUrlToken; +import com.google.gerrit.httpd.WebSession; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.server.account.AccountException; +import com.google.gerrit.server.account.AccountManager; +import com.google.gerrit.server.account.AuthResult; +import com.google.gwtorm.server.OrmException; +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.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** OAuth protocol implementation */ +@SessionScoped +class OAuthSessionOverOpenID { + static final String GERRIT_LOGIN = "/login"; + private static final Logger log = LoggerFactory.getLogger( + OAuthSessionOverOpenID.class); + private static final SecureRandom randomState = newRandomGenerator(); + private final String state; + private final DynamicItem webSession; + private final AccountManager accountManager; + private final CanonicalWebUrl urlProvider; + private OAuthServiceProvider serviceProvider; + private OAuthToken token; + private OAuthUserInfo user; + private String redirectToken; + + @Inject + OAuthSessionOverOpenID(DynamicItem webSession, + AccountManager accountManager, + CanonicalWebUrl urlProvider) { + this.state = generateRandomState(); + this.webSession = webSession; + this.accountManager = accountManager; + this.urlProvider = urlProvider; + } + + 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(new OAuthVerifier(request.getParameter("code"))); + + user = oauth.getUserInfo(token); + + if (isLoggedIn()) { + log.debug("Login-SUCCESS " + this); + authenticateAndRedirect(request, response); + return true; + } else { + response.sendError(SC_UNAUTHORIZED); + return false; + } + } else { + log.debug("Login-PHASE1 " + this); + redirectToken = LoginUrlToken.getToken(request); + response.sendRedirect(oauth.getAuthorizationUrl() + + "&state=" + state); + return false; + } + } + + private void authenticateAndRedirect(HttpServletRequest req, + HttpServletResponse rsp) throws IOException { + com.google.gerrit.server.account.AuthRequest areq = + new com.google.gerrit.server.account.AuthRequest(user.getExternalId()); + AuthResult arsp = null; + try { + String claimedIdentifier = user.getClaimedIdentity(); + Account.Id actualId = accountManager.lookup(user.getExternalId()); + if (!Strings.isNullOrEmpty(claimedIdentifier)) { + Account.Id claimedId = accountManager.lookup(claimedIdentifier); + if (claimedId != null && actualId != null) { + if (claimedId.equals(actualId)) { + // Both link to the same account, that's what we expected. + } else { + // This is (for now) a fatal error. There are two records + // for what might be the same user. + // + log.error("OAuth accounts disagree over user identity:\n" + + " Claimed ID: " + claimedId + " is " + claimedIdentifier + + "\n" + " Delgate ID: " + actualId + " is " + + user.getExternalId()); + rsp.sendError(HttpServletResponse.SC_FORBIDDEN); + return; + } + } else if (claimedId != null && actualId == null) { + // Claimed account already exists: link to it. + // + try { + accountManager.link(claimedId, areq); + } catch (OrmException e) { + log.error("Cannot link: " + user.getExternalId() + + " to user identity:\n" + + " Claimed ID: " + claimedId + " is " + claimedIdentifier); + rsp.sendError(HttpServletResponse.SC_FORBIDDEN); + return; + } + } + } + areq.setUserName(user.getUserName()); + areq.setEmailAddress(user.getEmailAddress()); + areq.setDisplayName(user.getDisplayName()); + 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); + StringBuilder rdr = new StringBuilder(urlProvider.get(req)); + rdr.append(Url.decode(redirectToken)); + rsp.sendRedirect(rdr.toString()); + } + + void logout() { + token = null; + user = null; + redirectToken = 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-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthWebFilterOverOpenID.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthWebFilterOverOpenID.java new file mode 100644 index 0000000000..53fc889305 --- /dev/null +++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthWebFilterOverOpenID.java @@ -0,0 +1,116 @@ +// 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.openid; + +import com.google.common.collect.Iterables; +import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider; +import com.google.gerrit.extensions.registration.DynamicMap; +import com.google.gerrit.server.CurrentUser; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; + +import java.io.IOException; +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.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + + +/** OAuth web filter uses active OAuth session to perform OAuth requests */ +@Singleton +class OAuthWebFilterOverOpenID implements Filter { + static final String GERRIT_LOGIN = "/login"; + + private final Provider currentUserProvider; + private final Provider oauthSessionProvider; + private final DynamicMap oauthServiceProviders; + private OAuthServiceProvider ssoProvider; + + @Inject + OAuthWebFilterOverOpenID(Provider currentUserProvider, + DynamicMap oauthServiceProviders, + Provider oauthSessionProvider) { + this.currentUserProvider = currentUserProvider; + this.oauthServiceProviders = oauthServiceProviders; + this.oauthSessionProvider = oauthSessionProvider; + } + + @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; + + OAuthSessionOverOpenID oauthSession = oauthSessionProvider.get(); + OAuthServiceProvider service = ssoProvider == null + ? oauthSession.getServiceProvider() + : ssoProvider; + + if ((isGerritLogin(httpRequest) + || oauthSession.isOAuthFinal(httpRequest)) + && !oauthSession.isLoggedIn()) { + if (service == null) { + throw new IllegalStateException("service is unknown"); + } + oauthSession.setServiceProvider(service); + oauthSession.login(httpRequest, httpResponse, service); + } else { + chain.doFilter(httpRequest, response); + } + } + + private void pickSSOServiceProvider() + throws ServletException { + SortedSet plugins = oauthServiceProviders.plugins(); + 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-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdModule.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdModule.java index c87a0cf31a..ace0c5349b 100644 --- a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdModule.java +++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdModule.java @@ -14,6 +14,8 @@ package com.google.gerrit.httpd.auth.openid; +import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider; +import com.google.gerrit.extensions.registration.DynamicMap; import com.google.inject.servlet.ServletModule; /** Servlets related to OpenID authentication. */ @@ -21,9 +23,12 @@ public class OpenIdModule extends ServletModule { @Override protected void configureServlets() { serve("/login", "/login/*").with(LoginForm.class); + serve("/logout").with(OAuthOverOpenIDLogoutServlet.class); + filter("/oauth").through(OAuthWebFilterOverOpenID.class); serve("/" + OpenIdServiceImpl.RETURN_URL).with(OpenIdLoginServlet.class); serve("/" + XrdsServlet.LOCATION).with(XrdsServlet.class); filter("/").through(XrdsFilter.class); bind(OpenIdServiceImpl.class); + DynamicMap.mapOf(binder(), OAuthServiceProvider.class); } } diff --git a/gerrit-openid/src/main/resources/com/google/gerrit/httpd/auth/openid/LoginForm.html b/gerrit-openid/src/main/resources/com/google/gerrit/httpd/auth/openid/LoginForm.html index 1e2c51052d..07e09f5324 100644 --- a/gerrit-openid/src/main/resources/com/google/gerrit/httpd/auth/openid/LoginForm.html +++ b/gerrit-openid/src/main/resources/com/google/gerrit/httpd/auth/openid/LoginForm.html @@ -16,9 +16,19 @@ #logo_box { padding-left: 160px; } - #logo_img { + #logo_oauth { + width: 96px; + height: 96px; + display: inline-block; + margin-bottom: 20px; + background: url('data:image/png;base64,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') no-repeat 0px 0px; + } + #logo_openid { width: 200px; height: 80px; + display: inline-block; + margin-left: 100px; + margin-bottom: 28px; background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAABQCAYAAABcbTqwAAANbUlEQVR42uxdeXBV1Rm/URYpuNDRFjtuZcaOZTpd7D+O07pi65QqFMQEE7KzhF2kQEUr1gUkIBQiIKDUBYRgUBCCIHEBBLKR5GUlKwlZSAKJtvUPZzr8+r73u80dfHnv3iyveeF9v5lvQsi53zvv3O93z/mWc66hUCgUCoVCoVAoFAqFQqFQKBQKhUKhUCgUCoVCoVAoFAoFQaClAtgQDhSmw1AoFBbwxRZgzgggwgBcShCFwgM0FAGrHgaiDEqcWwoPKEEUChxIBmZ+H5gkxBBRgigUBqozgeX3AU8YQLRb4kWUIAqFgT1LgSlDSI54ESWIQmH5G7EGMFkIoQRRKC4B6l3A1EFArBJEoeiEIIXA1MFKEIVCCaJQKEEUCiWIQqEEUSiUIApFcOdBlCAKhS9cPOtyRJCLWs2rCAVcuHBhakNDA2pqalBWVYOqI3twccogIM4XQfizdv8mlFXXea6T60WPoVD0d5w7dw6VlZUoLCxEbm4ucnJykJWV5ZHMUwUoPrTTEUHKd69DZn5Rx7XZ2dkefS6Xy6O/qalJZxhF/0BjYyNKSko8BmwatBBDRP6vQ7LzC1HySaojglR8kIJsF3Wa0qFTyCKfIf9XVFSEs2fPdoksLS0tOe5ZSQmmCBza2tpGnz59mqTgE96LEJReIoi3eJGlrKwM58+fX274gcw+J0+elKVbjwmCtjogOxVIXw58uJSy9wXgxLtAQ7ESMBQhBiizhRimLSm8CcIlVncIYi8dfZL+WUSxZrm8vLyOGe7MmTPoNjFKPwXWjgeShrJk/09uecSUsQa3DCcOBJbfD2TvuuyJguNvA4nXAnNvAM7XAuVHgZnXUmZcDSQN85YZbll4G/Dyb4B3ZgJZO/v/OMkT2jRCRwbLJZHlgwhBSg/vckSQyg9fcxOkVD7La4ZyShTpryynSktLuezj9T0iCLbNYQRuglmyv2gksH4i8O4sYPtcYHMUsPSXQLzZJtItKROA9st3SYcvNgHhBpBwBdBaDc8DJNqgJIQBiVd6S3wYEGWO0TiDD5qFtwOHU/rfONXV1dHwnBHDy0eQ5Uxzc3Ob6Pq2OtdRmPfb3D34n78gxlxcXIxTp06RLM774UXonhAEKx8CHvfMENwNWbAPPttWnQRem0AjeMwtz7lJc6EWlyVBjmwRA+c2htYaoOwz3t/EAcCpD4DGEqC+0JKGYuBMLpC3Fzi4Clg3Hpj+PWCiQaKt/gPwTdtooz/gO09ff9LRTpY4sqTpUaKw6ONOr5folfTJ/Dyny68eEwSbnhBycNZIewaOr/v8dSBhEG/+S3eFDkFiDGDKQOBcORzpaCoDNkbIA4XjvOqh4B+r/Px8J0/rjjZVVVX4f5aaVFdXW58fQILgy7doAJPckrqg6zPP0TeA6DA+HdMWI5QIIve7a9uxnwciTZJ88GxwjlV7e/tIy/Dso0jl5eXoy1osiaZJXwJGkIU/5g17+e7uO/ZbJlPHtKuAxlIoQXwD68bJw4RO/oW64Bsrifb4NziSR9qJj9DXxYrinwSKIO6QLW9+TFiP6sOEFJg2hCR535pFRCfKj+DSZdkm4PVIYMUDQPKDwJo/AmlLgLo8Z31uqQIOrABSJgLJD4gervPTV3icaK/2/2pNhSsdKP7E6ldzBbB7CbB6DHWsHA1sjoX0LeAEqcuTKCBJcvDV4CKIZKrF+O3IIUbZ19W8sqSziW71nCDrHzOd7J/bt7fXRV/kmVEgaUr4XZ++Ax1kWfgTCRuTSDEGxynCjPQkDga2z/PbDzEozBgOjDfY7xiK+3Opd9YNwOcbqcMiKX2rGdeAUamNwJSh/Mwosw+RnutptItHARXHEBiCEG5SMsK19pEgIQgNTozH1rjECe/Lcvf6+npPX06cOCH96bLIdeK/OOrvkyNoaDvm95wgR9+kIU0ZALRUAs0V/K5L7wRy0oCYK2ioshzL2QWcLZCID/8m5DIjYlgzBp3qf3+RkIlGvPxe4NhWoDYPkALRY//gbBRu+lJ7n4dFkHR+7pzrgf3L2GbezUyAVp4Amko9swu2z5FQLsn75I1A21kEhCD8LvyuC24KHoLYPY3l71Jj1Zf7QVpbW1PFuCV0LLNAN0TIwUSiDXAmlwYRZfRKIsttrEIO6svdLWFfhkLn/wiYeyOQdL3fQ7vx2UYglgYqy59L/pa9E4g0jf8932TGjqekDclYkmHNXHEG+xLnlmX3AO318JEUZJuJdKIDRBAubSOpUx4mwZIE9OuQy89Q2jAF137e6IQrgZos9IrOWdfRwD9ZI0bIsYg2TNKkwcGTldcnXQ20WFFD97KNT9x1Y+11bHicy5fl99DAiw7JmLMP824EvmqAjRPN61+5D+bM2PsEcaVTR/wVXM71NYQAdkur2traUCIIw7O88b1WW4Uld9A/2PeSZNdlLLgsWvmgI/34pn2ke8YhGT56ASaRadwJgxwZJOqLgISBQFwYPMGD019wzCeZOm2AjBSSdMEt/HxZvvUyQYQUMnuLHvl+Rp+Ba3pb30Oy2KG25RZHNsMiSBF6ReezPzMJ8vKlBMlYB8c63p5Gh/nV39NAd86nQ736Yec6XrmXs4As2yTrH+t8pkTeXvosScMCR5DK4+xPTF+/CoN5BL/LKzPXEXoEyU0TQ6DfUJffK/1xO8Jcv3+cLASh7jjDY1RdcPZJqgU300DXjCHp9ix1ruO9uSys3D5PwsxiiCwg/Lf9xjSpckC0tL86UAShXxRrcJYr+7RvbUEcb39LLJlduH8ixAhSeVzWwDSGvL097Q9zC9MG05hObJMkmDjGlHrHMxQNNIYGKpEkLP2VLLHo7C8Z5ZY7/Iv4K3NvYLQqZRxQ6NYXS30OjTfgBJEqaEymDi5v+7akxDZ6JdGjECMIE2izruV6+6MXe04Q3nRWstaeEie7OwTh+jzezMrX5kqY2NTrUOIMVtTGDgY2hgcnQQ6s4LjPHg7xu4KdIAyLhhRBCKy4nw5x8gM9J8hbU4GJVmxf8gsWQQq7OYPUsVI4QnIbfwOaykSXvTQU8edXjTLW1BdMBEkZz3F/8S4EQ/Zcl1g+4PYVaHwJA+39EPvoEwmyNQFmZbMYEY0t/6Ou+iBWFGnV7+i071/Wnf4xURgdPATBV03ArOGcQdKeDv4ciPytoqIiNAkifsJMLrOk5L27eiR8igiWfUhY9ZKxCKeD3fUoFqNW2DabUay1j6JLIeyUOHgSfyWHg2oGcZOCYxUvUbXsvrcDOfQgMzPTX5JQChNDkCCEO2NMI54cBmTt6PoNr84Cpg/j7LF+ArzGIpL1Wd3Og2Sn0kCnDwXO1zjT8+dbgDEGNyxVHg8WgjBBmHgVx2pjuHVtf0gUCpFCjyAEnv81b1rSdUC+44gWy1WeupUz0LwRUk3rTZAYJumk9sn+6bqYumZ8J5O+8Fb2b/Nkex2SxY8wdXzd3MYtsgEiSKPjuj2W0iSZs/X8mzhWQbRz0K7UhMnC0CQIK28X3MYnd8IAIO0vQFs9/N/wDfIGX5NYw4HSDHiNxZRBMh40iOgwSF2T330X8QNpQCw27IBU6CLCrLpNXehbR8E+YOowJgl3LQIN/uOAEUQeEH511eQAh1YDL97Nvj8m5LiN5SXBdgKigzJ3VvKGHkGsPMay34qB8kbO+yGwNRE4vI57rwv2S2aahzcsvl0Mlu2eHgVUfolOxyJxgBgafYHZ11P32rHAyW1ShetVzctl2kR02r83Yq0981K5e/wdqyJYiiPfjJPEG8mx8iHvat6koTBs4d0eR7cCk5hQJUE+tUr1F41k3mXJTztEfpcSf8y8RkLe/E7jDV7z90eB5nIYwQhxxB2UuzOrHsKnu8vJG1h0O594Yz03lwbyhEmIR82S8zk/AHYtBv7JAyt8EIRyvpoHGTx3p+ig0Mioa5wY4BDODn4g+RruB/GhIzYM2BzNQxGsGcEqHbGFd3scE4KYM4Z1aIMI20V1KrzP04aQNG9NB0qYMQ9qFBQUONowJZGvUH/9AfL3AbufATZEcMfdygf59N8xnzPC1+dgMxad5kGQlcqzopJHczfg+nAJ4TquB0NrFXBoDfB6FGeSZFPHh38FqjNtdhR6w6492us5exYd5O/uchUPiVxuKfQhBelA+TEuU/sX6LA7cdolfyL71/X9IDZwRhA9ibE/nchuOuaODoaTDUhKECVISJKEyy37Pd4S4ZLdekoQJUjogJW+QgBHx4ya7STrzuLGHr1hKl0JougHsE4PEXFCFLblsUDizEuCseMFOP+pzfdPkBgzGuQKAYLU5UskjFKXrwTp7687kHN2vY4htSeL9b4QVzHKMt73fXh1FAki8XzJ8BqXOSSCIxW+Iu5/K0H6P3g2rnl2VteP/Mxzdfb6A6vU4tlfAK59aiiK/g85rV2y6vQ9rFmlKy/QEeE5UVdJQk2Jobg8IdErc0+J17s8fBIkyiTHK/fLoQFKDkUogCekiGMuuxStdxaSNJmyxDq4AxeFHLNHABmvKTEUuhST87Qk9FtUVoHTGWm4uCVWjt1UcigUCoVCoVAoFAqFQqFQKBQKhUKhUCgUCsV/24MDEgAAAABB/1+3I1ABAGAilVZ2IKvvzEMAAAAASUVORK5CYII=') no-repeat 0px 0px; } #f_openid { @@ -36,7 +46,7 @@

Sign In to Gerrit Code Review at example.com

-
+
Invalid OpenID identifier.
Cancel
+
+
+