diff options
author | Saša Živkov <zivkov@gmail.com> | 2015-04-16 10:52:58 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2015-04-16 10:52:59 +0000 |
commit | a76fb517bc960a82d61bfb0acfc5d62a4d5e26ac (patch) | |
tree | c8e9b39341d4c545ab24f87a8595b30e36eac857 | |
parent | f1ac0da7c3f0e32b20de53f888233a09d4a211ae (diff) | |
parent | 8b5aa48f1da37ca35beddabcd85c306438be3fe3 (diff) |
Merge "Support hybrid OpenID and OAuth2 authentication" into stable-2.10
8 files changed, 493 insertions, 5 deletions
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<String> suggestProviders; private final Provider<String> urlProvider; + private final Provider<OAuthSessionOverOpenID> oauthSessionProvider; private final OpenIdServiceImpl impl; private final int maxRedirectUrlLength; private final String ssoUrl; private final SiteHeaderFooter header; + private final Provider<CurrentUser> currentUserProvider; + private final DynamicMap<OAuthServiceProvider> oauthServiceProviders; @Inject LoginForm( @@ -72,13 +78,19 @@ class LoginForm extends HttpServlet { @GerritServerConfig Config config, AuthConfig authConfig, OpenIdServiceImpl impl, - SiteHeaderFooter header) { + SiteHeaderFooter header, + Provider<OAuthSessionOverOpenID> oauthSessionProvider, + Provider<CurrentUser> currentUserProvider, + DynamicMap<OAuthServiceProvider> 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<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); } @@ -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<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(); + } + } + } + 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<OAuthSessionOverOpenID> oauthSession; + + @Inject + OAuthOverOpenIDLogoutServlet(AuthConfig authConfig, + DynamicItem<WebSession> webSession, + AccountManager accountManager, + @CanonicalWebUrl @Nullable Provider<String> urlProvider, + AuditService audit, + Provider<OAuthSessionOverOpenID> 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> 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> 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<CurrentUser> currentUserProvider; + private final Provider<OAuthSessionOverOpenID> oauthSessionProvider; + private final DynamicMap<OAuthServiceProvider> oauthServiceProviders; + private OAuthServiceProvider ssoProvider; + + @Inject + OAuthWebFilterOverOpenID(Provider<CurrentUser> currentUserProvider, + DynamicMap<OAuthServiceProvider> oauthServiceProviders, + Provider<OAuthSessionOverOpenID> 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<String> plugins = oauthServiceProviders.plugins(); + 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-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 @@ <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="logo_box"><div id="logo_oauth"></div><div id="logo_openid"></div></div> <div id="error_message">Invalid OpenID identifier.</div> <div> <input type="text" @@ -57,6 +67,9 @@ <a href="../" id="cancel_link">Cancel</a> </div> + <div id="providers"> + </div> + <div id="provider_launchpad"> <img height="16" width="16" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAHKSURBVCiRjZI9SFtRGIafc5tiVFCkhUxKBAcLYoPWwE0GEUPBwc0f1MHFoWLBiKABobWtBh2kacFYwUUQRXBoB3EJCVUvyBVrIB1UskTawUGFQOoP5h6H3ki8deg7vuc853zvxyuklORLDUc9QH9nZeAL0AGM+1v0cywSOVANR8uBaaALOOysDLwCYsAp8AaY97fo2RyoqOFooRqOvgUOTMiqJ8AsEA9tuH13IFkcQD1QZHpbQE+bPXEADAK5MSsA1/p+1SMAoTbF9oAh2WYUAcWbz5tThiQE1EsI7VxVzMWvHe2lGWP1RaqgWyKe1g5vjyhAHfBdrCmuWG1zqSHRAS9gFxBQC451/+PdhbqUPSYR00JSAmDLy+IE7ICwZHRkbBQKqMo3lQeW8V/KB5PAMSAtd06Kb7iQf8/vgXuA9/2V9nN7dCKjCNyABlxKmPqx7HQvDjX0HX29bBLIUSlIA4gNT9Bpw/gMtJqPaYD/2evJX5FgTQcwDpQBaeCD8+XZx8aZZFaxYZyYv/4xQS+wFAnWVAOfTAgzRrxxJpkFUHza2IVPG3sHVAMrD+zhFBgAXL0JPZIzhbXkEe+kB+j/nf52V/LexL8lvwVTCpkwGXEEfAAAAABJRU5ErkJggg=="/> <a href="?id=https://login.launchpad.net/%2Bopenid" id="id_launchpad">Sign in with a Launchpad ID</a> |