|
13 | 13 | */
|
14 | 14 | package org.asynchttpclient.oauth;
|
15 | 15 |
|
16 |
| -import static java.nio.charset.StandardCharsets.UTF_8; |
17 |
| -import static org.asynchttpclient.util.MiscUtils.isNonEmpty; |
| 16 | +import java.security.InvalidKeyException; |
| 17 | +import java.security.NoSuchAlgorithmException; |
18 | 18 |
|
19 |
| -import java.nio.ByteBuffer; |
20 |
| -import java.util.ArrayList; |
21 |
| -import java.util.Arrays; |
22 |
| -import java.util.List; |
23 |
| -import java.util.concurrent.ThreadLocalRandom; |
24 |
| -import java.util.regex.Pattern; |
25 |
| - |
26 |
| -import org.asynchttpclient.Param; |
27 | 19 | import org.asynchttpclient.Request;
|
28 | 20 | import org.asynchttpclient.RequestBuilderBase;
|
29 | 21 | import org.asynchttpclient.SignatureCalculator;
|
30 |
| -import org.asynchttpclient.uri.Uri; |
31 |
| -import org.asynchttpclient.util.Base64; |
32 |
| -import org.asynchttpclient.util.StringUtils; |
33 |
| -import org.asynchttpclient.util.Utf8UrlEncoder; |
34 | 22 |
|
35 | 23 | /**
|
36 |
| - * Simple OAuth signature calculator that can used for constructing client signatures for accessing services that use OAuth for authorization. <br> |
37 |
| - * Supports most common signature inclusion and calculation methods: HMAC-SHA1 for calculation, and Header inclusion as inclusion method. Nonce generation uses simple random |
38 |
| - * numbers with base64 encoding. |
39 |
| - * |
40 |
| - * @author tatu ([email protected]) |
| 24 | + * OAuth {@link SignatureCalculator} that delegates to {@link OAuthSignatureCalculatorInstance}s. |
41 | 25 | */
|
42 | 26 | public class OAuthSignatureCalculator implements SignatureCalculator {
|
43 |
| - public final static String HEADER_AUTHORIZATION = "Authorization"; |
44 |
| - |
45 |
| - private static final String KEY_OAUTH_CONSUMER_KEY = "oauth_consumer_key"; |
46 |
| - private static final String KEY_OAUTH_NONCE = "oauth_nonce"; |
47 |
| - private static final String KEY_OAUTH_SIGNATURE = "oauth_signature"; |
48 |
| - private static final String KEY_OAUTH_SIGNATURE_METHOD = "oauth_signature_method"; |
49 |
| - private static final String KEY_OAUTH_TIMESTAMP = "oauth_timestamp"; |
50 |
| - private static final String KEY_OAUTH_TOKEN = "oauth_token"; |
51 |
| - private static final String KEY_OAUTH_VERSION = "oauth_version"; |
52 | 27 |
|
53 |
| - private static final String OAUTH_VERSION_1_0 = "1.0"; |
54 |
| - private static final String OAUTH_SIGNATURE_METHOD = "HMAC-SHA1"; |
55 |
| - |
56 |
| - protected static final ThreadLocal<byte[]> NONCE_BUFFER = new ThreadLocal<byte[]>() { |
57 |
| - protected byte[] initialValue() { |
58 |
| - return new byte[16]; |
59 |
| - } |
| 28 | + private static final ThreadLocal<OAuthSignatureCalculatorInstance> INSTANCES = new ThreadLocal<OAuthSignatureCalculatorInstance>() { |
| 29 | + protected OAuthSignatureCalculatorInstance initialValue() { |
| 30 | + try { |
| 31 | + return new OAuthSignatureCalculatorInstance(); |
| 32 | + } catch (NoSuchAlgorithmException e) { |
| 33 | + throw new ExceptionInInitializerError(e); |
| 34 | + } |
| 35 | + }; |
60 | 36 | };
|
61 | 37 |
|
62 |
| - protected final ThreadSafeHMAC mac; |
63 |
| - |
64 |
| - protected final ConsumerKey consumerAuth; |
| 38 | + private final ConsumerKey consumerAuth; |
65 | 39 |
|
66 |
| - protected final RequestToken userAuth; |
| 40 | + private final RequestToken userAuth; |
67 | 41 |
|
68 | 42 | /**
|
69 | 43 | * @param consumerAuth Consumer key to use for signature calculation
|
70 | 44 | * @param userAuth Request/access token to use for signature calculation
|
71 | 45 | */
|
72 | 46 | public OAuthSignatureCalculator(ConsumerKey consumerAuth, RequestToken userAuth) {
|
73 |
| - mac = new ThreadSafeHMAC(consumerAuth, userAuth); |
74 | 47 | this.consumerAuth = consumerAuth;
|
75 | 48 | this.userAuth = userAuth;
|
76 | 49 | }
|
77 | 50 |
|
78 | 51 | @Override
|
79 | 52 | public void calculateAndAddSignature(Request request, RequestBuilderBase<?> requestBuilder) {
|
80 |
| - String nonce = generateNonce(); |
81 |
| - long timestamp = generateTimestamp(); |
82 |
| - String signature = calculateSignature(request, timestamp, nonce); |
83 |
| - String headerValue = constructAuthHeader(signature, nonce, timestamp); |
84 |
| - requestBuilder.setHeader(HEADER_AUTHORIZATION, headerValue); |
85 |
| - } |
86 |
| - |
87 |
| - private String encodedParams(long oauthTimestamp, String nonce, List<Param> formParams, List<Param> queryParams) { |
88 |
| - /** |
89 |
| - * List of all query and form parameters added to this request; needed for calculating request signature |
90 |
| - */ |
91 |
| - int allParametersSize = 5 + (userAuth.getKey() != null ? 1 : 0) + (formParams != null ? formParams.size() : 0) + (queryParams != null ? queryParams.size() : 0); |
92 |
| - OAuthParameterSet allParameters = new OAuthParameterSet(allParametersSize); |
93 |
| - |
94 |
| - // start with standard OAuth parameters we need |
95 |
| - allParameters.add(KEY_OAUTH_CONSUMER_KEY, Utf8UrlEncoder.percentEncodeQueryElement(consumerAuth.getKey())); |
96 |
| - allParameters.add(KEY_OAUTH_NONCE, Utf8UrlEncoder.percentEncodeQueryElement(nonce)); |
97 |
| - allParameters.add(KEY_OAUTH_SIGNATURE_METHOD, OAUTH_SIGNATURE_METHOD); |
98 |
| - allParameters.add(KEY_OAUTH_TIMESTAMP, String.valueOf(oauthTimestamp)); |
99 |
| - if (userAuth.getKey() != null) { |
100 |
| - allParameters.add(KEY_OAUTH_TOKEN, Utf8UrlEncoder.percentEncodeQueryElement(userAuth.getKey())); |
101 |
| - } |
102 |
| - allParameters.add(KEY_OAUTH_VERSION, OAUTH_VERSION_1_0); |
103 |
| - |
104 |
| - if (formParams != null) { |
105 |
| - for (Param param : formParams) { |
106 |
| - // formParams are not already encoded |
107 |
| - allParameters.add(Utf8UrlEncoder.percentEncodeQueryElement(param.getName()), Utf8UrlEncoder.percentEncodeQueryElement(param.getValue())); |
108 |
| - } |
109 |
| - } |
110 |
| - if (queryParams != null) { |
111 |
| - for (Param param : queryParams) { |
112 |
| - // queryParams are already form-url-encoded |
113 |
| - // but OAuth1 uses RFC3986_UNRESERVED_CHARS so * and + have to be encoded |
114 |
| - allParameters.add(percentEncodeAlreadyFormUrlEncoded(param.getName()), percentEncodeAlreadyFormUrlEncoded(param.getValue())); |
115 |
| - } |
116 |
| - } |
117 |
| - return allParameters.sortAndConcat(); |
118 |
| - } |
119 |
| - |
120 |
| - private String baseUrl(Uri uri) { |
121 |
| - /* |
122 |
| - * 07-Oct-2010, tatu: URL may contain default port number; if so, need to remove from base URL. |
123 |
| - */ |
124 |
| - String scheme = uri.getScheme(); |
125 |
| - |
126 |
| - StringBuilder sb = StringUtils.stringBuilder(); |
127 |
| - sb.append(scheme).append("://").append(uri.getHost()); |
128 |
| - |
129 |
| - int port = uri.getPort(); |
130 |
| - if (scheme.equals("http")) { |
131 |
| - if (port == 80) |
132 |
| - port = -1; |
133 |
| - } else if (scheme.equals("https")) { |
134 |
| - if (port == 443) |
135 |
| - port = -1; |
136 |
| - } |
137 |
| - |
138 |
| - if (port != -1) |
139 |
| - sb.append(':').append(port); |
140 |
| - |
141 |
| - if (isNonEmpty(uri.getPath())) |
142 |
| - sb.append(uri.getPath()); |
143 |
| - |
144 |
| - return sb.toString(); |
145 |
| - } |
146 |
| - |
147 |
| - private static final Pattern STAR_CHAR_PATTERN = Pattern.compile("*", Pattern.LITERAL); |
148 |
| - private static final Pattern PLUS_CHAR_PATTERN = Pattern.compile("+", Pattern.LITERAL); |
149 |
| - private static final Pattern ENCODED_TILDE_PATTERN = Pattern.compile("%7E", Pattern.LITERAL); |
150 |
| - |
151 |
| - private String percentEncodeAlreadyFormUrlEncoded(String s) { |
152 |
| - s = STAR_CHAR_PATTERN.matcher(s).replaceAll("%2A"); |
153 |
| - s = PLUS_CHAR_PATTERN.matcher(s).replaceAll("%20"); |
154 |
| - s = ENCODED_TILDE_PATTERN.matcher(s).replaceAll("~"); |
155 |
| - return s; |
156 |
| - } |
157 |
| - |
158 |
| - StringBuilder signatureBaseString(Request request, long oauthTimestamp, String nonce) { |
159 |
| - |
160 |
| - // beware: must generate first as we're using pooled StringBuilder |
161 |
| - String baseUrl = baseUrl(request.getUri()); |
162 |
| - String encodedParams = encodedParams(oauthTimestamp, nonce, request.getFormParams(), request.getQueryParams()); |
163 |
| - |
164 |
| - StringBuilder sb = StringUtils.stringBuilder(); |
165 |
| - sb.append(request.getMethod()); // POST / GET etc (nothing to URL encode) |
166 |
| - sb.append('&'); |
167 |
| - Utf8UrlEncoder.encodeAndAppendPercentEncoded(sb, baseUrl); |
168 |
| - |
169 |
| - // and all that needs to be URL encoded (... again!) |
170 |
| - sb.append('&'); |
171 |
| - Utf8UrlEncoder.encodeAndAppendPercentEncoded(sb, encodedParams); |
172 |
| - return sb; |
173 |
| - } |
174 |
| - |
175 |
| - String calculateSignature(Request request, long oauthTimestamp, String nonce) { |
176 |
| - |
177 |
| - StringBuilder sb = signatureBaseString(request, oauthTimestamp, nonce); |
178 |
| - |
179 |
| - ByteBuffer rawBase = StringUtils.charSequence2ByteBuffer(sb, UTF_8); |
180 |
| - byte[] rawSignature = mac.digest(rawBase); |
181 |
| - // and finally, base64 encoded... phew! |
182 |
| - return Base64.encode(rawSignature); |
183 |
| - } |
184 |
| - |
185 |
| - String constructAuthHeader(String signature, String nonce, long oauthTimestamp) { |
186 |
| - StringBuilder sb = StringUtils.stringBuilder(); |
187 |
| - sb.append("OAuth "); |
188 |
| - sb.append(KEY_OAUTH_CONSUMER_KEY).append("=\"").append(consumerAuth.getKey()).append("\", "); |
189 |
| - if (userAuth.getKey() != null) { |
190 |
| - sb.append(KEY_OAUTH_TOKEN).append("=\"").append(userAuth.getKey()).append("\", "); |
191 |
| - } |
192 |
| - sb.append(KEY_OAUTH_SIGNATURE_METHOD).append("=\"").append(OAUTH_SIGNATURE_METHOD).append("\", "); |
193 |
| - |
194 |
| - // careful: base64 has chars that need URL encoding: |
195 |
| - sb.append(KEY_OAUTH_SIGNATURE).append("=\""); |
196 |
| - Utf8UrlEncoder.encodeAndAppendPercentEncoded(sb, signature).append("\", "); |
197 |
| - sb.append(KEY_OAUTH_TIMESTAMP).append("=\"").append(oauthTimestamp).append("\", "); |
198 |
| - |
199 |
| - // also: nonce may contain things that need URL encoding (esp. when using base64): |
200 |
| - sb.append(KEY_OAUTH_NONCE).append("=\""); |
201 |
| - Utf8UrlEncoder.encodeAndAppendPercentEncoded(sb, nonce); |
202 |
| - sb.append("\", "); |
203 |
| - |
204 |
| - sb.append(KEY_OAUTH_VERSION).append("=\"").append(OAUTH_VERSION_1_0).append("\""); |
205 |
| - return sb.toString(); |
206 |
| - } |
207 |
| - |
208 |
| - long generateTimestamp() { |
209 |
| - return System.currentTimeMillis() / 1000L; |
210 |
| - } |
211 |
| - |
212 |
| - String generateNonce() { |
213 |
| - byte[] nonceBuffer = NONCE_BUFFER.get(); |
214 |
| - ThreadLocalRandom.current().nextBytes(nonceBuffer); |
215 |
| - // let's use base64 encoding over hex, slightly more compact than hex or decimals |
216 |
| - return Base64.encode(nonceBuffer); |
217 |
| - // return String.valueOf(Math.abs(random.nextLong())); |
218 |
| - } |
219 |
| - |
220 |
| - /** |
221 |
| - * Container for parameters used for calculating OAuth signature. About the only confusing aspect is that of whether entries are to be sorted before encoded or vice versa: if |
222 |
| - * my reading is correct, encoding is to occur first, then sorting; although this should rarely matter (since sorting is primary by key, which usually has nothing to encode)... |
223 |
| - * of course, rarely means that when it would occur it'd be harder to track down. |
224 |
| - */ |
225 |
| - final static class OAuthParameterSet { |
226 |
| - private final ArrayList<Parameter> allParameters; |
227 |
| - |
228 |
| - public OAuthParameterSet(int size) { |
229 |
| - allParameters = new ArrayList<>(size); |
230 |
| - } |
231 |
| - |
232 |
| - public OAuthParameterSet add(String key, String value) { |
233 |
| - allParameters.add(new Parameter(key, value)); |
234 |
| - return this; |
235 |
| - } |
236 |
| - |
237 |
| - public String sortAndConcat() { |
238 |
| - // then sort them (AFTER encoding, important) |
239 |
| - Parameter[] params = allParameters.toArray(new Parameter[allParameters.size()]); |
240 |
| - Arrays.sort(params); |
241 |
| - |
242 |
| - // and build parameter section using pre-encoded pieces: |
243 |
| - StringBuilder encodedParams = new StringBuilder(100); |
244 |
| - for (Parameter param : params) { |
245 |
| - if (encodedParams.length() > 0) { |
246 |
| - encodedParams.append('&'); |
247 |
| - } |
248 |
| - encodedParams.append(param.key()).append('=').append(param.value()); |
249 |
| - } |
250 |
| - return encodedParams.toString(); |
251 |
| - } |
252 |
| - } |
253 |
| - |
254 |
| - /** |
255 |
| - * Helper class for sorting query and form parameters that we need |
256 |
| - */ |
257 |
| - final static class Parameter implements Comparable<Parameter> { |
258 |
| - |
259 |
| - private final String key, value; |
260 |
| - |
261 |
| - public Parameter(String key, String value) { |
262 |
| - this.key = key; |
263 |
| - this.value = value; |
264 |
| - } |
265 |
| - |
266 |
| - public String key() { |
267 |
| - return key; |
268 |
| - } |
269 |
| - |
270 |
| - public String value() { |
271 |
| - return value; |
272 |
| - } |
273 |
| - |
274 |
| - @Override |
275 |
| - public int compareTo(Parameter other) { |
276 |
| - int diff = key.compareTo(other.key); |
277 |
| - if (diff == 0) { |
278 |
| - diff = value.compareTo(other.value); |
279 |
| - } |
280 |
| - return diff; |
281 |
| - } |
282 |
| - |
283 |
| - @Override |
284 |
| - public String toString() { |
285 |
| - return key + "=" + value; |
286 |
| - } |
287 |
| - |
288 |
| - @Override |
289 |
| - public boolean equals(Object o) { |
290 |
| - if (this == o) |
291 |
| - return true; |
292 |
| - if (o == null || getClass() != o.getClass()) |
293 |
| - return false; |
294 |
| - |
295 |
| - Parameter parameter = (Parameter) o; |
296 |
| - |
297 |
| - if (!key.equals(parameter.key)) |
298 |
| - return false; |
299 |
| - if (!value.equals(parameter.value)) |
300 |
| - return false; |
301 |
| - |
302 |
| - return true; |
303 |
| - } |
304 |
| - |
305 |
| - @Override |
306 |
| - public int hashCode() { |
307 |
| - int result = key.hashCode(); |
308 |
| - result = 31 * result + value.hashCode(); |
309 |
| - return result; |
| 53 | + try { |
| 54 | + INSTANCES.get().sign(consumerAuth, userAuth, request, requestBuilder); |
| 55 | + } catch (InvalidKeyException e) { |
| 56 | + throw new IllegalArgumentException("Failed to compute a valid key from consumer and user secrets", e); |
310 | 57 | }
|
311 | 58 | }
|
312 | 59 | }
|
0 commit comments