LCOV - code coverage report
Current view: top level - httpd/auth/openid - LoginForm.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 21 188 11.2 %
Date: 2022-11-19 15:00:39 Functions: 2 12 16.7 %

          Line data    Source code
       1             : // Copyright (C) 2009 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.httpd.auth.openid;
      16             : 
      17             : import static java.nio.charset.StandardCharsets.UTF_8;
      18             : 
      19             : import com.google.common.base.MoreObjects;
      20             : import com.google.common.base.Strings;
      21             : import com.google.common.collect.ImmutableMap;
      22             : import com.google.common.collect.ImmutableSet;
      23             : import com.google.common.flogger.FluentLogger;
      24             : import com.google.gerrit.common.Nullable;
      25             : import com.google.gerrit.common.PageLinks;
      26             : import com.google.gerrit.common.auth.openid.OpenIdUrls;
      27             : import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
      28             : import com.google.gerrit.extensions.client.AuthType;
      29             : import com.google.gerrit.extensions.registration.DynamicMap;
      30             : import com.google.gerrit.extensions.restapi.Url;
      31             : import com.google.gerrit.httpd.HtmlDomUtil;
      32             : import com.google.gerrit.httpd.LoginUrlToken;
      33             : import com.google.gerrit.httpd.template.SiteHeaderFooter;
      34             : import com.google.gerrit.server.CurrentUser;
      35             : import com.google.gerrit.server.config.AuthConfig;
      36             : import com.google.gerrit.server.config.CanonicalWebUrl;
      37             : import com.google.gerrit.server.config.GerritServerConfig;
      38             : import com.google.inject.Inject;
      39             : import com.google.inject.Provider;
      40             : import com.google.inject.Singleton;
      41             : import java.io.IOException;
      42             : import java.util.HashSet;
      43             : import java.util.Map;
      44             : import java.util.Set;
      45             : import javax.servlet.ServletOutputStream;
      46             : import javax.servlet.http.Cookie;
      47             : import javax.servlet.http.HttpServlet;
      48             : import javax.servlet.http.HttpServletRequest;
      49             : import javax.servlet.http.HttpServletResponse;
      50             : import org.eclipse.jgit.lib.Config;
      51             : import org.w3c.dom.Document;
      52             : import org.w3c.dom.Element;
      53             : 
      54             : /** Handles OpenID based login flow. */
      55             : @Singleton
      56             : class LoginForm extends HttpServlet {
      57             :   private static final long serialVersionUID = 1L;
      58             : 
      59          99 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      60             : 
      61          99 :   private static final ImmutableMap<String, String> ALL_PROVIDERS =
      62          99 :       ImmutableMap.of("launchpad", OpenIdUrls.URL_LAUNCHPAD);
      63             : 
      64             :   private final ImmutableSet<String> suggestProviders;
      65             :   private final Provider<String> urlProvider;
      66             :   private final Provider<OAuthSessionOverOpenID> oauthSessionProvider;
      67             :   private final OpenIdServiceImpl impl;
      68             :   private final int maxRedirectUrlLength;
      69             :   private final String ssoUrl;
      70             :   private final SiteHeaderFooter header;
      71             :   private final Provider<CurrentUser> currentUserProvider;
      72             :   private final DynamicMap<OAuthServiceProvider> oauthServiceProviders;
      73             : 
      74             :   @Inject
      75             :   LoginForm(
      76             :       @CanonicalWebUrl @Nullable Provider<String> urlProvider,
      77             :       @GerritServerConfig Config config,
      78             :       AuthConfig authConfig,
      79             :       OpenIdServiceImpl impl,
      80             :       SiteHeaderFooter header,
      81             :       Provider<OAuthSessionOverOpenID> oauthSessionProvider,
      82             :       Provider<CurrentUser> currentUserProvider,
      83          99 :       DynamicMap<OAuthServiceProvider> oauthServiceProviders) {
      84          99 :     this.urlProvider = urlProvider;
      85          99 :     this.impl = impl;
      86          99 :     this.header = header;
      87          99 :     this.maxRedirectUrlLength = config.getInt("openid", "maxRedirectUrlLength", 10);
      88          99 :     this.oauthSessionProvider = oauthSessionProvider;
      89          99 :     this.currentUserProvider = currentUserProvider;
      90          99 :     this.oauthServiceProviders = oauthServiceProviders;
      91             : 
      92          99 :     if (urlProvider == null || Strings.isNullOrEmpty(urlProvider.get())) {
      93           0 :       logger.atSevere().log("gerrit.canonicalWebUrl must be set in gerrit.config");
      94             :     }
      95             : 
      96          99 :     if (authConfig.getAuthType() == AuthType.OPENID_SSO) {
      97           0 :       suggestProviders = ImmutableSet.of();
      98           0 :       ssoUrl = authConfig.getOpenIdSsoUrl();
      99             :     } else {
     100          99 :       Set<String> providers = new HashSet<>();
     101          99 :       for (Map.Entry<String, String> e : ALL_PROVIDERS.entrySet()) {
     102          99 :         if (impl.isAllowedOpenID(e.getValue())) {
     103          99 :           providers.add(e.getKey());
     104             :         }
     105          99 :       }
     106          99 :       suggestProviders = ImmutableSet.copyOf(providers);
     107          99 :       ssoUrl = null;
     108             :     }
     109          99 :   }
     110             : 
     111             :   @Override
     112             :   protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
     113           0 :     if (ssoUrl != null) {
     114           0 :       String token = LoginUrlToken.getToken(req);
     115             :       SignInMode mode;
     116           0 :       if (PageLinks.REGISTER.equals(token)) {
     117           0 :         mode = SignInMode.REGISTER;
     118           0 :         token = PageLinks.MINE;
     119             :       } else {
     120           0 :         mode = SignInMode.SIGN_IN;
     121             :       }
     122           0 :       discover(req, res, false, ssoUrl, false, token, mode);
     123           0 :     } else {
     124           0 :       String id = Strings.nullToEmpty(req.getParameter("id")).trim();
     125           0 :       if (!id.isEmpty()) {
     126           0 :         doPost(req, res);
     127             :       } else {
     128           0 :         boolean link = req.getParameter("link") != null;
     129           0 :         sendForm(req, res, link, null);
     130             :       }
     131             :     }
     132           0 :   }
     133             : 
     134             :   @Override
     135             :   protected void doPost(HttpServletRequest req, HttpServletResponse res) throws IOException {
     136           0 :     boolean link = req.getParameter("link") != null;
     137           0 :     String id = Strings.nullToEmpty(req.getParameter("id")).trim();
     138           0 :     if (id.isEmpty()) {
     139           0 :       sendForm(req, res, link, null);
     140           0 :       return;
     141             :     }
     142           0 :     if (!id.startsWith("http://") && !id.startsWith("https://")) {
     143           0 :       id = "http://" + id;
     144             :     }
     145           0 :     if ((ssoUrl != null && !ssoUrl.equals(id)) || !impl.isAllowedOpenID(id)) {
     146           0 :       sendForm(req, res, link, "OpenID provider not permitted by site policy.");
     147           0 :       return;
     148             :     }
     149             : 
     150           0 :     boolean remember = "1".equals(req.getParameter("rememberme"));
     151           0 :     String token = LoginUrlToken.getToken(req);
     152             :     SignInMode mode;
     153           0 :     if (link) {
     154           0 :       mode = SignInMode.LINK_IDENTIY;
     155           0 :     } else if (PageLinks.REGISTER.equals(token)) {
     156           0 :       mode = SignInMode.REGISTER;
     157           0 :       token = PageLinks.MINE;
     158             :     } else {
     159           0 :       mode = SignInMode.SIGN_IN;
     160             :     }
     161             : 
     162           0 :     logger.atFine().log("mode \"%s\"", mode);
     163           0 :     OAuthServiceProvider oauthProvider = lookupOAuthServiceProvider(id);
     164             : 
     165           0 :     if (oauthProvider == null) {
     166           0 :       logger.atFine().log("OpenId provider \"%s\"", id);
     167           0 :       discover(req, res, link, id, remember, token, mode);
     168             :     } else {
     169           0 :       logger.atFine().log("OAuth provider \"%s\"", id);
     170           0 :       OAuthSessionOverOpenID oauthSession = oauthSessionProvider.get();
     171           0 :       if (!currentUserProvider.get().isIdentifiedUser() && oauthSession.isLoggedIn()) {
     172           0 :         oauthSession.logout();
     173             :       }
     174           0 :       if ((isGerritLogin(req) || oauthSession.isOAuthFinal(req))) {
     175           0 :         oauthSession.setServiceProvider(oauthProvider);
     176           0 :         oauthSession.setLinkMode(link);
     177           0 :         oauthSession.login(req, res, oauthProvider);
     178             :       }
     179             :     }
     180           0 :   }
     181             : 
     182             :   private void discover(
     183             :       HttpServletRequest req,
     184             :       HttpServletResponse res,
     185             :       boolean link,
     186             :       String id,
     187             :       boolean remember,
     188             :       String token,
     189             :       SignInMode mode)
     190             :       throws IOException {
     191           0 :     if (ssoUrl != null) {
     192           0 :       remember = false;
     193             :     }
     194             : 
     195           0 :     DiscoveryResult r = impl.discover(req, id, mode, remember, token);
     196           0 :     switch (r.status) {
     197             :       case VALID:
     198           0 :         redirect(r, res);
     199           0 :         break;
     200             : 
     201             :       case NO_PROVIDER:
     202           0 :         sendForm(req, res, link, "Provider is not supported, or was incorrectly entered.");
     203           0 :         break;
     204             : 
     205             :       case ERROR:
     206           0 :         sendForm(req, res, link, "Unable to connect with OpenID provider.");
     207             :         break;
     208             :     }
     209           0 :   }
     210             : 
     211             :   private void redirect(DiscoveryResult r, HttpServletResponse res) throws IOException {
     212           0 :     StringBuilder url = new StringBuilder();
     213           0 :     url.append(r.providerUrl);
     214           0 :     if (r.providerArgs != null && !r.providerArgs.isEmpty()) {
     215           0 :       boolean first = true;
     216           0 :       for (Map.Entry<String, String> arg : r.providerArgs.entrySet()) {
     217           0 :         if (first) {
     218           0 :           url.append('?');
     219           0 :           first = false;
     220             :         } else {
     221           0 :           url.append('&');
     222             :         }
     223           0 :         url.append(Url.encode(arg.getKey())).append('=').append(Url.encode(arg.getValue()));
     224           0 :       }
     225             :     }
     226           0 :     if (url.length() <= maxRedirectUrlLength) {
     227           0 :       res.sendRedirect(url.toString());
     228           0 :       return;
     229             :     }
     230             : 
     231           0 :     Document doc = HtmlDomUtil.parseFile(LoginForm.class, "RedirectForm.html");
     232           0 :     Element form = HtmlDomUtil.find(doc, "redirect_form");
     233           0 :     form.setAttribute("action", r.providerUrl);
     234           0 :     if (r.providerArgs != null && !r.providerArgs.isEmpty()) {
     235           0 :       for (Map.Entry<String, String> arg : r.providerArgs.entrySet()) {
     236           0 :         Element in = doc.createElement("input");
     237           0 :         in.setAttribute("type", "hidden");
     238           0 :         in.setAttribute("name", arg.getKey());
     239           0 :         in.setAttribute("value", arg.getValue());
     240           0 :         form.appendChild(in);
     241           0 :       }
     242             :     }
     243           0 :     sendHtml(res, doc);
     244           0 :   }
     245             : 
     246             :   private void sendForm(
     247             :       HttpServletRequest req, HttpServletResponse res, boolean link, @Nullable String errorMessage)
     248             :       throws IOException {
     249           0 :     String self = req.getRequestURI();
     250           0 :     String cancel = MoreObjects.firstNonNull(urlProvider != null ? urlProvider.get() : "/", "/");
     251           0 :     cancel += LoginUrlToken.getToken(req);
     252             : 
     253           0 :     Document doc = header.parse(LoginForm.class, "LoginForm.html");
     254           0 :     HtmlDomUtil.find(doc, "hostName").setTextContent(req.getServerName());
     255           0 :     HtmlDomUtil.find(doc, "login_form").setAttribute("action", self);
     256           0 :     HtmlDomUtil.find(doc, "cancel_link").setAttribute("href", cancel);
     257             : 
     258           0 :     if (!link || ssoUrl != null) {
     259           0 :       Element input = HtmlDomUtil.find(doc, "f_link");
     260           0 :       input.getParentNode().removeChild(input);
     261             :     }
     262             : 
     263           0 :     String last = getLastId(req);
     264           0 :     if (last != null) {
     265           0 :       HtmlDomUtil.find(doc, "f_openid").setAttribute("value", last);
     266             :     }
     267             : 
     268           0 :     Element emsg = HtmlDomUtil.find(doc, "error_message");
     269           0 :     if (Strings.isNullOrEmpty(errorMessage)) {
     270           0 :       emsg.getParentNode().removeChild(emsg);
     271             :     } else {
     272           0 :       emsg.setTextContent(errorMessage);
     273             :     }
     274             : 
     275           0 :     for (String name : ALL_PROVIDERS.keySet()) {
     276           0 :       Element div = HtmlDomUtil.find(doc, "provider_" + name);
     277           0 :       if (div == null) {
     278           0 :         continue;
     279             :       }
     280           0 :       if (!suggestProviders.contains(name)) {
     281           0 :         div.getParentNode().removeChild(div);
     282           0 :         continue;
     283             :       }
     284           0 :       Element a = HtmlDomUtil.find(div, "id_" + name);
     285           0 :       if (a == null) {
     286           0 :         div.getParentNode().removeChild(div);
     287           0 :         continue;
     288             :       }
     289           0 :       StringBuilder u = new StringBuilder();
     290           0 :       u.append(self).append(a.getAttribute("href"));
     291           0 :       if (link) {
     292           0 :         u.append("&link");
     293             :       }
     294           0 :       a.setAttribute("href", u.toString());
     295           0 :     }
     296             : 
     297             :     // OAuth: Add plugin based providers
     298           0 :     Element providers = HtmlDomUtil.find(doc, "providers");
     299           0 :     Set<String> plugins = oauthServiceProviders.plugins();
     300           0 :     for (String pluginName : plugins) {
     301           0 :       Map<String, Provider<OAuthServiceProvider>> m = oauthServiceProviders.byPlugin(pluginName);
     302           0 :       for (Map.Entry<String, Provider<OAuthServiceProvider>> e : m.entrySet()) {
     303           0 :         addProvider(providers, link, pluginName, e.getKey(), e.getValue().get().getName());
     304           0 :       }
     305           0 :     }
     306             : 
     307           0 :     sendHtml(res, doc);
     308           0 :   }
     309             : 
     310             :   private void sendHtml(HttpServletResponse res, Document doc) throws IOException {
     311           0 :     byte[] bin = HtmlDomUtil.toUTF8(doc);
     312           0 :     res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
     313           0 :     res.setContentType("text/html");
     314           0 :     res.setCharacterEncoding(UTF_8.name());
     315           0 :     res.setContentLength(bin.length);
     316           0 :     try (ServletOutputStream out = res.getOutputStream()) {
     317           0 :       out.write(bin);
     318             :     }
     319           0 :   }
     320             : 
     321             :   private static void addProvider(
     322             :       Element form, boolean link, String pluginName, String id, String serviceName) {
     323           0 :     Element div = form.getOwnerDocument().createElement("div");
     324           0 :     div.setAttribute("id", id);
     325           0 :     Element hyperlink = form.getOwnerDocument().createElement("a");
     326           0 :     StringBuilder u = new StringBuilder(String.format("?id=%s_%s", pluginName, id));
     327           0 :     if (link) {
     328           0 :       u.append("&link");
     329             :     }
     330           0 :     hyperlink.setAttribute("href", u.toString());
     331             : 
     332           0 :     hyperlink.setTextContent(serviceName + " (" + pluginName + " plugin)");
     333           0 :     div.appendChild(hyperlink);
     334           0 :     form.appendChild(div);
     335           0 :   }
     336             : 
     337             :   @Nullable
     338             :   private OAuthServiceProvider lookupOAuthServiceProvider(String providerId) {
     339           0 :     if (providerId.startsWith("http://")) {
     340           0 :       providerId = providerId.substring("http://".length());
     341             :     }
     342           0 :     Set<String> plugins = oauthServiceProviders.plugins();
     343           0 :     for (String pluginName : plugins) {
     344           0 :       Map<String, Provider<OAuthServiceProvider>> m = oauthServiceProviders.byPlugin(pluginName);
     345           0 :       for (Map.Entry<String, Provider<OAuthServiceProvider>> e : m.entrySet()) {
     346           0 :         if (providerId.equals(String.format("%s_%s", pluginName, e.getKey()))) {
     347           0 :           return e.getValue().get();
     348             :         }
     349           0 :       }
     350           0 :     }
     351           0 :     return null;
     352             :   }
     353             : 
     354             :   @Nullable
     355             :   private static String getLastId(HttpServletRequest req) {
     356           0 :     Cookie[] cookies = req.getCookies();
     357           0 :     if (cookies != null) {
     358           0 :       for (Cookie c : cookies) {
     359           0 :         if (OpenIdUrls.LASTID_COOKIE.equals(c.getName())) {
     360           0 :           return c.getValue();
     361             :         }
     362             :       }
     363             :     }
     364           0 :     return null;
     365             :   }
     366             : 
     367             :   private static boolean isGerritLogin(HttpServletRequest request) {
     368           0 :     return request.getRequestURI().contains(OAuthSessionOverOpenID.GERRIT_LOGIN);
     369             :   }
     370             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750