/*
* Copyright 2010 Ning, Inc.
*
* Ning licenses this file to you under the Apache License, version 2.0
* (the "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
*/
package com.ning.http.client.oauth;
import com.ning.http.client.FluentStringsMap;
import com.ning.http.client.Request;
import com.ning.http.client.RequestBuilderBase;
import com.ning.http.client.SignatureCalculator;
import com.ning.http.util.Base64;
import com.ning.http.util.UTF8Codec;
import com.ning.http.util.UTF8UrlEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Random;
/**
* Simple OAuth signature calculator that can used for constructing client signatures
* for accessing services that use OAuth for authorization.
* <p/>
* 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.
*
* @author tatu (tatu.saloranta@iki.fi)
*/
public class OAuthSignatureCalculator
implements SignatureCalculator {
public final static String HEADER_AUTHORIZATION = "Authorization";
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";
/**
* To generate Nonce, need some (pseudo)randomness; no need for
* secure variant here.
*/
protected final Random random;
protected final byte[] nonceBuffer = new byte[16];
protected final ThreadSafeHMAC mac;
protected final ConsumerKey consumerAuth;
protected final RequestToken userAuth;
/**
* @param consumerAuth Consumer key to use for signature calculation
* @param userAuth Request/access token to use for signature calculation
*/
public OAuthSignatureCalculator(ConsumerKey consumerAuth, RequestToken userAuth) {
mac = new ThreadSafeHMAC(consumerAuth, userAuth);
this.consumerAuth = consumerAuth;
this.userAuth = userAuth;
random = new Random(System.identityHashCode(this) + System.currentTimeMillis());
}
//@Override // silly 1.5; doesn't allow this for interfaces
public void calculateAndAddSignature(String baseURL, Request request, RequestBuilderBase<?> requestBuilder) {
String method = request.getMethod(); // POST etc
String nonce = generateNonce();
long timestamp = System.currentTimeMillis() / 1000L;
String signature = calculateSignature(method, baseURL, timestamp, nonce, request.getParams(), request.getQueryParams());
String headerValue = constructAuthHeader(signature, nonce, timestamp);
requestBuilder.setHeader(HEADER_AUTHORIZATION, headerValue);
}
/**
* Method for calculating OAuth signature using HMAC/SHA-1 method.
*/
public String calculateSignature(String method, String baseURL, long oauthTimestamp, String nonce,
FluentStringsMap formParams, FluentStringsMap queryParams) {
StringBuilder signedText = new StringBuilder(100);
signedText.append(method); // POST / GET etc (nothing to URL encode)
signedText.append('&');
/* 07-Oct-2010, tatu: URL may contain default port number; if so, need to extract
* from base URL.
*/
if (baseURL.startsWith("http:")) {
int i = baseURL.indexOf(":80/", 4);
if (i > 0) {
baseURL = baseURL.substring(0, i) + baseURL.substring(i + 3);
}
} else if (baseURL.startsWith("https:")) {
int i = baseURL.indexOf(":443/", 5);
if (i > 0) {
baseURL = baseURL.substring(0, i) + baseURL.substring(i + 4);
}
}
signedText.append(UTF8UrlEncoder.encode(baseURL));
/**
* List of all query and form parameters added to this request; needed
* for calculating request signature
*/
OAuthParameterSet allParameters = new OAuthParameterSet();
// start with standard OAuth parameters we need
allParameters.add(KEY_OAUTH_CONSUMER_KEY, 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));
allParameters.add(KEY_OAUTH_TOKEN, userAuth.getKey());
allParameters.add(KEY_OAUTH_VERSION, OAUTH_VERSION_1_0);
if (formParams != null) {
for (Map.Entry<String, List<String>> entry : formParams) {
String key = entry.getKey();
for (String value : entry.getValue()) {
allParameters.add(key, value);
}
}
}
if (queryParams != null) {
for (Map.Entry<String, List<String>> entry : queryParams) {
String key = entry.getKey();
for (String value : entry.getValue()) {
allParameters.add(key, value);
}
}
}
String encodedParams = allParameters.sortAndConcat();
// and all that needs to be URL encoded (... again!)
signedText.append('&');
UTF8UrlEncoder.appendEncoded(signedText, encodedParams);
byte[] rawBase = UTF8Codec.toUTF8(signedText.toString());
byte[] rawSignature = mac.digest(rawBase);
// and finally, base64 encoded... phew!
return Base64.encode(rawSignature);
}
/**
* Method used for constructing
*/
public String constructAuthHeader(String signature, String nonce, long oauthTimestamp) {
StringBuilder sb = new StringBuilder(200);
sb.append("OAuth ");
sb.append(KEY_OAUTH_CONSUMER_KEY).append("=\"").append(consumerAuth.getKey()).append("\", ");
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.appendEncoded(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.appendEncoded(sb, nonce);
sb.append("\", ");
sb.append(KEY_OAUTH_VERSION).append("=\"").append(OAUTH_VERSION_1_0).append("\"");
return sb.toString();
}
private synchronized String generateNonce() {
random.nextBytes(nonceBuffer);
// let's use base64 encoding over hex, slightly more compact than hex or decimals
return Base64.encode(nonceBuffer);
// return String.valueOf(Math.abs(random.nextLong()));
}
/**
* 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 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)... of course, rarely means that
* when it would occur it'd be harder to track down.
*/
final static class OAuthParameterSet {
final private ArrayList<Parameter> allParameters = new ArrayList<Parameter>();
public OAuthParameterSet() {
}
public OAuthParameterSet add(String key, String value) {
Parameter p = new Parameter(UTF8UrlEncoder.encode(key), UTF8UrlEncoder.encode(value));
allParameters.add(p);
return this;
}
public String sortAndConcat() {
// then sort them (AFTER encoding, important)
Parameter[] params = allParameters.toArray(new Parameter[allParameters.size()]);
Arrays.sort(params);
// and build parameter section using pre-encoded pieces:
StringBuilder encodedParams = new StringBuilder(100);
for (Parameter param : params) {
if (encodedParams.length() > 0) {
encodedParams.append('&');
}
encodedParams.append(param.key()).append('=').append(param.value());
}
return encodedParams.toString();
}
}
/**
* Helper class for sorting query and form parameters that we need
*/
final static class Parameter implements Comparable<Parameter> {
private final String key, value;
public Parameter(String key, String value) {
this.key = key;
this.value = value;
}
public String key() {
return key;
}
public String value() {
return value;
}
//@Override // silly 1.5; doesn't allow this for interfaces
public int compareTo(Parameter other) {
int diff = key.compareTo(other.key);
if (diff == 0) {
diff = value.compareTo(other.value);
}
return diff;
}
@Override
public String toString() {
return key + "=" + value;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Parameter parameter = (Parameter) o;
if (!key.equals(parameter.key)) return false;
if (!value.equals(parameter.value)) return false;
return true;
}
@Override
public int hashCode() {
int result = key.hashCode();
result = 31 * result + value.hashCode();
return result;
}
}
}