From bda7d76dea0346bf8643131fa40c274c832e1ae6 Mon Sep 17 00:00:00 2001 From: pastith Date: Thu, 5 Mar 2009 14:45:19 +0000 Subject: [PATCH] Add support for authenticated uploads from browser-based web apps using form POST. Essentially we just defer the authentication until we verify that it's a multipart POST with the proper parameters (Date & Authorization with the same content as the respective headers). This resulted in extracting the two main validation procedures into separate reusable methods, isTimeValid() & isSignatureValid(). --- gss/src/gr/ebs/gss/server/rest/FilesHandler.java | 93 ++++++++++++++++++-- gss/src/gr/ebs/gss/server/rest/RequestHandler.java | 64 ++++++++++++-- gss/test/rest-api-test.html | 23 +++++ 3 files changed, 166 insertions(+), 14 deletions(-) diff --git a/gss/src/gr/ebs/gss/server/rest/FilesHandler.java b/gss/src/gr/ebs/gss/server/rest/FilesHandler.java index b791628..cf8e7c0 100644 --- a/gss/src/gr/ebs/gss/server/rest/FilesHandler.java +++ b/gss/src/gr/ebs/gss/server/rest/FilesHandler.java @@ -66,6 +66,9 @@ import org.apache.commons.fileupload.FileItemStream; import org.apache.commons.fileupload.FileUploadException; import org.apache.commons.fileupload.ProgressListener; import org.apache.commons.fileupload.servlet.ServletFileUpload; +import org.apache.commons.fileupload.util.Streams; +import org.apache.commons.httpclient.util.DateParseException; +import org.apache.commons.httpclient.util.DateUtil; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.json.JSONArray; @@ -93,6 +96,16 @@ public class FilesHandler extends RequestHandler { private static final int TRACK_PROGRESS_PERCENT = 5; /** + * The form parameter name that contains the signature in a browser POST upload. + */ + private static final String AUTHORIZATION_PARAMETER = "Authorization"; + + /** + * The form parameter name that contains the date in a browser POST upload. + */ + private static final String DATE_PARAMETER = "Date"; + + /** * The logger. */ private static Log logger = LogFactory.getLog(FilesHandler.class); @@ -414,13 +427,24 @@ public class FilesHandler extends RequestHandler { * @exception IOException if an input/output error occurs */ void postResource(HttpServletRequest req, HttpServletResponse resp) throws IOException { - if (req.getParameterMap().size() > 1) { + boolean authDeferred = getAuthDeferred(req); + if (!authDeferred && req.getParameterMap().size() > 1) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST); return; } String path = getInnerPath(req, PATH_FILES); path = path.endsWith("/")? path: path + '/'; path = URLDecoder.decode(path, "UTF-8"); + // We only defer authenticating multipart POST requests. + if (authDeferred) { + if (!ServletFileUpload.isMultipartContent(req)) { + resp.sendError(HttpServletResponse.SC_FORBIDDEN); + return; + } + handleMultipart(req, resp, path); + return; + } + String newName = req.getParameter(NEW_FOLDER_PARAMETER); boolean hasUpdateParam = req.getParameterMap().containsKey(RESOURCE_UPDATE_PARAMETER); boolean hasTrashParam = req.getParameterMap().containsKey(RESOURCE_TRASH_PARAMETER); @@ -440,8 +464,6 @@ public class FilesHandler extends RequestHandler { copyResource(req, resp, path, copyTo); else if (moveTo != null) moveResource(req, resp, path, moveTo); - else if (ServletFileUpload.isMultipartContent(req)) - handleMultipart(req, resp, path); else resp.sendError(HttpServletResponse.SC_BAD_REQUEST); } @@ -460,7 +482,6 @@ public class FilesHandler extends RequestHandler { if (logger.isDebugEnabled()) logger.debug("Multipart POST for resource: " + path); - User user = getUser(request); User owner = getOwner(request); boolean exists = true; Object resource = null; @@ -510,10 +531,72 @@ public class FilesHandler extends RequestHandler { StatusProgressListener progressListener = new StatusProgressListener(getService()); upload.setProgressListener(progressListener); iter = upload.getItemIterator(request); + String dateParam = null; + String auth = null; while (iter.hasNext()) { FileItemStream item = iter.next(); + String name = item.getFieldName(); InputStream stream = item.openStream(); - if (!item.isFormField()) { + if (item.isFormField()) { + final String value = Streams.asString(stream); + if (name.equals(DATE_PARAMETER)) + dateParam = value; + else if (name.equals(AUTHORIZATION_PARAMETER)) + auth = value; + + if (logger.isDebugEnabled()) + logger.debug(name + ":" + value); + } else { + // Fetch the timestamp used to guard against replay attacks. + if (dateParam == null) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "No Date parameter"); + return; + } + + long timestamp; + try { + timestamp = DateUtil.parseDate(dateParam).getTime(); + } catch (DateParseException e) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, e.getMessage()); + return; + } + if (!isTimeValid(timestamp)) { + response.sendError(HttpServletResponse.SC_FORBIDDEN); + return; + } + + // Fetch the Authorization parameter and find the user specified in it. + if (auth == null) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "No Authorization parameter"); + return; + } + String[] authParts = auth.split(" "); + if (authParts.length != 2) { + response.sendError(HttpServletResponse.SC_FORBIDDEN); + return; + } + String username = authParts[0]; + String signature = authParts[1]; + User user = null; + try { + user = getService().findUser(username); + } catch (RpcException e) { + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path); + return; + } + if (user == null) { + response.sendError(HttpServletResponse.SC_FORBIDDEN); + return; + } + request.setAttribute(USER_ATTRIBUTE, user); + + // Validate the signature in the Authorization parameter. + String data = request.getMethod() + dateParam + URLEncoder.encode(request.getPathInfo(), "UTF-8"); + if (!isSignatureValid(signature, user, data)) { + response.sendError(HttpServletResponse.SC_FORBIDDEN); + return; + } + progressListener.setUserId(user.getId()); progressListener.setFilename(fileName); String contentType = item.getContentType(); diff --git a/gss/src/gr/ebs/gss/server/rest/RequestHandler.java b/gss/src/gr/ebs/gss/server/rest/RequestHandler.java index 3d4089d..f545cf2 100644 --- a/gss/src/gr/ebs/gss/server/rest/RequestHandler.java +++ b/gss/src/gr/ebs/gss/server/rest/RequestHandler.java @@ -199,7 +199,8 @@ public class RequestHandler extends Webdav { logger.debug("[" + method + "] " + path); if (!isRequestValid(request)) { - if (!method.equals(METHOD_GET) && !method.equals(METHOD_HEAD)) { + if (!method.equals(METHOD_GET) && !method.equals(METHOD_HEAD) && + !method.equals(METHOD_POST)) { response.sendError(HttpServletResponse.SC_FORBIDDEN); return; } @@ -403,14 +404,26 @@ public class RequestHandler extends Webdav { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + boolean authDeferred = getAuthDeferred(req); // Strip the username part String path; try { path = getUserPath(req); } catch (ObjectNotFoundException e) { + if (authDeferred) { + // We do not want to leak information if the request + // was not authenticated. + resp.sendError(HttpServletResponse.SC_FORBIDDEN); + return; + } resp.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage()); return; } + if (authDeferred && !path.startsWith(PATH_FILES)) { + // Only POST to files may be authenticated without an Authorization header. + resp.sendError(HttpServletResponse.SC_FORBIDDEN); + return; + } if (path.startsWith(PATH_OTHERS)) { resp.addHeader("Allow", methodsAllowed.get(PATH_OTHERS)); @@ -567,14 +580,10 @@ public class RequestHandler extends Webdav { } catch (IllegalArgumentException e) { return false; } - if (timestamp == -1) - return false; - Calendar cal = Calendar.getInstance(); - if (logger.isDebugEnabled()) - logger.debug("Time: server=" + cal.getTimeInMillis() + ", client=" + timestamp); - // Ignore the request if the timestamp is too far off. - if (Math.abs(timestamp - cal.getTimeInMillis()) > TIME_SKEW) + if (!isTimeValid(timestamp)) return false; + + // Fetch the Authorization header and find the user specified in it. String auth = request.getHeader(AUTHORIZATION_HEADER); String[] authParts = auth.split(" "); if (authParts.length != 2) @@ -591,14 +600,30 @@ public class RequestHandler extends Webdav { return false; request.setAttribute(USER_ATTRIBUTE, user); + + // Validate the signature in the Authorization header. String dateHeader = useGssDateHeader? request.getHeader(GSS_DATE_HEADER): - request.getHeader(DATE_HEADER); + request.getHeader(DATE_HEADER); String data; try { data = request.getMethod() + dateHeader + URLEncoder.encode(request.getPathInfo(), "UTF-8"); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } + return isSignatureValid(signature, user, data); + } + + /** + * Calculates the signature for the specified data String and then + * compares it against the provided signature. If the signatures match, + * the method returns true. Otherwise it returns false. + * + * @param signature the signature to compare against + * @param user the current user + * @param data the data to sign + * @return true if the calculated signature matches the supplied one + */ + protected boolean isSignatureValid(String signature, User user, String data) { if (logger.isDebugEnabled()) logger.debug("server pre-signing data: "+data); String serverSignature = null; @@ -627,6 +652,27 @@ public class RequestHandler extends Webdav { return true; } + /** + * A helper method that checks if the timestamp of the request + * is within TIME_SKEW milliseconds of the current time. If + * the timestamp is older (or even newer) than that, it is + * considered invalid. + * + * @param timestamp the time of the request + * @return true if the timestamp is valid + */ + protected boolean isTimeValid(long timestamp) { + if (timestamp == -1) + return false; + Calendar cal = Calendar.getInstance(); + if (logger.isDebugEnabled()) + logger.debug("Time: server=" + cal.getTimeInMillis() + ", client=" + timestamp); + // Ignore the request if the timestamp is too far off. + if (Math.abs(timestamp - cal.getTimeInMillis()) > TIME_SKEW) + return false; + return true; + } + protected boolean getAuthDeferred(HttpServletRequest req) { Boolean attr = (Boolean) req.getAttribute(AUTH_DEFERRED_ATTR); return attr == null? false: attr; diff --git a/gss/test/rest-api-test.html b/gss/test/rest-api-test.html index a852a55..58dffa5 100644 --- a/gss/test/rest-api-test.html +++ b/gss/test/rest-api-test.html @@ -17,6 +17,7 @@ function send() { var file = document.getElementById("file").value; var form = document.getElementById("form").value; var update = document.getElementById("update").value; + var formfile = document.getElementById('formfile'); var params = null; var now = (new Date()).toUTCString(); var q = resource.indexOf('?'); @@ -28,6 +29,22 @@ function send() { else if (update) params = update; + // Browser upload with POST. + if (formfile.value) { + var formdate = document.getElementById('formdate'); + var formauth = document.getElementById('formauth'); + res = resource+formfile.value; + data = 'POST' + now + encodeURIComponent(decodeURIComponent(res)); + sig = b64_hmac_sha1(atob(token), data); + formauth.value = user + " " + sig; + formdate.value = now; + var upload = document.upload; + upload.action = '/gss/rest'+res; + upload.submit(); + return; + } + + // All other API operations. var req = new XMLHttpRequest(); req.open(method, '/gss/rest'+resource, true); req.onreadystatechange = function (event) { @@ -75,6 +92,12 @@ function send() { POST form POST JSON update +
+ + +File upload + +

-- 1.7.10.4