Remove login dialogs and replace with /login/ URLs

Insetad of handling OpenID and username/password authentication in
the GWT UI redirect all forms of authentication through /login/.

This allows the server to return normal HTML to handle the user
sign-in process. By using typical form based HTML for username
and password login browsers can offer the choice to save this
data locally in the user's password store.

Using /login/ for all authentication simplifies some UI code and
may make things easier for plugins.  A plugin only needs to do a
redirect to /login/ to upgrade an anonymous user to be an
authenticated one.

Change-Id: I0d7a59693a257c5da8bab19e3ef0a627f8a41c6e
This commit is contained in:
Shawn Pearce
2013-03-27 21:55:59 -04:00
parent 037551174b
commit 63befc1cc8
48 changed files with 714 additions and 1590 deletions

View File

@@ -0,0 +1,47 @@
// 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 java.util.Map;
final class DiscoveryResult {
static enum Status {
/** Provider was discovered and {@code providerUrl} is valid. */
VALID,
/** Identifier isn't for an OpenID provider. */
NO_PROVIDER,
/** The provider was discovered, but something else failed. */
ERROR;
}
Status status;
String providerUrl;
Map<String, String> providerArgs;
DiscoveryResult() {
}
DiscoveryResult(String redirect, Map<String, String> args) {
status = Status.VALID;
providerUrl = redirect;
providerArgs = args;
}
DiscoveryResult(Status s) {
status = s;
}
}

View File

@@ -0,0 +1,306 @@
// 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 com.google.common.base.Objects;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.auth.openid.OpenIdUrls;
import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.httpd.HtmlDomUtil;
import com.google.gerrit.reviewdb.client.AuthType;
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 org.eclipse.jgit.lib.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/** Handles OpenID based login flow. */
@SuppressWarnings("serial")
@Singleton
class LoginForm extends HttpServlet {
private static final Logger log = LoggerFactory.getLogger(LoginForm.class);
private static final ImmutableMap<String, String> ALL_PROVIDERS = ImmutableMap.of(
"google", OpenIdUrls.URL_GOOGLE,
"yahoo", OpenIdUrls.URL_YAHOO);
private final ImmutableSet<String> suggestProviders;
private final Provider<String> urlProvider;
private final OpenIdServiceImpl impl;
private final int maxRedirectUrlLength;
private final String ssoUrl;
@Inject
LoginForm(
@CanonicalWebUrl @Nullable Provider<String> urlProvider,
@GerritServerConfig Config config,
AuthConfig authConfig,
OpenIdServiceImpl impl) {
this.urlProvider = urlProvider;
this.impl = impl;
this.maxRedirectUrlLength = config.getInt(
"openid", "maxRedirectUrlLength",
10);
if (Strings.isNullOrEmpty(urlProvider.get())) {
log.error("gerrit.canonicalWebUrl must be set in gerrit.config");
}
if (authConfig.getAuthType() == AuthType.OPENID_SSO) {
suggestProviders = ImmutableSet.of();
ssoUrl = authConfig.getOpenIdSsoUrl();
} else {
Set<String> providers = Sets.newHashSet();
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 = 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 = 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;
}
discover(req, res, link, id, remember, token, mode);
}
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(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 static String getToken(HttpServletRequest req) {
String token = req.getPathInfo();
if (token == null || token.isEmpty()) {
token = PageLinks.MINE;
} else if (!token.startsWith("/")) {
token = "/" + token;
}
return token;
}
private void sendForm(HttpServletRequest req, HttpServletResponse res,
boolean link, @Nullable String errorMessage) throws IOException {
String self = req.getRequestURI();
String cancel = Objects.firstNonNull(urlProvider.get(), "/");
String token = getToken(req);
if (!token.equals("/")) {
cancel += "#" + token;
}
Document doc = HtmlDomUtil.parseFile(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());
}
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");
res.setContentLength(bin.length);
ServletOutputStream out = res.getOutputStream();
try {
out.write(bin);
} finally {
out.close();
}
}
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;
}
}

View File

@@ -14,22 +14,16 @@
package com.google.gerrit.httpd.auth.openid;
import com.google.gerrit.httpd.rpc.RpcServletModule;
import com.google.inject.servlet.ServletModule;
/** Servlets and RPC support related to OpenID authentication. */
/** Servlets related to OpenID authentication. */
public class OpenIdModule extends ServletModule {
@Override
protected void configureServlets() {
serve("/login/*").with(LoginForm.class);
serve("/" + OpenIdServiceImpl.RETURN_URL).with(OpenIdLoginServlet.class);
serve("/" + XrdsServlet.LOCATION).with(XrdsServlet.class);
filter("/").through(XrdsFilter.class);
install(new RpcServletModule(RpcServletModule.PREFIX) {
@Override
protected void configureServlets() {
rpc(OpenIdServiceImpl.class);
}
});
bind(OpenIdServiceImpl.class);
}
}

View File

@@ -15,10 +15,6 @@
package com.google.gerrit.httpd.auth.openid;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.auth.SignInMode;
import com.google.gerrit.common.auth.openid.DiscoveryResult;
import com.google.gerrit.common.auth.openid.OpenIdProviderPattern;
import com.google.gerrit.common.auth.openid.OpenIdService;
import com.google.gerrit.common.auth.openid.OpenIdUrls;
import com.google.gerrit.httpd.WebSession;
import com.google.gerrit.reviewdb.client.Account;
@@ -26,11 +22,11 @@ import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.UrlEncoded;
import com.google.gerrit.server.account.AccountException;
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.auth.openid.OpenIdProviderPattern;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gwtjsonrpc.common.AsyncCallback;
import com.google.gwtorm.client.KeyUtil;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -73,7 +69,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Singleton
class OpenIdServiceImpl implements OpenIdService {
class OpenIdServiceImpl {
private static final Logger log =
LoggerFactory.getLogger(OpenIdServiceImpl.class);
@@ -149,19 +145,12 @@ class OpenIdServiceImpl implements OpenIdService {
}
@SuppressWarnings("unchecked")
public void discover(final String openidIdentifier, final SignInMode mode,
final boolean remember, final String returnToken,
final AsyncCallback<DiscoveryResult> cb) {
if (!isAllowedOpenID(openidIdentifier)) {
cb.onSuccess(new DiscoveryResult(DiscoveryResult.Status.NOT_ALLOWED));
return;
}
DiscoveryResult discover(final String openidIdentifier, final SignInMode mode,
final boolean remember, final String returnToken) {
final State state;
state = init(openidIdentifier, mode, remember, returnToken);
if (state == null) {
cb.onSuccess(new DiscoveryResult(DiscoveryResult.Status.NO_PROVIDER));
return;
return new DiscoveryResult(DiscoveryResult.Status.NO_PROVIDER);
}
final AuthRequest aReq;
@@ -189,16 +178,15 @@ class OpenIdServiceImpl implements OpenIdService {
}
} catch (MessageException e) {
log.error("Cannot create OpenID redirect for " + openidIdentifier, e);
cb.onSuccess(new DiscoveryResult(DiscoveryResult.Status.ERROR));
return;
return new DiscoveryResult(DiscoveryResult.Status.ERROR);
} catch (ConsumerException e) {
log.error("Cannot create OpenID redirect for " + openidIdentifier, e);
cb.onSuccess(new DiscoveryResult(DiscoveryResult.Status.ERROR));
return;
return new DiscoveryResult(DiscoveryResult.Status.ERROR);
}
cb.onSuccess(new DiscoveryResult(aReq.getDestinationUrl(false), //
aReq.getParameterMap()));
return new DiscoveryResult(
aReq.getDestinationUrl(false),
aReq.getParameterMap());
}
private boolean requestRegistration(final AuthRequest aReq) {
@@ -209,7 +197,6 @@ class OpenIdServiceImpl implements OpenIdService {
// registration information, in case the identity is new to us.
//
return true;
}
// We might already have this account on file. Look for it.
@@ -222,7 +209,7 @@ class OpenIdServiceImpl implements OpenIdService {
}
}
/** Called by {@link OpenIdLoginServlet} doGet, doPost */
/** Called by {@link OpenIdLoginForm} doGet, doPost */
void doAuth(final HttpServletRequest req, final HttpServletResponse rsp)
throws Exception {
if (OMODE_CANCEL.equals(req.getParameter(OPENID_MODE))) {
@@ -436,7 +423,7 @@ class OpenIdServiceImpl implements OpenIdService {
arsp = accountManager.authenticate(areq);
final Cookie lastId = new Cookie(OpenIdUrls.LASTID_COOKIE, "");
lastId.setPath(req.getContextPath() + "/");
lastId.setPath(req.getContextPath() + "/login/");
if (remember) {
lastId.setValue(rediscoverIdentifier);
lastId.setMaxAge(LASTID_AGE);
@@ -559,7 +546,7 @@ class OpenIdServiceImpl implements OpenIdService {
return new State(discovered, retTo, contextUrl);
}
private boolean isAllowedOpenID(final String id) {
boolean isAllowedOpenID(final String id) {
for (final OpenIdProviderPattern pattern : allowedOpenIDs) {
if (pattern.matches(id)) {
return true;

View File

@@ -0,0 +1,19 @@
// Copyright (C) 2008 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;
enum SignInMode {
SIGN_IN, LINK_IDENTIY, REGISTER;
}