|
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 (tatu.saloranta@iki.fi) |
| 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