diff --git a/api/src/main/java/org/asynchttpclient/oauth/OAuthSignatureCalculator.java b/api/src/main/java/org/asynchttpclient/oauth/OAuthSignatureCalculator.java index 91f8103898..7f9ff35997 100644 --- a/api/src/main/java/org/asynchttpclient/oauth/OAuthSignatureCalculator.java +++ b/api/src/main/java/org/asynchttpclient/oauth/OAuthSignatureCalculator.java @@ -128,34 +128,33 @@ private String encodedParams(long oauthTimestamp, String nonce, List form OAuthParameterSet allParameters = new OAuthParameterSet(allParametersSize); // start with standard OAuth parameters we need - allParameters.add(KEY_OAUTH_CONSUMER_KEY, consumerAuth.getKey()); + allParameters.add(KEY_OAUTH_CONSUMER_KEY, Utf8UrlEncoder.encodeQueryElement(consumerAuth.getKey())); allParameters.add(KEY_OAUTH_NONCE, nonce); allParameters.add(KEY_OAUTH_SIGNATURE_METHOD, OAUTH_SIGNATURE_METHOD); allParameters.add(KEY_OAUTH_TIMESTAMP, String.valueOf(oauthTimestamp)); if (userAuth.getKey() != null) { - allParameters.add(KEY_OAUTH_TOKEN, userAuth.getKey()); + allParameters.add(KEY_OAUTH_TOKEN, Utf8UrlEncoder.encodeQueryElement(userAuth.getKey())); } allParameters.add(KEY_OAUTH_VERSION, OAUTH_VERSION_1_0); if (formParams != null) { for (Param param : formParams) { - allParameters.add(param.getName(), param.getValue()); + // formParams are not already encoded + allParameters.add(Utf8UrlEncoder.encodeQueryElement(param.getName()), Utf8UrlEncoder.encodeQueryElement(param.getValue())); } } if (queryParams != null) { for (Param param : queryParams) { + // queryParams are already encoded allParameters.add(param.getName(), param.getValue()); } } return allParameters.sortAndConcat(); } - /** - * Method for calculating OAuth signature using HMAC/SHA-1 method. - */ - public String calculateSignature(String method, Uri uri, long oauthTimestamp, String nonce, + StringBuilder signatureBaseString(String method, Uri uri, long oauthTimestamp, String nonce, List formParams, List queryParams) { - + // beware: must generate first as we're using pooled StringBuilder String baseUrl = baseUrl(uri); String encodedParams = encodedParams(oauthTimestamp, nonce, formParams, queryParams); @@ -169,6 +168,16 @@ public String calculateSignature(String method, Uri uri, long oauthTimestamp, St // and all that needs to be URL encoded (... again!) sb.append('&'); Utf8UrlEncoder.encodeAndAppendQueryElement(sb, encodedParams); + return sb; + } + + /** + * Method for calculating OAuth signature using HMAC/SHA-1 method. + */ + public String calculateSignature(String method, Uri uri, long oauthTimestamp, String nonce, + List formParams, List queryParams) { + + StringBuilder sb = signatureBaseString(method, uri, oauthTimestamp, nonce, formParams, queryParams); ByteBuffer rawBase = StringUtils.charSequence2ByteBuffer(sb, UTF_8); byte[] rawSignature = mac.digest(rawBase); @@ -202,7 +211,11 @@ private String constructAuthHeader(String signature, String nonce, long oauthTim return sb.toString(); } - protected synchronized String generateNonce() { + protected long generateTimestamp() { + return System.currentTimeMillis() / 1000L; + } + + protected String generateNonce() { byte[] nonceBuffer = NONCE_BUFFER.get(); ThreadLocalRandom.current().nextBytes(nonceBuffer); // let's use base64 encoding over hex, slightly more compact than hex or decimals @@ -210,10 +223,6 @@ protected synchronized String generateNonce() { // return String.valueOf(Math.abs(random.nextLong())); } - protected long generateTimestamp() { - return System.currentTimeMillis() / 1000L; - } - /** * Container for parameters used for calculating OAuth signature. * About the only confusing aspect is that of whether entries are to be sorted @@ -230,8 +239,7 @@ public OAuthParameterSet(int size) { } public OAuthParameterSet add(String key, String value) { - Parameter p = new Parameter(Utf8UrlEncoder.encodeQueryElement(key), Utf8UrlEncoder.encodeQueryElement(value)); - allParameters.add(p); + allParameters.add(new Parameter(key, value)); return this; } diff --git a/api/src/test/java/org/asynchttpclient/oauth/OAuthSignatureCalculatorTest.java b/api/src/test/java/org/asynchttpclient/oauth/OAuthSignatureCalculatorTest.java index f4d9bf9306..8790e5f0b6 100644 --- a/api/src/test/java/org/asynchttpclient/oauth/OAuthSignatureCalculatorTest.java +++ b/api/src/test/java/org/asynchttpclient/oauth/OAuthSignatureCalculatorTest.java @@ -34,8 +34,9 @@ /** * Tests the OAuth signature behavior. * - * See Signature Tester for an - * online oauth signature checker. + * See Signature Tester for an online oauth signature checker. * */ public class OAuthSignatureCalculatorTest { @@ -52,16 +53,16 @@ public class OAuthSignatureCalculatorTest { final static long TIMESTAMP = 1191242096; private static class StaticOAuthSignatureCalculator extends OAuthSignatureCalculator { - + private final long timestamp; private final String nonce; - + public StaticOAuthSignatureCalculator(ConsumerKey consumerAuth, RequestToken userAuth, long timestamp, String nonce) { super(consumerAuth, userAuth); - this.timestamp = timestamp; + this.timestamp = timestamp; this.nonce = nonce; } - + @Override protected long generateTimestamp() { return timestamp; @@ -72,7 +73,64 @@ protected String generateNonce() { return nonce; } } - + + // sample from RFC https://tools.ietf.org/html/rfc5849#section-3.4.1 + private void testSignatureBaseString(Request request) { + ConsumerKey consumer = new ConsumerKey("9djdj82h48djs9d2", CONSUMER_SECRET); + RequestToken user = new RequestToken("kkk9d7dh3k39sjv7", TOKEN_SECRET); + OAuthSignatureCalculator calc = new OAuthSignatureCalculator(consumer, user); + + String signatureBaseString = calc.signatureBaseString(// + request.getMethod(),// + request.getUri(),// + 137131201,// + "7d8f3e4a",// + request.getFormParams(),// + request.getQueryParams()).toString(); + + assertEquals(signatureBaseString, "POST&" // + + "http%3A%2F%2Fexample.com%2Frequest" // + + "&a2%3Dr%2520b%26"// + + "a3%3D2%2520q%26" + "a3%3Da%26"// + + "b5%3D%253D%25253D%26"// + + "c%2540%3D%26"// + + "c2%3D%26"// + + "oauth_consumer_key%3D9djdj82h48djs9d2%26"// + + "oauth_nonce%3D7d8f3e4a%26"// + + "oauth_signature_method%3DHMAC-SHA1%26"// + + "oauth_timestamp%3D137131201%26"// + + "oauth_token%3Dkkk9d7dh3k39sjv7%26"// + + "oauth_version%3D1.0"); + } + + @Test(groups = "fast") + public void testSignatureBaseStringWithProperlyEncodedUri() { + + Request request = new RequestBuilder("POST")// + .setUrl("http://example.com/request?b5=%3D%253D&a3=a&c%40=&a2=r%20b")// + .addFormParam("c2", "")// + .addFormParam("a3", "2 q")// + .build(); + + testSignatureBaseString(request); + } + + @Test(groups = "fast") + public void testSignatureBaseStringWithRawUri() { + + // note: @ is legal so don't decode it into %40 because it won't be + // encoded back + // note: we don't know how to fix a = that should have been encoded as + // %3D but who would be stupid enough to do that? + Request request = new RequestBuilder("POST")// + .setUrl("http://example.com/request?b5=%3D%253D&a3=a&c%40=&a2=r b")// + .addFormParam("c2", "")// + .addFormParam("a3", "2 q")// + .build(); + + testSignatureBaseString(request); + } + // based on the reference test case from // http://oauth.pbwiki.com/TestCases @Test(groups = "fast") @@ -99,16 +157,20 @@ public void testPostCalculateSignature() { formParams.add(new Param("file", "vacation.jpg")); formParams.add(new Param("size", "original")); String url = "http://photos.example.net/photos"; - final Request req = new RequestBuilder("POST") - .setUri(Uri.create(url)) - .setFormParams(formParams) - .setSignatureCalculator(calc).build(); + final Request req = new RequestBuilder("POST")// + .setUri(Uri.create(url))// + .setFormParams(formParams)// + .setSignatureCalculator(calc)// + .build(); // From the signature tester, POST should look like: - // normalized parameters: file=vacation.jpg&oauth_consumer_key=dpf43f3p2l4k3l03&oauth_nonce=kllo9940pd9333jh&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1191242096&oauth_token=nnch734d00sl2jdk&oauth_version=1.0&size=original - // signature base string: POST&http%3A%2F%2Fphotos.example.net%2Fphotos&file%3Dvacation.jpg%26oauth_consumer_key%3Ddpf43f3p2l4k3l03%26oauth_nonce%3Dkllo9940pd9333jh%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1191242096%26oauth_token%3Dnnch734d00sl2jdk%26oauth_version%3D1.0%26size%3Doriginal + // normalized parameters: + // file=vacation.jpg&oauth_consumer_key=dpf43f3p2l4k3l03&oauth_nonce=kllo9940pd9333jh&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1191242096&oauth_token=nnch734d00sl2jdk&oauth_version=1.0&size=original + // signature base string: + // POST&http%3A%2F%2Fphotos.example.net%2Fphotos&file%3Dvacation.jpg%26oauth_consumer_key%3Ddpf43f3p2l4k3l03%26oauth_nonce%3Dkllo9940pd9333jh%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1191242096%26oauth_token%3Dnnch734d00sl2jdk%26oauth_version%3D1.0%26size%3Doriginal // signature: wPkvxykrw+BTdCcGqKr+3I+PsiM= - // header: OAuth realm="",oauth_version="1.0",oauth_consumer_key="dpf43f3p2l4k3l03",oauth_token="nnch734d00sl2jdk",oauth_timestamp="1191242096",oauth_nonce="kllo9940pd9333jh",oauth_signature_method="HMAC-SHA1",oauth_signature="wPkvxykrw%2BBTdCcGqKr%2B3I%2BPsiM%3D" + // header: OAuth + // realm="",oauth_version="1.0",oauth_consumer_key="dpf43f3p2l4k3l03",oauth_token="nnch734d00sl2jdk",oauth_timestamp="1191242096",oauth_nonce="kllo9940pd9333jh",oauth_signature_method="HMAC-SHA1",oauth_signature="wPkvxykrw%2BBTdCcGqKr%2B3I%2BPsiM%3D" String authHeader = req.getHeaders().get("Authorization").get(0); Matcher m = Pattern.compile("oauth_signature=\"(.+?)\"").matcher(authHeader); @@ -135,19 +197,23 @@ public void testGetWithRequestBuilder() { queryParams.add(new Param("size", "original")); String url = "http://photos.example.net/photos"; - final Request req = new RequestBuilder("GET") - .setUri(Uri.create(url)) - .setQueryParams(queryParams) - .setSignatureCalculator(calc).build(); + final Request req = new RequestBuilder("GET")// + .setUri(Uri.create(url))// + .setQueryParams(queryParams)// + .setSignatureCalculator(calc)// + .build(); final List params = req.getQueryParams(); assertEquals(params.size(), 2); - + // From the signature tester, the URL should look like: - //normalized parameters: file=vacation.jpg&oauth_consumer_key=dpf43f3p2l4k3l03&oauth_nonce=kllo9940pd9333jh&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1191242096&oauth_token=nnch734d00sl2jdk&oauth_version=1.0&size=original - //signature base string: GET&http%3A%2F%2Fphotos.example.net%2Fphotos&file%3Dvacation.jpg%26oauth_consumer_key%3Ddpf43f3p2l4k3l03%26oauth_nonce%3Dkllo9940pd9333jh%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1191242096%26oauth_token%3Dnnch734d00sl2jdk%26oauth_version%3D1.0%26size%3Doriginal - //signature: tR3+Ty81lMeYAr/Fid0kMTYa/WM= - //Authorization header: OAuth realm="",oauth_version="1.0",oauth_consumer_key="dpf43f3p2l4k3l03",oauth_token="nnch734d00sl2jdk",oauth_timestamp="1191242096",oauth_nonce="kllo9940pd9333jh",oauth_signature_method="HMAC-SHA1",oauth_signature="tR3%2BTy81lMeYAr%2FFid0kMTYa%2FWM%3D" + // normalized parameters: + // file=vacation.jpg&oauth_consumer_key=dpf43f3p2l4k3l03&oauth_nonce=kllo9940pd9333jh&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1191242096&oauth_token=nnch734d00sl2jdk&oauth_version=1.0&size=original + // signature base string: + // GET&http%3A%2F%2Fphotos.example.net%2Fphotos&file%3Dvacation.jpg%26oauth_consumer_key%3Ddpf43f3p2l4k3l03%26oauth_nonce%3Dkllo9940pd9333jh%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1191242096%26oauth_token%3Dnnch734d00sl2jdk%26oauth_version%3D1.0%26size%3Doriginal + // signature: tR3+Ty81lMeYAr/Fid0kMTYa/WM= + // Authorization header: OAuth + // realm="",oauth_version="1.0",oauth_consumer_key="dpf43f3p2l4k3l03",oauth_token="nnch734d00sl2jdk",oauth_timestamp="1191242096",oauth_nonce="kllo9940pd9333jh",oauth_signature_method="HMAC-SHA1",oauth_signature="tR3%2BTy81lMeYAr%2FFid0kMTYa%2FWM%3D" String authHeader = req.getHeaders().get("Authorization").get(0); Matcher m = Pattern.compile("oauth_signature=\"(.+?)\"").matcher(authHeader);