Remove auth token based REST API support

This never turned into a real implementation. Drop it and
look at other ways to do authentication.

Change-Id: I404c834e5e9d0d61248d28471785d825c136f3ad
This commit is contained in:
Shawn O. Pearce 2012-11-12 12:38:15 -08:00
parent f8836d734f
commit bf7fbd62bf
6 changed files with 2 additions and 445 deletions

View File

@ -44,11 +44,9 @@ public class RestApi {
private class MyRequestCallback<T extends JavaScriptObject> implements
RequestCallback {
private final boolean wasGet;
private final AsyncCallback<T> cb;
public MyRequestCallback(boolean wasGet, AsyncCallback<T> cb) {
this.wasGet = wasGet;
MyRequestCallback(AsyncCallback<T> cb) {
this.cb = cb;
}
@ -79,11 +77,6 @@ public class RestApi {
}
json = json.substring(JSON_MAGIC.length());
if (wasGet && json.startsWith("{\"_authkey\":")) {
RestApi.this.resendPost(cb, json);
return;
}
T data;
try {
// javac generics bug
@ -168,7 +161,7 @@ public class RestApi {
public <T extends JavaScriptObject> void send(final AsyncCallback<T> cb) {
RequestBuilder req = new RequestBuilder(RequestBuilder.GET, url.toString());
req.setHeader("Accept", JsonConstants.JSON_TYPE);
req.setCallback(new MyRequestCallback<T>(true, cb));
req.setCallback(new MyRequestCallback<T>(cb));
try {
RpcStatus.INSTANCE.onRpcStart();
req.send();
@ -178,21 +171,6 @@ public class RestApi {
}
}
private <T extends JavaScriptObject> void resendPost(
final AsyncCallback<T> cb, String token) {
RequestBuilder req = new RequestBuilder(RequestBuilder.POST, url.toString());
req.setHeader("Accept", JsonConstants.JSON_TYPE);
req.setHeader("Content-Type", JsonConstants.JSON_TYPE);
req.setRequestData(token);
req.setCallback(new MyRequestCallback<T>(false, cb));
try {
req.send();
} catch (RequestException e) {
RpcStatus.INSTANCE.onRpcComplete();
cb.onFailure(e);
}
}
private static boolean isJsonBody(Response res) {
return isContentType(res, JsonConstants.JSON_TYPE);
}

View File

@ -1,58 +0,0 @@
// Copyright (C) 2012 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 com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.mail.RegisterNewEmailSender;
/** Verifies the token sent by {@link RegisterNewEmailSender}. */
public interface RestTokenVerifier {
/**
* Construct a token to verify a REST PUT request.
*
* @param user the caller that wants to make a PUT request
* @param url the URL being requested
* @return an unforgeable string to send to the user as the body of a GET
* request. Presenting the string in a follow-up POST request provides
* proof the user has the ability to read messages sent to thier
* browser and they likely aren't making the request via XSRF.
*/
public String sign(Account.Id user, String url);
/**
* Decode a token previously created.
*
* @param user the user making the verify request.
* @param url the url user is attempting to access.
* @param token the string created by sign.
* @throws InvalidTokenException the token is invalid, expired, malformed,
* etc.
*/
public void verify(Account.Id user, String url, String token)
throws InvalidTokenException;
/** Exception thrown when a token does not parse correctly. */
public static class InvalidTokenException extends Exception {
private static final long serialVersionUID = 1L;
public InvalidTokenException() {
super("Invalid token");
}
public InvalidTokenException(Throwable cause) {
super("Invalid token", cause);
}
}
}

View File

@ -1,97 +0,0 @@
// Copyright (C) 2012 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 com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gwtjsonrpc.server.SignedToken;
import com.google.gwtjsonrpc.server.ValidToken;
import com.google.gwtjsonrpc.server.XsrfException;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import org.eclipse.jgit.util.Base64;
import java.io.UnsupportedEncodingException;
/** Verifies the token sent by {@link RestApiServlet}. */
public class SignedTokenRestTokenVerifier implements RestTokenVerifier {
private final SignedToken restToken;
public static class Module extends AbstractModule {
@Override
protected void configure() {
bind(RestTokenVerifier.class).to(SignedTokenRestTokenVerifier.class);
}
}
@Inject
SignedTokenRestTokenVerifier(AuthConfig config) {
restToken = config.getRestToken();
}
@Override
public String sign(Account.Id user, String url) {
try {
String payload = String.format("%s:%s", user, url);
byte[] utf8 = payload.getBytes("UTF-8");
String base64 = Base64.encodeBytes(utf8);
return restToken.newToken(base64);
} catch (XsrfException e) {
throw new IllegalArgumentException(e);
} catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException(e);
}
}
@Override
public void verify(Account.Id user, String url, String tokenString)
throws InvalidTokenException {
ValidToken token;
try {
token = restToken.checkToken(tokenString, null);
} catch (XsrfException err) {
throw new InvalidTokenException(err);
}
if (token == null || token.getData() == null || token.getData().isEmpty()) {
throw new InvalidTokenException();
}
String payload;
try {
payload = new String(Base64.decode(token.getData()), "UTF-8");
} catch (UnsupportedEncodingException err) {
throw new InvalidTokenException(err);
}
int colonPos = payload.indexOf(':');
if (colonPos == -1) {
throw new InvalidTokenException();
}
Account.Id tokenUser;
try {
tokenUser = Account.Id.parse(payload.substring(0, colonPos));
} catch (IllegalArgumentException err) {
throw new InvalidTokenException(err);
}
String tokenUrl = payload.substring(colonPos+1);
if (!tokenUser.equals(user) || !tokenUrl.equals(url)) {
throw new InvalidTokenException();
}
}
}

View File

@ -1,263 +0,0 @@
// Copyright (C) 2012 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 javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
import com.google.common.base.Strings;
import com.google.common.collect.Iterators;
import com.google.common.collect.Maps;
import com.google.gerrit.httpd.RestTokenVerifier.InvalidTokenException;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.OutputFormat;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.Enumeration;
import java.util.Map;
import javax.annotation.Nullable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
public abstract class TokenVerifiedRestApiServlet extends RestApiServlet {
private static final long serialVersionUID = 1L;
private static final String FORM_ENCODED = "application/x-www-form-urlencoded";
private static final String UTF_8 = "UTF-8";
private static final String AUTHKEY_NAME = "_authkey";
private static final String AUTHKEY_HEADER = "X-authkey";
private final Gson gson;
private final Provider<CurrentUser> userProvider;
private final RestTokenVerifier verifier;
@Inject
protected TokenVerifiedRestApiServlet(Provider<CurrentUser> userProvider,
RestTokenVerifier verifier) {
super(userProvider);
this.gson = OutputFormat.JSON_COMPACT.newGson();
this.userProvider = userProvider;
this.verifier = verifier;
}
/**
* Process the (possibly state changing) request.
*
* @param req incoming HTTP request.
* @param res outgoing response.
* @param requestData JSON object representing the HTTP request parameters.
* Null if the request body was not supplied in JSON format.
* @throws IOException
* @throws ServletException
*/
protected abstract void doRequest(HttpServletRequest req,
HttpServletResponse res,
@Nullable JsonObject requestData) throws IOException, ServletException;
@Override
protected final void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
CurrentUser user = userProvider.get();
if (!(user instanceof IdentifiedUser)) {
sendError(res, SC_UNAUTHORIZED, "API requires authentication");
return;
}
TokenInfo info = new TokenInfo();
info._authkey = verifier.sign(
((IdentifiedUser) user).getAccountId(),
computeUrl(req));
ByteArrayOutputStream buf = new ByteArrayOutputStream();
String type;
buf.write(JSON_MAGIC);
if (acceptsJson(req)) {
type = JSON_TYPE;
buf.write(gson.toJson(info).getBytes(UTF_8));
} else {
type = FORM_ENCODED;
buf.write(String.format("%s=%s",
AUTHKEY_NAME,
URLEncoder.encode(info._authkey, UTF_8)).getBytes(UTF_8));
}
res.setContentType(type);
res.setCharacterEncoding(UTF_8);
res.setHeader("Content-Disposition", "attachment");
send(req, res, buf.toByteArray());
}
@Override
protected final void doPost(HttpServletRequest req, HttpServletResponse res)
throws IOException, ServletException {
CurrentUser user = userProvider.get();
if (!(user instanceof IdentifiedUser)) {
sendError(res, SC_UNAUTHORIZED, "API requires authentication");
return;
}
ParsedBody body;
if (JSON_TYPE.equals(req.getContentType())) {
body = parseJson(req, res);
} else if (FORM_ENCODED.equals(req.getContentType())) {
body = parseForm(req, res);
} else {
sendError(res, SC_BAD_REQUEST, String.format(
"Expected Content-Type: %s or %s",
JSON_TYPE, FORM_ENCODED));
return;
}
if (body == null) {
return;
}
if (Strings.isNullOrEmpty(body._authkey)) {
String h = req.getHeader(AUTHKEY_HEADER);
if (Strings.isNullOrEmpty(h)) {
sendError(res, SC_BAD_REQUEST, String.format(
"Expected %s in request body or %s in HTTP headers",
AUTHKEY_NAME, AUTHKEY_HEADER));
return;
}
body._authkey = URLDecoder.decode(h, UTF_8);
}
try {
verifier.verify(
((IdentifiedUser) user).getAccountId(),
computeUrl(req),
body._authkey);
} catch (InvalidTokenException err) {
sendError(res, SC_BAD_REQUEST,
String.format("Invalid or expired %s", AUTHKEY_NAME));
return;
}
doRequest(body.req, res, body.json);
}
private static ParsedBody parseJson(HttpServletRequest req,
HttpServletResponse res) throws IOException {
try {
JsonElement element = new JsonParser().parse(req.getReader());
if (!element.isJsonObject()) {
sendError(res, SC_BAD_REQUEST, "Expected JSON object in request body");
return null;
}
ParsedBody body = new ParsedBody();
body.req = req;
body.json = (JsonObject) element;
JsonElement authKey = body.json.remove(AUTHKEY_NAME);
if (authKey != null
&& authKey.isJsonPrimitive()
&& authKey.getAsJsonPrimitive().isString()) {
body._authkey = authKey.getAsString();
}
return body;
} catch (JsonParseException e) {
sendError(res, SC_BAD_REQUEST, "Invalid JSON object in request body");
return null;
}
}
private static ParsedBody parseForm(HttpServletRequest req,
HttpServletResponse res) throws IOException {
ParsedBody body = new ParsedBody();
body.req = new WrappedRequest(req);
body._authkey = req.getParameter(AUTHKEY_NAME);
return body;
}
private static String computeUrl(HttpServletRequest req) {
StringBuffer url = req.getRequestURL();
String qs = req.getQueryString();
if (!Strings.isNullOrEmpty(qs)) {
url.append('?').append(qs);
}
return url.toString();
}
private static class TokenInfo {
String _authkey;
}
private static class ParsedBody {
HttpServletRequest req;
String _authkey;
JsonObject json;
}
private static class WrappedRequest extends HttpServletRequestWrapper {
@SuppressWarnings("rawtypes")
private Map parameters;
WrappedRequest(HttpServletRequest req) {
super(req);
}
@Override
public String getParameter(String name) {
if (AUTHKEY_NAME.equals(name)) {
return null;
}
return super.getParameter(name);
}
@Override
public String[] getParameterValues(String name) {
if (AUTHKEY_NAME.equals(name)) {
return null;
}
return super.getParameterValues(name);
}
@SuppressWarnings({"rawtypes", "unchecked"})
@Override
public Map getParameterMap() {
Map m = parameters;
if (m == null) {
m = super.getParameterMap();
if (m.containsKey(AUTHKEY_NAME)) {
m = Maps.newHashMap(m);
m.remove(AUTHKEY_NAME);
}
parameters = m;
}
return m;
}
@SuppressWarnings({"rawtypes", "unchecked"})
@Override
public Enumeration getParameterNames() {
return Iterators.asEnumeration(getParameterMap().keySet().iterator());
}
}
}

View File

@ -22,7 +22,6 @@ import com.google.gerrit.httpd.CacheBasedWebSession;
import com.google.gerrit.httpd.GitOverHttpModule;
import com.google.gerrit.httpd.HttpCanonicalWebUrlProvider;
import com.google.gerrit.httpd.RequestContextFilter;
import com.google.gerrit.httpd.SignedTokenRestTokenVerifier;
import com.google.gerrit.httpd.WebModule;
import com.google.gerrit.httpd.WebSshGlueModule;
import com.google.gerrit.httpd.auth.openid.OpenIdModule;
@ -295,7 +294,6 @@ public class Daemon extends SiteProgram {
modules.add(new DefaultCacheFactory.Module());
modules.add(new SmtpEmailSender.Module());
modules.add(new SignedTokenEmailTokenVerifier.Module());
modules.add(new SignedTokenRestTokenVerifier.Module());
modules.add(new PluginModule());
if (httpd) {
modules.add(new CanonicalWebUrlModule() {

View File

@ -231,7 +231,6 @@ public class WebAppInitializer extends GuiceServletContextListener {
modules.add(new DefaultCacheFactory.Module());
modules.add(new SmtpEmailSender.Module());
modules.add(new SignedTokenEmailTokenVerifier.Module());
modules.add(new SignedTokenRestTokenVerifier.Module());
modules.add(new PluginModule());
modules.add(new CanonicalWebUrlModule() {
@Override