gerrit-server: use hashed passwords for HTTP.
Consequences: * Removes the GET endpoint for the HTTP password * Removes digest authentication * Removes auth.gitBasicAuth config option. With the move to NoteDB, the per-account data (including the HTTP password) will be stored in a branch in the All-Users repo, where it is subject to Gerrit ACLs. Since these are notoriously hard to setup correctly, we want to avoid storing the password in plaintext. With this change, we support hashed passwords, and a schema upgrade populates the existing 'password' field using previous passwords. Tested migration manually: * ran schema upgrade * verified that schema upgrade inserts hashed passwords with gsql. * verified that the password still works with the new code. Tested passwords manually: * verified that correct passwords get accepted when using curl --user. * verified that wrong passwords get rejected when using curl --user. Change-Id: I26f5bcd7848040107e3721eeabf75baeb79c1724
This commit is contained in:

committed by
Edwin Kempin

parent
64f54cce18
commit
84d830b5b3
@@ -42,14 +42,10 @@ public class GitOverHttpModule extends ServletModule {
|
||||
Class<? extends Filter> authFilter;
|
||||
if (authConfig.isTrustContainerAuth()) {
|
||||
authFilter = ContainerAuthFilter.class;
|
||||
} else if (authConfig.isGitBasicAuth()) {
|
||||
if (authConfig.getAuthType() == OAUTH) {
|
||||
authFilter = ProjectOAuthFilter.class;
|
||||
} else {
|
||||
authFilter = ProjectBasicAuthFilter.class;
|
||||
}
|
||||
} else if (authConfig.getAuthType() == OAUTH) {
|
||||
authFilter = ProjectOAuthFilter.class;
|
||||
} else {
|
||||
authFilter = ProjectDigestFilter.class;
|
||||
authFilter = ProjectBasicAuthFilter.class;
|
||||
}
|
||||
|
||||
if (isHttpEnabled()) {
|
||||
|
@@ -140,7 +140,7 @@ class ProjectBasicAuthFilter implements Filter {
|
||||
GitBasicAuthPolicy gitBasicAuthPolicy = authConfig.getGitBasicAuthPolicy();
|
||||
if (gitBasicAuthPolicy == GitBasicAuthPolicy.HTTP
|
||||
|| gitBasicAuthPolicy == GitBasicAuthPolicy.HTTP_LDAP) {
|
||||
if (passwordMatchesTheUserGeneratedOne(who, username, password)) {
|
||||
if (who.checkPassword(password, username)) {
|
||||
return succeedAuthentication(who);
|
||||
}
|
||||
}
|
||||
@@ -157,7 +157,7 @@ class ProjectBasicAuthFilter implements Filter {
|
||||
setUserIdentified(whoAuthResult.getAccountId());
|
||||
return true;
|
||||
} catch (NoSuchUserException e) {
|
||||
if (password.equals(who.getPassword(who.getUserName()))) {
|
||||
if (who.checkPassword(password, who.getUserName())) {
|
||||
return succeedAuthentication(who);
|
||||
}
|
||||
log.warn("Authentication failed for " + username, e);
|
||||
@@ -193,12 +193,6 @@ class ProjectBasicAuthFilter implements Filter {
|
||||
ws.setAccessPathOk(AccessPath.REST_API, true);
|
||||
}
|
||||
|
||||
private boolean passwordMatchesTheUserGeneratedOne(
|
||||
AccountState who, String username, String password) {
|
||||
String accountPassword = who.getPassword(username);
|
||||
return accountPassword != null && password != null && accountPassword.equals(password);
|
||||
}
|
||||
|
||||
private String encoding(HttpServletRequest req) {
|
||||
return MoreObjects.firstNonNull(req.getCharacterEncoding(), UTF_8.name());
|
||||
}
|
||||
|
@@ -1,337 +0,0 @@
|
||||
// Copyright (C) 2010 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;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static java.util.concurrent.TimeUnit.HOURS;
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
|
||||
|
||||
import com.google.gerrit.common.Nullable;
|
||||
import com.google.gerrit.extensions.registration.DynamicItem;
|
||||
import com.google.gerrit.server.AccessPath;
|
||||
import com.google.gerrit.server.account.AccountCache;
|
||||
import com.google.gerrit.server.account.AccountState;
|
||||
import com.google.gerrit.server.config.CanonicalWebUrl;
|
||||
import com.google.gerrit.server.config.GerritServerConfig;
|
||||
import com.google.gwtjsonrpc.server.SignedToken;
|
||||
import com.google.gwtjsonrpc.server.XsrfException;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
import com.google.inject.Singleton;
|
||||
import java.io.IOException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import javax.servlet.Filter;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.FilterConfig;
|
||||
import javax.servlet.ServletContext;
|
||||
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.HttpServletResponseWrapper;
|
||||
import org.eclipse.jgit.lib.Config;
|
||||
|
||||
/**
|
||||
* Authenticates the current user by HTTP digest authentication.
|
||||
*
|
||||
* <p>The current HTTP request is authenticated by looking up the username from the Authorization
|
||||
* header and checking the digest response against the stored password. This filter is intended only
|
||||
* to protect the {@link GitOverHttpServlet} and its handled URLs, which provide remote repository
|
||||
* access over HTTP.
|
||||
*
|
||||
* @see <a href="http://www.ietf.org/rfc/rfc2617.txt">RFC 2617</a>
|
||||
*/
|
||||
@Singleton
|
||||
class ProjectDigestFilter implements Filter {
|
||||
public static final String REALM_NAME = "Gerrit Code Review";
|
||||
private static final String AUTHORIZATION = "Authorization";
|
||||
|
||||
private final Provider<String> urlProvider;
|
||||
private final DynamicItem<WebSession> session;
|
||||
private final AccountCache accountCache;
|
||||
private final Config config;
|
||||
private final SignedToken tokens;
|
||||
private ServletContext context;
|
||||
|
||||
@Inject
|
||||
ProjectDigestFilter(
|
||||
@CanonicalWebUrl @Nullable Provider<String> urlProvider,
|
||||
DynamicItem<WebSession> session,
|
||||
AccountCache accountCache,
|
||||
@GerritServerConfig Config config)
|
||||
throws XsrfException {
|
||||
this.urlProvider = urlProvider;
|
||||
this.session = session;
|
||||
this.accountCache = accountCache;
|
||||
this.config = config;
|
||||
this.tokens = new SignedToken((int) SECONDS.convert(1, HOURS));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(FilterConfig config) {
|
||||
context = config.getServletContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {}
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
HttpServletRequest req = (HttpServletRequest) request;
|
||||
Response rsp = new Response(req, (HttpServletResponse) response);
|
||||
|
||||
if (verify(req, rsp)) {
|
||||
chain.doFilter(req, rsp);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean verify(HttpServletRequest req, Response rsp) throws IOException {
|
||||
final String hdr = req.getHeader(AUTHORIZATION);
|
||||
if (hdr == null || !hdr.startsWith("Digest ")) {
|
||||
// Allow an anonymous connection through, or it might be using a
|
||||
// session cookie instead of digest authentication.
|
||||
return true;
|
||||
}
|
||||
|
||||
final Map<String, String> p = parseAuthorization(hdr);
|
||||
final String user = p.get("username");
|
||||
final String realm = p.get("realm");
|
||||
final String nonce = p.get("nonce");
|
||||
final String uri = p.get("uri");
|
||||
final String response = p.get("response");
|
||||
final String qop = p.get("qop");
|
||||
final String nc = p.get("nc");
|
||||
final String cnonce = p.get("cnonce");
|
||||
final String method = req.getMethod();
|
||||
|
||||
if (user == null //
|
||||
|| realm == null //
|
||||
|| nonce == null //
|
||||
|| uri == null //
|
||||
|| response == null //
|
||||
|| !"auth".equals(qop) //
|
||||
|| !REALM_NAME.equals(realm)) {
|
||||
context.log("Invalid header: " + AUTHORIZATION + ": " + hdr);
|
||||
rsp.sendError(SC_FORBIDDEN);
|
||||
return false;
|
||||
}
|
||||
|
||||
String username = user;
|
||||
if (config.getBoolean("auth", "userNameToLowerCase", false)) {
|
||||
username = username.toLowerCase(Locale.US);
|
||||
}
|
||||
|
||||
final AccountState who = accountCache.getByUsername(username);
|
||||
if (who == null || !who.getAccount().isActive()) {
|
||||
rsp.sendError(SC_UNAUTHORIZED);
|
||||
return false;
|
||||
}
|
||||
|
||||
final String passwd = who.getPassword(username);
|
||||
if (passwd == null) {
|
||||
rsp.sendError(SC_UNAUTHORIZED);
|
||||
return false;
|
||||
}
|
||||
|
||||
final String A1 = user + ":" + realm + ":" + passwd;
|
||||
final String A2 = method + ":" + uri;
|
||||
final String expect = KD(H(A1), nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + H(A2));
|
||||
|
||||
if (expect.equals(response)) {
|
||||
try {
|
||||
if (tokens.checkToken(nonce, "") != null) {
|
||||
WebSession ws = session.get();
|
||||
ws.setUserAccountId(who.getAccount().getId());
|
||||
ws.setAccessPathOk(AccessPath.GIT, true);
|
||||
ws.setAccessPathOk(AccessPath.REST_API, true);
|
||||
return true;
|
||||
}
|
||||
rsp.stale = true;
|
||||
rsp.sendError(SC_UNAUTHORIZED);
|
||||
return false;
|
||||
} catch (XsrfException e) {
|
||||
context.log("Error validating nonce for digest authentication", e);
|
||||
rsp.sendError(SC_INTERNAL_SERVER_ERROR);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
rsp.sendError(SC_UNAUTHORIZED);
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String H(String data) {
|
||||
MessageDigest md = newMD5();
|
||||
md.update(data.getBytes(UTF_8));
|
||||
return LHEX(md.digest());
|
||||
}
|
||||
|
||||
private static String KD(String secret, String data) {
|
||||
MessageDigest md = newMD5();
|
||||
md.update(secret.getBytes(UTF_8));
|
||||
md.update((byte) ':');
|
||||
md.update(data.getBytes(UTF_8));
|
||||
return LHEX(md.digest());
|
||||
}
|
||||
|
||||
private static MessageDigest newMD5() {
|
||||
try {
|
||||
return MessageDigest.getInstance("MD5");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("No MD5 available", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static final char[] LHEX = {
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', //
|
||||
'a', 'b', 'c', 'd', 'e', 'f',
|
||||
};
|
||||
|
||||
private static String LHEX(byte[] bin) {
|
||||
StringBuilder r = new StringBuilder(bin.length * 2);
|
||||
for (byte b : bin) {
|
||||
r.append(LHEX[(b >>> 4) & 0x0f]);
|
||||
r.append(LHEX[b & 0x0f]);
|
||||
}
|
||||
return r.toString();
|
||||
}
|
||||
|
||||
private Map<String, String> parseAuthorization(String auth) {
|
||||
Map<String, String> p = new HashMap<>();
|
||||
int next = "Digest ".length();
|
||||
while (next < auth.length()) {
|
||||
if (next < auth.length() && auth.charAt(next) == ',') {
|
||||
next++;
|
||||
}
|
||||
while (next < auth.length() && Character.isWhitespace(auth.charAt(next))) {
|
||||
next++;
|
||||
}
|
||||
|
||||
int eq = auth.indexOf('=', next);
|
||||
if (eq < 0 || eq + 1 == auth.length()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
final String name = auth.substring(next, eq);
|
||||
final String value;
|
||||
if (auth.charAt(eq + 1) == '"') {
|
||||
int dq = auth.indexOf('"', eq + 2);
|
||||
if (dq < 0) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
value = auth.substring(eq + 2, dq);
|
||||
next = dq + 1;
|
||||
|
||||
} else {
|
||||
int space = auth.indexOf(' ', eq + 1);
|
||||
int comma = auth.indexOf(',', eq + 1);
|
||||
if (space < 0) {
|
||||
space = auth.length();
|
||||
}
|
||||
if (comma < 0) {
|
||||
comma = auth.length();
|
||||
}
|
||||
|
||||
final int e = Math.min(space, comma);
|
||||
value = auth.substring(eq + 1, e);
|
||||
next = e + 1;
|
||||
}
|
||||
p.put(name, value);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
private String newNonce() {
|
||||
try {
|
||||
return tokens.newToken("");
|
||||
} catch (XsrfException e) {
|
||||
throw new RuntimeException("Cannot generate new nonce", e);
|
||||
}
|
||||
}
|
||||
|
||||
class Response extends HttpServletResponseWrapper {
|
||||
private static final String WWW_AUTHENTICATE = "WWW-Authenticate";
|
||||
private final HttpServletRequest req;
|
||||
Boolean stale;
|
||||
|
||||
Response(HttpServletRequest req, HttpServletResponse rsp) {
|
||||
super(rsp);
|
||||
this.req = req;
|
||||
}
|
||||
|
||||
private void status(int sc) {
|
||||
if (sc == SC_UNAUTHORIZED) {
|
||||
StringBuilder v = new StringBuilder();
|
||||
v.append("Digest");
|
||||
v.append(" realm=\"").append(REALM_NAME).append("\"");
|
||||
|
||||
String url = urlProvider.get();
|
||||
if (url == null) {
|
||||
url = req.getContextPath();
|
||||
if (url != null && !url.isEmpty() && !url.endsWith("/")) {
|
||||
url += "/";
|
||||
}
|
||||
}
|
||||
if (url != null && !url.isEmpty()) {
|
||||
v.append(", domain=\"").append(url).append("\"");
|
||||
}
|
||||
|
||||
v.append(", qop=\"auth\"");
|
||||
if (stale != null) {
|
||||
v.append(", stale=").append(stale);
|
||||
}
|
||||
v.append(", nonce=\"").append(newNonce()).append("\"");
|
||||
setHeader(WWW_AUTHENTICATE, v.toString());
|
||||
|
||||
} else if (containsHeader(WWW_AUTHENTICATE)) {
|
||||
setHeader(WWW_AUTHENTICATE, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendError(int sc, String msg) throws IOException {
|
||||
status(sc);
|
||||
super.sendError(sc, msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendError(int sc) throws IOException {
|
||||
status(sc);
|
||||
super.sendError(sc);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public void setStatus(int sc, String sm) {
|
||||
status(sc);
|
||||
super.setStatus(sc, sm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStatus(int sc) {
|
||||
status(sc);
|
||||
super.setStatus(sc);
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user