Transmitting OAuth2 access tokens as HTTP cookies

This patch adds support for transmitting OAuth2 access
tokens as HTTP cookies instead of in Basic Authorization
headers. Native Git clients based on the libcurl HTTP client
support this mechanism out of the box (see http.cookiefile
configuration option [1]) in addition to the familiar .netrc
mechanism. The latter, however, is known to have problems with
the overly long access token strings used by some OAuth2 and
Open ID Connect providers [2]. On client side such cookies
can be stored in a text file (usually in ~/.gitcookies).

An access token cookie must have a name starting with the prefix
"git-", followed by the username. Example:

  git-user@example.org

If the username contains characters that are not commonly allowed
in cookie names (like non-ASCII and some punctuation characters),
it should be properly converted to UTF-8 and URL encoded.

The value of the cookie is the access token, followed by an at sign
("@"), followed by the identifier of the OAuth provider in the form
pluginName:providerName (same as for auth.gitOAuthProvider).
Example:

  4596bb471c2f2803940a@pluginName:providerName

If a default OAuth2 login provider was configured with
auth.gitOAuthProvider, the provider part may be omitted.
The cookie value is then just the access token.

If no cookie is found in a request that matches above form then
the access token is extracted from the Authorization header as
usual. Note that in this case the request can only be authenticated
if a default OAuth provider was configured.

[1]
https://www.kernel.org/pub/software/scm/git/docs/v1.7.10.1/git-config.html
[2]
https://groups.google.com/a/chromium.org/d/msg/chromium-os-dev/uQIZ-ltbwLM/Nx8HCoG52iwJ

Change-Id: I28f3fa1c6fb41006edc7f6f37e2dad59930783c2
Signed-off-by: Michael Ochmann <michael.ochmann@sap.com>
This commit is contained in:
Michael Ochmann
2015-10-20 16:18:51 +02:00
parent e9e046a4b3
commit 6c05d5b541

View File

@@ -41,6 +41,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Locale;
import java.util.NoSuchElementException;
@@ -50,6 +52,7 @@ import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
@@ -68,6 +71,7 @@ class ProjectOAuthFilter implements Filter {
private static final String REALM_NAME = "Gerrit Code Review";
private static final String AUTHORIZATION = "Authorization";
private static final String BASIC = "Basic ";
private static final String GIT_COOKIE_PREFIX = "git-";
private final DynamicItem<WebSession> session;
private final DynamicMap<OAuthLoginProvider> loginProviders;
@@ -120,24 +124,42 @@ class ProjectOAuthFilter implements Filter {
private boolean verify(HttpServletRequest req, Response rsp)
throws IOException {
String hdr = req.getHeader(AUTHORIZATION);
if (hdr == null || !hdr.startsWith(BASIC)) {
// Allow an anonymous connection through, or it might be using a
// session cookie instead of basic authentication.
return true;
AuthInfo authInfo = null;
// first check if there is a suitable git cookie; such cookies are
// expected to have names starting with the prefix git-
if (req.getCookies() != null) {
for (Cookie cookie: req.getCookies()) {
if (cookie.getName().startsWith(GIT_COOKIE_PREFIX)) {
authInfo = extractAuthInfo(cookie);
if (authInfo != null) {
break;
}
}
}
}
byte[] decoded = Base64.decodeBase64(hdr.substring(BASIC.length()));
String usernamePassword = new String(decoded, encoding(req));
int splitPos = usernamePassword.indexOf(':');
if (splitPos < 1) {
rsp.sendError(SC_UNAUTHORIZED);
return false;
}
// if there is no suitable git cookie fall back to Basic authentication
if (authInfo == null) {
String hdr = req.getHeader(AUTHORIZATION);
if (hdr == null || !hdr.startsWith(BASIC)) {
// Allow an anonymous connection through, or it might be using a
// session cookie instead of basic authentication.
return true;
}
AuthInfo authInfo = new AuthInfo(usernamePassword.substring(0, splitPos),
usernamePassword.substring(splitPos + 1),
defaultAuthPlugin, defaultAuthProvider);
byte[] decoded = Base64.decodeBase64(hdr.substring(BASIC.length()));
String usernamePassword = new String(decoded, encoding(req));
int splitPos = usernamePassword.indexOf(':');
if (splitPos < 1) {
rsp.sendError(SC_UNAUTHORIZED);
return false;
}
authInfo = new AuthInfo(usernamePassword.substring(0, splitPos),
usernamePassword.substring(splitPos + 1),
defaultAuthPlugin, defaultAuthProvider);
}
if (Strings.isNullOrEmpty(authInfo.tokenOrSecret)) {
rsp.sendError(SC_UNAUTHORIZED);
@@ -218,6 +240,35 @@ class ProjectOAuthFilter implements Filter {
}
}
private AuthInfo extractAuthInfo(Cookie cookie)
throws UnsupportedEncodingException {
String username = URLDecoder.decode(cookie.getName()
.substring(GIT_COOKIE_PREFIX.length()), UTF_8.name());
String value = cookie.getValue();
int splitPos = value.lastIndexOf('@');
if (splitPos < 1 || splitPos == value.length() - 1) {
// no providerId in the cookie value => assume default provider
// note: a leading/trailing at sign is considered to belong to
// the access token rather than being a separator
return new AuthInfo(username, cookie.getValue(),
defaultAuthPlugin, defaultAuthProvider);
}
String token = value.substring(0, splitPos);
String providerId = value.substring(splitPos + 1);
splitPos = providerId.lastIndexOf(':');
if (splitPos < 1 || splitPos == providerId.length() - 1) {
// no colon at all or leading/trailing colon: malformed providerId
return null;
}
String pluginName = providerId.substring(0, splitPos);
String exportName = providerId.substring(splitPos + 1);
OAuthLoginProvider provider = loginProviders.get(pluginName, exportName);
if (provider == null) {
return null;
}
return new AuthInfo(username, token, pluginName, exportName);
}
private static String encoding(HttpServletRequest req) {
return MoreObjects.firstNonNull(req.getCharacterEncoding(), UTF_8.name());
}