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 : }
|