/*
* Copyright (c) 2017 AsyncHttpClient Project. All rights reserved.
*
* This program is licensed to you under the Apache License Version 2.0,
* and you may not use this file except in compliance with the Apache License Version 2.0.
* You may obtain a copy of the Apache License Version 2.0 at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the Apache License Version 2.0 is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
*/
package org.asynchttpclient.oauth;
import static java.nio.charset.StandardCharsets.UTF_8;
import io.netty.handler.codec.http.HttpHeaderNames;
import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.regex.Pattern;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.asynchttpclient.Param;
import org.asynchttpclient.Request;
import org.asynchttpclient.RequestBuilderBase;
import org.asynchttpclient.SignatureCalculator;
import org.asynchttpclient.util.Base64;
import org.asynchttpclient.util.StringBuilderPool;
import org.asynchttpclient.util.StringUtils;
import org.asynchttpclient.util.Utf8UrlEncoder;
/**
* Non thread-safe {@link SignatureCalculator} for OAuth1.
*
* Supports most common signature inclusion and calculation methods: HMAC-SHA1 for calculation, and Header inclusion as inclusion method. Nonce generation uses simple random
* numbers with base64 encoding.
*/
class OAuthSignatureCalculatorInstance {
private static final Pattern STAR_CHAR_PATTERN = Pattern.compile("*", Pattern.LITERAL);
private static final Pattern PLUS_CHAR_PATTERN = Pattern.compile("+", Pattern.LITERAL);
private static final Pattern ENCODED_TILDE_PATTERN = Pattern.compile("%7E", Pattern.LITERAL);
private static final String KEY_OAUTH_CONSUMER_KEY = "oauth_consumer_key";
private static final String KEY_OAUTH_NONCE = "oauth_nonce";
private static final String KEY_OAUTH_SIGNATURE = "oauth_signature";
private static final String KEY_OAUTH_SIGNATURE_METHOD = "oauth_signature_method";
private static final String KEY_OAUTH_TIMESTAMP = "oauth_timestamp";
private static final String KEY_OAUTH_TOKEN = "oauth_token";
private static final String KEY_OAUTH_VERSION = "oauth_version";
private static final String OAUTH_VERSION_1_0 = "1.0";
private static final String OAUTH_SIGNATURE_METHOD = "HMAC-SHA1";
private static final String HMAC_SHA1_ALGORITHM = "HmacSHA1";
private final Mac mac;
private final byte[] nonceBuffer = new byte[16];
private final Parameters parameters = new Parameters();
public OAuthSignatureCalculatorInstance() throws NoSuchAlgorithmException {
mac = Mac.getInstance(HMAC_SHA1_ALGORITHM);
}
public void sign(ConsumerKey consumerAuth, RequestToken userAuth, Request request, RequestBuilderBase<?> requestBuilder) throws InvalidKeyException {
String nonce = generateNonce();
long timestamp = generateTimestamp();
sign(consumerAuth, userAuth, request, requestBuilder, nonce, timestamp);
}
private String generateNonce() {
ThreadLocalRandom.current().nextBytes(nonceBuffer);
// let's use base64 encoding over hex, slightly more compact than hex or decimals
return Base64.encode(nonceBuffer);
}
private static long generateTimestamp() {
return System.currentTimeMillis() / 1000L;
}
void sign(ConsumerKey consumerAuth, RequestToken userAuth, Request request, RequestBuilderBase<?> requestBuilder, String nonce, long timestamp)
throws InvalidKeyException {
String signature = calculateSignature(consumerAuth, userAuth, request, timestamp, nonce);
String headerValue = constructAuthHeader(consumerAuth, userAuth, signature, nonce, timestamp);
requestBuilder.setHeader(HttpHeaderNames.AUTHORIZATION, headerValue);
}
String calculateSignature(ConsumerKey consumerAuth, RequestToken userAuth, Request request, long oauthTimestamp, String nonce) throws InvalidKeyException {
StringBuilder sb = signatureBaseString(consumerAuth, userAuth, request, oauthTimestamp, nonce);
ByteBuffer rawBase = StringUtils.charSequence2ByteBuffer(sb, UTF_8);
byte[] rawSignature = digest(consumerAuth, userAuth, rawBase);
// and finally, base64 encoded... phew!
return Base64.encode(rawSignature);
}
StringBuilder signatureBaseString(ConsumerKey consumerAuth, RequestToken userAuth, Request request, long oauthTimestamp, String nonce) {
// beware: must generate first as we're using pooled StringBuilder
String baseUrl = request.getUri().toBaseUrl();
String encodedParams = encodedParams(consumerAuth, userAuth, oauthTimestamp, nonce, request.getFormParams(), request.getQueryParams());
StringBuilder sb = StringBuilderPool.DEFAULT.stringBuilder();
sb.append(request.getMethod()); // POST / GET etc (nothing to URL encode)
sb.append('&');
Utf8UrlEncoder.encodeAndAppendPercentEncoded(sb, baseUrl);
// and all that needs to be URL encoded (... again!)
sb.append('&');
Utf8UrlEncoder.encodeAndAppendPercentEncoded(sb, encodedParams);
return sb;
}
private String encodedParams(ConsumerKey consumerAuth, RequestToken userAuth, long oauthTimestamp, String nonce, List<Param> formParams, List<Param> queryParams) {
parameters.reset();
/**
* List of all query and form parameters added to this request; needed for calculating request signature
*/
// start with standard OAuth parameters we need
parameters.add(KEY_OAUTH_CONSUMER_KEY, Utf8UrlEncoder.percentEncodeQueryElement(consumerAuth.getKey()))
.add(KEY_OAUTH_NONCE, Utf8UrlEncoder.percentEncodeQueryElement(nonce)).add(KEY_OAUTH_SIGNATURE_METHOD, OAUTH_SIGNATURE_METHOD)
.add(KEY_OAUTH_TIMESTAMP, String.valueOf(oauthTimestamp));
if (userAuth.getKey() != null) {
parameters.add(KEY_OAUTH_TOKEN, Utf8UrlEncoder.percentEncodeQueryElement(userAuth.getKey()));
}
parameters.add(KEY_OAUTH_VERSION, OAUTH_VERSION_1_0);
if (formParams != null) {
for (Param param : formParams) {
// formParams are not already encoded
parameters.add(Utf8UrlEncoder.percentEncodeQueryElement(param.getName()), Utf8UrlEncoder.percentEncodeQueryElement(param.getValue()));
}
}
if (queryParams != null) {
for (Param param : queryParams) {
// queryParams are already form-url-encoded
// but OAuth1 uses RFC3986_UNRESERVED_CHARS so * and + have to be encoded
parameters.add(percentEncodeAlreadyFormUrlEncoded(param.getName()), percentEncodeAlreadyFormUrlEncoded(param.getValue()));
}
}
return parameters.sortAndConcat();
}
private String percentEncodeAlreadyFormUrlEncoded(String s) {
s = STAR_CHAR_PATTERN.matcher(s).replaceAll("%2A");
s = PLUS_CHAR_PATTERN.matcher(s).replaceAll("%20");
s = ENCODED_TILDE_PATTERN.matcher(s).replaceAll("~");
return s;
}
private byte[] digest(ConsumerKey consumerAuth, RequestToken userAuth, ByteBuffer message) throws InvalidKeyException {
StringBuilder sb = StringBuilderPool.DEFAULT.stringBuilder();
Utf8UrlEncoder.encodeAndAppendQueryElement(sb, consumerAuth.getSecret());
sb.append('&');
if (userAuth != null && userAuth.getSecret() != null) {
Utf8UrlEncoder.encodeAndAppendQueryElement(sb, userAuth.getSecret());
}
byte[] keyBytes = StringUtils.charSequence2Bytes(sb, UTF_8);
SecretKeySpec signingKey = new SecretKeySpec(keyBytes, HMAC_SHA1_ALGORITHM);
mac.init(signingKey);
mac.reset();
mac.update(message);
return mac.doFinal();
}
String constructAuthHeader(ConsumerKey consumerAuth, RequestToken userAuth, String signature, String nonce, long oauthTimestamp) {
StringBuilder sb = StringBuilderPool.DEFAULT.stringBuilder();
sb.append("OAuth ");
sb.append(KEY_OAUTH_CONSUMER_KEY).append("=\"").append(consumerAuth.getKey()).append("\", ");
if (userAuth.getKey() != null) {
sb.append(KEY_OAUTH_TOKEN).append("=\"").append(userAuth.getKey()).append("\", ");
}
sb.append(KEY_OAUTH_SIGNATURE_METHOD).append("=\"").append(OAUTH_SIGNATURE_METHOD).append("\", ");
// careful: base64 has chars that need URL encoding:
sb.append(KEY_OAUTH_SIGNATURE).append("=\"");
Utf8UrlEncoder.encodeAndAppendPercentEncoded(sb, signature).append("\", ");
sb.append(KEY_OAUTH_TIMESTAMP).append("=\"").append(oauthTimestamp).append("\", ");
// also: nonce may contain things that need URL encoding (esp. when using base64):
sb.append(KEY_OAUTH_NONCE).append("=\"");
Utf8UrlEncoder.encodeAndAppendPercentEncoded(sb, nonce);
sb.append("\", ");
sb.append(KEY_OAUTH_VERSION).append("=\"").append(OAUTH_VERSION_1_0).append("\"");
return sb.toString();
}
}