diff --git a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java index 4adac3feace..7ddde7064fc 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -444,7 +444,7 @@ private AuthenticatedUser getAuthenticatedUserFromSignedUrl() { String signedUrl = httpRequest.getRequestURL().toString(); String method = httpRequest.getMethod(); - boolean validated = UrlSignerUtil.isValidUrl(signedUrl, method, user, key); + boolean validated = UrlSignerUtil.isValidUrl(signedUrl, user, method, key); if (validated){ authUser = targetUser; } diff --git a/src/main/java/edu/harvard/iq/dataverse/util/UrlSignerUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/UrlSignerUtil.java index 1da1797a8ae..b11334520e6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/UrlSignerUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/UrlSignerUtil.java @@ -17,134 +17,135 @@ */ public class UrlSignerUtil { - private static final Logger logger = Logger.getLogger(UrlSignerUtil.class.getName()); + private static final Logger logger = Logger.getLogger(UrlSignerUtil.class.getName()); - /** - * - * @param baseUrl - the URL to sign - cannot contain query params - * "until","user", "method", or "token" - * @param timeout - how many minutes to make the URL valid for (note - time skew - * between the creator and receiver could affect the validation - * @param user - a string representing the user - should be understood by the - * creator/receiver - * @param method - one of the HTTP methods - * @param key - a secret key shared by the creator/receiver. In Dataverse - * this could be an APIKey (when sending URL to a tool that will - * use it to retrieve info from Dataverse) - * @return - the signed URL - */ - public static String signUrl(String baseUrl, Integer timeout, String user, String method, String key) { - StringBuilder signedUrl = new StringBuilder(baseUrl); + /** + * + * @param baseUrl - the URL to sign - cannot contain query params + * "until","user", "method", or "token" + * @param timeout - how many minutes to make the URL valid for (note - time skew + * between the creator and receiver could affect the validation + * @param user - a string representing the user - should be understood by the + * creator/receiver + * @param method - one of the HTTP methods + * @param key - a secret key shared by the creator/receiver. In Dataverse + * this could be an APIKey (when sending URL to a tool that will + * use it to retrieve info from Dataverse) + * @return - the signed URL + */ + public static String signUrl(String baseUrl, Integer timeout, String user, String method, String key) { + StringBuilder signedUrl = new StringBuilder(baseUrl); - boolean firstParam = true; - if (baseUrl.contains("?")) { - firstParam = false; - } - if (timeout != null) { - LocalDateTime validTime = LocalDateTime.now(); - validTime = validTime.plusMinutes(timeout); - validTime.toString(); - signedUrl.append(firstParam ? "?" : "&").append("until=").append(validTime); - firstParam=false; - } - if (user != null) { - signedUrl.append(firstParam ? "?" : "&").append("user=").append(user); - firstParam=false; - } - if (method != null) { - signedUrl.append(firstParam ? "?" : "&").append("method=").append(method); - } - signedUrl.append("&token="); - logger.fine("String to sign: " + signedUrl.toString() + ""); - signedUrl.append(DigestUtils.sha512Hex(signedUrl.toString() + key)); - logger.fine("Generated Signed URL: " + signedUrl.toString()); - if (logger.isLoggable(Level.FINE)) { - logger.fine( - "URL signature is " + (isValidUrl(signedUrl.toString(), method, user, key) ? "valid" : "invalid")); - } - return signedUrl.toString(); - } + boolean firstParam = true; + if (baseUrl.contains("?")) { + firstParam = false; + } + if (timeout != null) { + LocalDateTime validTime = LocalDateTime.now(); + validTime = validTime.plusMinutes(timeout); + validTime.toString(); + signedUrl.append(firstParam ? "?" : "&").append("until=").append(validTime); + firstParam = false; + } + if (user != null) { + signedUrl.append(firstParam ? "?" : "&").append("user=").append(user); + firstParam = false; + } + if (method != null) { + signedUrl.append(firstParam ? "?" : "&").append("method=").append(method); + firstParam=false; + } + signedUrl.append(firstParam ? "?" : "&").append("token="); + logger.fine("String to sign: " + signedUrl.toString() + ""); + signedUrl.append(DigestUtils.sha512Hex(signedUrl.toString() + key)); + logger.fine("Generated Signed URL: " + signedUrl.toString()); + if (logger.isLoggable(Level.FINE)) { + logger.fine( + "URL signature is " + (isValidUrl(signedUrl.toString(), user, method, key) ? "valid" : "invalid")); + } + return signedUrl.toString(); + } - /** - * This method will only return true if the URL and parameters except the - * "token" are unchanged from the original/match the values sent to this method, - * and the "token" parameter matches what this method recalculates using the - * shared key THe method also assures that the "until" timestamp is after the - * current time. - * - * @param signedUrl - the signed URL as received from Dataverse - * @param method - an HTTP method. If provided, the method in the URL must - * match - * @param user - a string representing the user, if provided the value must - * match the one in the url - * @param key - the shared secret key to be used in validation - * @return - true if valid, false if not: e.g. the key is not the same as the - * one used to generate the "token" any part of the URL preceding the - * "token" has been altered the method doesn't match (e.g. the server - * has received a POST request and the URL only allows GET) the user - * string doesn't match (e.g. the server knows user A is logged in, but - * the URL is only for user B) the url has expired (was used after the - * until timestamp) - */ - public static boolean isValidUrl(String signedUrl, String method, String user, String key) { - boolean valid = true; - try { - URL url = new URL(signedUrl); - List params = URLEncodedUtils.parse(url.getQuery(), Charset.forName("UTF-8")); - String hash = null; - String dateString = null; - String allowedMethod = null; - String allowedUser = null; - for (NameValuePair nvp : params) { - if (nvp.getName().equals("token")) { - hash = nvp.getValue(); - logger.fine("Hash: " + hash); - } - if (nvp.getName().equals("until")) { - dateString = nvp.getValue(); - logger.fine("Until: " + dateString); - } - if (nvp.getName().equals("method")) { - allowedMethod = nvp.getValue(); - logger.fine("Method: " + allowedMethod); - } - if (nvp.getName().equals("user")) { - allowedUser = nvp.getValue(); - logger.fine("User: " + allowedUser); - } - } + /** + * This method will only return true if the URL and parameters except the + * "token" are unchanged from the original/match the values sent to this method, + * and the "token" parameter matches what this method recalculates using the + * shared key THe method also assures that the "until" timestamp is after the + * current time. + * + * @param signedUrl - the signed URL as received from Dataverse + * @param method - an HTTP method. If provided, the method in the URL must + * match + * @param user - a string representing the user, if provided the value must + * match the one in the url + * @param key - the shared secret key to be used in validation + * @return - true if valid, false if not: e.g. the key is not the same as the + * one used to generate the "token" any part of the URL preceding the + * "token" has been altered the method doesn't match (e.g. the server + * has received a POST request and the URL only allows GET) the user + * string doesn't match (e.g. the server knows user A is logged in, but + * the URL is only for user B) the url has expired (was used after the + * until timestamp) + */ + public static boolean isValidUrl(String signedUrl, String user, String method, String key) { + boolean valid = true; + try { + URL url = new URL(signedUrl); + List params = URLEncodedUtils.parse(url.getQuery(), Charset.forName("UTF-8")); + String hash = null; + String dateString = null; + String allowedMethod = null; + String allowedUser = null; + for (NameValuePair nvp : params) { + if (nvp.getName().equals("token")) { + hash = nvp.getValue(); + logger.fine("Hash: " + hash); + } + if (nvp.getName().equals("until")) { + dateString = nvp.getValue(); + logger.fine("Until: " + dateString); + } + if (nvp.getName().equals("method")) { + allowedMethod = nvp.getValue(); + logger.fine("Method: " + allowedMethod); + } + if (nvp.getName().equals("user")) { + allowedUser = nvp.getValue(); + logger.fine("User: " + allowedUser); + } + } - int index = signedUrl.indexOf("&token="); - // Assuming the token is last - doesn't have to be, but no reason for the URL - // params to be rearranged either, and this should only cause false negatives if - // it does happen - String urlToHash = signedUrl.substring(0, index + 7); - logger.fine("String to hash: " + urlToHash + ""); - String newHash = DigestUtils.sha512Hex(urlToHash + key); - logger.fine("Calculated Hash: " + newHash); - if (!hash.equals(newHash)) { - logger.fine("Hash doesn't match"); - valid = false; - } - if (dateString != null && LocalDateTime.parse(dateString).isBefore(LocalDateTime.now())) { - logger.fine("Url is expired"); - valid = false; - } - if (method != null && !method.equals(allowedMethod)) { - logger.fine("Method doesn't match"); - valid = false; - } - if (user != null && !user.equals(allowedUser)) { - logger.fine("User doesn't match"); - valid = false; - } - } catch (Throwable t) { - // Want to catch anything like null pointers, etc. to force valid=false upon any - // error - logger.warning("Bad URL: " + signedUrl + " : " + t.getMessage()); - valid = false; - } - return valid; - } + int index = signedUrl.indexOf(((dateString==null && allowedMethod==null && allowedUser==null) ? "?":"&") + "token="); + // Assuming the token is last - doesn't have to be, but no reason for the URL + // params to be rearranged either, and this should only cause false negatives if + // it does happen + String urlToHash = signedUrl.substring(0, index + 7); + logger.fine("String to hash: " + urlToHash + ""); + String newHash = DigestUtils.sha512Hex(urlToHash + key); + logger.fine("Calculated Hash: " + newHash); + if (!hash.equals(newHash)) { + logger.fine("Hash doesn't match"); + valid = false; + } + if (dateString != null && LocalDateTime.parse(dateString).isBefore(LocalDateTime.now())) { + logger.fine("Url is expired"); + valid = false; + } + if (method != null && !method.equals(allowedMethod)) { + logger.fine("Method doesn't match"); + valid = false; + } + if (user != null && !user.equals(allowedUser)) { + logger.fine("User doesn't match"); + valid = false; + } + } catch (Throwable t) { + // Want to catch anything like null pointers, etc. to force valid=false upon any + // error + logger.warning("Bad URL: " + signedUrl + " : " + t.getMessage()); + valid = false; + } + return valid; + } -} \ No newline at end of file +} diff --git a/src/test/java/edu/harvard/iq/dataverse/util/UrlSignerUtilTest.java b/src/test/java/edu/harvard/iq/dataverse/util/UrlSignerUtilTest.java new file mode 100644 index 00000000000..2b9d507758f --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/util/UrlSignerUtilTest.java @@ -0,0 +1,50 @@ +package edu.harvard.iq.dataverse.util; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.junit.Test; + +public class UrlSignerUtilTest { + + @Test + public void testSignAndValidate() { + + final String url = "http://localhost:8080/api/test1"; + final String get = "GET"; + final String post = "POST"; + + final String user1 = "Alice"; + final String user2 = "Bob"; + final int tooQuickTimeout = -1; + final int longTimeout = 1000; + final String key = "abracadabara open sesame"; + final String badkey = "abracadabara open says me"; + + Logger.getLogger(UrlSignerUtil.class.getName()).setLevel(Level.FINE); + + String signedUrl1 = UrlSignerUtil.signUrl(url, longTimeout, user1, get, key); + assertTrue(UrlSignerUtil.isValidUrl(signedUrl1, user1, get, key)); + assertTrue(UrlSignerUtil.isValidUrl(signedUrl1, user1, null, key)); + assertTrue(UrlSignerUtil.isValidUrl(signedUrl1, null, get, key)); + + assertFalse(UrlSignerUtil.isValidUrl(signedUrl1, null, get, badkey)); + assertFalse(UrlSignerUtil.isValidUrl(signedUrl1, user2, get, key)); + assertFalse(UrlSignerUtil.isValidUrl(signedUrl1, user1, post, key)); + assertFalse(UrlSignerUtil.isValidUrl(signedUrl1.replace(user1, user2), user1, get, key)); + assertFalse(UrlSignerUtil.isValidUrl(signedUrl1.replace(user1, user2), user2, get, key)); + assertFalse(UrlSignerUtil.isValidUrl(signedUrl1.replace(user1, user2), null, get, key)); + + String signedUrl2 = UrlSignerUtil.signUrl(url, null, null, null, key); + assertTrue(UrlSignerUtil.isValidUrl(signedUrl2, null, null, key)); + assertFalse(UrlSignerUtil.isValidUrl(signedUrl2, null, post, key)); + assertFalse(UrlSignerUtil.isValidUrl(signedUrl2, user1, null, key)); + + String signedUrl3 = UrlSignerUtil.signUrl(url, tooQuickTimeout, user1, get, key); + + assertFalse(UrlSignerUtil.isValidUrl(signedUrl3, user1, get, key)); + } +}