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:
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user