| // Copyright (C) 2009 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 java.nio.charset.StandardCharsets.UTF_8; |
| |
| import com.google.common.base.MoreObjects; |
| import com.google.common.base.Strings; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.flogger.FluentLogger; |
| 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.client.AuthType; |
| 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.server.CurrentUser; |
| import com.google.gerrit.server.config.AuthConfig; |
| import com.google.gerrit.server.config.CanonicalWebUrl; |
| import com.google.gerrit.server.config.GerritServerConfig; |
| import com.google.inject.Inject; |
| import com.google.inject.Provider; |
| import com.google.inject.Singleton; |
| import java.io.IOException; |
| import java.util.HashSet; |
| import java.util.Map; |
| import java.util.Set; |
| import javax.servlet.ServletOutputStream; |
| import javax.servlet.http.Cookie; |
| import javax.servlet.http.HttpServlet; |
| import javax.servlet.http.HttpServletRequest; |
| import javax.servlet.http.HttpServletResponse; |
| import org.eclipse.jgit.lib.Config; |
| import org.w3c.dom.Document; |
| import org.w3c.dom.Element; |
| |
| /** Handles OpenID based login flow. */ |
| @Singleton |
| class LoginForm extends HttpServlet { |
| private static final long serialVersionUID = 1L; |
| |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); |
| |
| private static final ImmutableMap<String, String> ALL_PROVIDERS = |
| ImmutableMap.of("launchpad", OpenIdUrls.URL_LAUNCHPAD); |
| |
| 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( |
| @CanonicalWebUrl @Nullable Provider<String> urlProvider, |
| @GerritServerConfig Config config, |
| AuthConfig authConfig, |
| OpenIdServiceImpl impl, |
| 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())) { |
| logger.atSevere().log("gerrit.canonicalWebUrl must be set in gerrit.config"); |
| } |
| |
| if (authConfig.getAuthType() == AuthType.OPENID_SSO) { |
| suggestProviders = ImmutableSet.of(); |
| ssoUrl = authConfig.getOpenIdSsoUrl(); |
| } else { |
| Set<String> providers = new HashSet<>(); |
| for (Map.Entry<String, String> e : ALL_PROVIDERS.entrySet()) { |
| if (impl.isAllowedOpenID(e.getValue())) { |
| providers.add(e.getKey()); |
| } |
| } |
| suggestProviders = ImmutableSet.copyOf(providers); |
| ssoUrl = null; |
| } |
| } |
| |
| @Override |
| protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException { |
| if (ssoUrl != null) { |
| String token = LoginUrlToken.getToken(req); |
| SignInMode mode; |
| if (PageLinks.REGISTER.equals(token)) { |
| mode = SignInMode.REGISTER; |
| token = PageLinks.MINE; |
| } else { |
| mode = SignInMode.SIGN_IN; |
| } |
| discover(req, res, false, ssoUrl, false, token, mode); |
| } else { |
| String id = Strings.nullToEmpty(req.getParameter("id")).trim(); |
| if (!id.isEmpty()) { |
| doPost(req, res); |
| } else { |
| boolean link = req.getParameter("link") != null; |
| sendForm(req, res, link, null); |
| } |
| } |
| } |
| |
| @Override |
| protected void doPost(HttpServletRequest req, HttpServletResponse res) throws IOException { |
| boolean link = req.getParameter("link") != null; |
| String id = Strings.nullToEmpty(req.getParameter("id")).trim(); |
| if (id.isEmpty()) { |
| sendForm(req, res, link, null); |
| return; |
| } |
| if (!id.startsWith("http://") && !id.startsWith("https://")) { |
| id = "http://" + id; |
| } |
| if ((ssoUrl != null && !ssoUrl.equals(id)) || !impl.isAllowedOpenID(id)) { |
| sendForm(req, res, link, "OpenID provider not permitted by site policy."); |
| return; |
| } |
| |
| boolean remember = "1".equals(req.getParameter("rememberme")); |
| String token = LoginUrlToken.getToken(req); |
| SignInMode mode; |
| if (link) { |
| mode = SignInMode.LINK_IDENTIY; |
| } else if (PageLinks.REGISTER.equals(token)) { |
| mode = SignInMode.REGISTER; |
| token = PageLinks.MINE; |
| } else { |
| mode = SignInMode.SIGN_IN; |
| } |
| |
| logger.atFine().log("mode \"%s\"", mode); |
| OAuthServiceProvider oauthProvider = lookupOAuthServiceProvider(id); |
| |
| if (oauthProvider == null) { |
| logger.atFine().log("OpenId provider \"%s\"", id); |
| discover(req, res, link, id, remember, token, mode); |
| } else { |
| logger.atFine().log("OAuth provider \"%s\"", id); |
| OAuthSessionOverOpenID oauthSession = oauthSessionProvider.get(); |
| if (!currentUserProvider.get().isIdentifiedUser() && oauthSession.isLoggedIn()) { |
| oauthSession.logout(); |
| } |
| if ((isGerritLogin(req) || oauthSession.isOAuthFinal(req))) { |
| oauthSession.setServiceProvider(oauthProvider); |
| oauthSession.setLinkMode(link); |
| |
| @SuppressWarnings("unused") |
| var unused = oauthSession.login(req, res, oauthProvider); |
| } |
| } |
| } |
| |
| private void discover( |
| HttpServletRequest req, |
| HttpServletResponse res, |
| boolean link, |
| String id, |
| boolean remember, |
| String token, |
| SignInMode mode) |
| throws IOException { |
| if (ssoUrl != null) { |
| remember = false; |
| } |
| |
| DiscoveryResult r = impl.discover(req, id, mode, remember, token); |
| switch (r.status) { |
| case VALID: |
| redirect(r, res); |
| break; |
| |
| case NO_PROVIDER: |
| sendForm(req, res, link, "Provider is not supported, or was incorrectly entered."); |
| break; |
| |
| case ERROR: |
| sendForm(req, res, link, "Unable to connect with OpenID provider."); |
| break; |
| } |
| } |
| |
| private void redirect(DiscoveryResult r, HttpServletResponse res) throws IOException { |
| StringBuilder url = new StringBuilder(); |
| url.append(r.providerUrl); |
| if (r.providerArgs != null && !r.providerArgs.isEmpty()) { |
| boolean first = true; |
| for (Map.Entry<String, String> arg : r.providerArgs.entrySet()) { |
| if (first) { |
| url.append('?'); |
| first = false; |
| } else { |
| url.append('&'); |
| } |
| url.append(Url.encode(arg.getKey())).append('=').append(Url.encode(arg.getValue())); |
| } |
| } |
| if (url.length() <= maxRedirectUrlLength) { |
| res.sendRedirect(url.toString()); |
| return; |
| } |
| |
| Document doc = HtmlDomUtil.parseFile(LoginForm.class, "RedirectForm.html"); |
| Element form = HtmlDomUtil.find(doc, "redirect_form"); |
| form.setAttribute("action", r.providerUrl); |
| if (r.providerArgs != null && !r.providerArgs.isEmpty()) { |
| for (Map.Entry<String, String> arg : r.providerArgs.entrySet()) { |
| Element in = doc.createElement("input"); |
| in.setAttribute("type", "hidden"); |
| in.setAttribute("name", arg.getKey()); |
| in.setAttribute("value", arg.getValue()); |
| form.appendChild(in); |
| } |
| } |
| sendHtml(res, doc); |
| } |
| |
| private void sendForm( |
| HttpServletRequest req, HttpServletResponse res, boolean link, @Nullable String errorMessage) |
| throws IOException { |
| String self = req.getRequestURI(); |
| String cancel = MoreObjects.firstNonNull(urlProvider != null ? urlProvider.get() : "/", "/"); |
| cancel += LoginUrlToken.getToken(req); |
| |
| Document doc = header.parse(LoginForm.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); |
| |
| if (!link || ssoUrl != null) { |
| Element input = HtmlDomUtil.find(doc, "f_link"); |
| input.getParentNode().removeChild(input); |
| } |
| |
| String last = getLastId(req); |
| if (last != null) { |
| HtmlDomUtil.find(doc, "f_openid").setAttribute("value", last); |
| } |
| |
| Element emsg = HtmlDomUtil.find(doc, "error_message"); |
| if (Strings.isNullOrEmpty(errorMessage)) { |
| emsg.getParentNode().removeChild(emsg); |
| } else { |
| emsg.setTextContent(errorMessage); |
| } |
| |
| for (String name : ALL_PROVIDERS.keySet()) { |
| Element div = HtmlDomUtil.find(doc, "provider_" + name); |
| if (div == null) { |
| continue; |
| } |
| if (!suggestProviders.contains(name)) { |
| div.getParentNode().removeChild(div); |
| continue; |
| } |
| Element a = HtmlDomUtil.find(div, "id_" + name); |
| if (a == null) { |
| div.getParentNode().removeChild(div); |
| continue; |
| } |
| StringBuilder u = new StringBuilder(); |
| u.append(self).append(a.getAttribute("href")); |
| if (link) { |
| u.append("&link"); |
| } |
| 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, link, pluginName, e.getKey(), e.getValue().get().getName()); |
| } |
| } |
| |
| sendHtml(res, doc); |
| } |
| |
| private void sendHtml(HttpServletResponse res, Document doc) throws IOException { |
| byte[] bin = HtmlDomUtil.toUTF8(doc); |
| res.setStatus(HttpServletResponse.SC_UNAUTHORIZED); |
| res.setContentType("text/html"); |
| res.setCharacterEncoding(UTF_8.name()); |
| res.setContentLength(bin.length); |
| try (ServletOutputStream out = res.getOutputStream()) { |
| out.write(bin); |
| } |
| } |
| |
| private static void addProvider( |
| Element form, boolean link, String pluginName, String id, String serviceName) { |
| Element div = form.getOwnerDocument().createElement("div"); |
| div.setAttribute("id", id); |
| Element hyperlink = form.getOwnerDocument().createElement("a"); |
| StringBuilder u = new StringBuilder(String.format("?id=%s_%s", pluginName, id)); |
| if (link) { |
| u.append("&link"); |
| } |
| hyperlink.setAttribute("href", u.toString()); |
| |
| hyperlink.setTextContent(serviceName + " (" + pluginName + " plugin)"); |
| div.appendChild(hyperlink); |
| form.appendChild(div); |
| } |
| |
| @Nullable |
| 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; |
| } |
| |
| @Nullable |
| private static String getLastId(HttpServletRequest req) { |
| Cookie[] cookies = req.getCookies(); |
| if (cookies != null) { |
| for (Cookie c : cookies) { |
| if (OpenIdUrls.LASTID_COOKIE.equals(c.getName())) { |
| return c.getValue(); |
| } |
| } |
| } |
| return null; |
| } |
| |
| private static boolean isGerritLogin(HttpServletRequest request) { |
| return request.getRequestURI().contains(OAuthSessionOverOpenID.GERRIT_LOGIN); |
| } |
| } |