Instantly share code, notes, and snippets.

Embed
What would you like to do?
An OkHttp interceptor which does OAuth1 signing. Requires Guava and Java 8, although those dependencies wouldn't be too hard to break if you didn't have them.
/*
* Copyright (C) 2015 Jake Wharton
*
* Licensed 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.
*/
import com.google.common.escape.Escaper;
import com.google.common.net.UrlEscapers;
import com.squareup.okhttp.HttpUrl;
import com.squareup.okhttp.Interceptor;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.time.Clock;
import java.util.Map;
import java.util.Random;
import java.util.SortedMap;
import java.util.TreeMap;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import okio.Buffer;
import okio.ByteString;
public final class Oauth1SigningInterceptor implements Interceptor {
private static final Escaper ESCAPER = UrlEscapers.urlFormParameterEscaper();
private static final String OAUTH_CONSUMER_KEY = "oauth_consumer_key";
private static final String OAUTH_NONCE = "oauth_nonce";
private static final String OAUTH_SIGNATURE = "oauth_signature";
private static final String OAUTH_SIGNATURE_METHOD = "oauth_signature_method";
private static final String OAUTH_SIGNATURE_METHOD_VALUE = "HMAC-SHA1";
private static final String OAUTH_TIMESTAMP = "oauth_timestamp";
private static final String OAUTH_ACCESS_TOKEN = "oauth_token";
private static final String OAUTH_VERSION = "oauth_version";
private static final String OAUTH_VERSION_VALUE = "1.0";
private final String consumerKey;
private final String consumerSecret;
private final String accessToken;
private final String accessSecret;
private final Random random;
private final Clock clock;
private Oauth1SigningInterceptor(String consumerKey, String consumerSecret, String accessToken,
String accessSecret, Random random, Clock clock) {
this.consumerKey = consumerKey;
this.consumerSecret = consumerSecret;
this.accessToken = accessToken;
this.accessSecret = accessSecret;
this.random = random;
this.clock = clock;
}
@Override public Response intercept(Chain chain) throws IOException {
return chain.proceed(signRequest(chain.request()));
}
public Request signRequest(Request request) throws IOException {
byte[] nonce = new byte[32];
random.nextBytes(nonce);
String oauthNonce = ByteString.of(nonce).base64().replaceAll("\\W", "");
String oauthTimestamp = String.valueOf(clock.millis());
String consumerKeyValue = ESCAPER.escape(consumerKey);
String accessTokenValue = ESCAPER.escape(accessToken);
SortedMap<String, String> parameters = new TreeMap<>();
parameters.put(OAUTH_CONSUMER_KEY, consumerKeyValue);
parameters.put(OAUTH_ACCESS_TOKEN, accessTokenValue);
parameters.put(OAUTH_NONCE, oauthNonce);
parameters.put(OAUTH_TIMESTAMP, oauthTimestamp);
parameters.put(OAUTH_SIGNATURE_METHOD, OAUTH_SIGNATURE_METHOD_VALUE);
parameters.put(OAUTH_VERSION, OAUTH_VERSION_VALUE);
HttpUrl url = request.httpUrl();
for (int i = 0; i < url.querySize(); i++) {
parameters.put(ESCAPER.escape(url.queryParameterName(i)),
ESCAPER.escape(url.queryParameterValue(i)));
}
RequestBody requestBody = request.body();
Buffer body = new Buffer();
requestBody.writeTo(body);
while (!body.exhausted()) {
long keyEnd = body.indexOf((byte) '=');
if (keyEnd == -1) throw new IllegalStateException("Key with no value: " + body.readUtf8());
String key = body.readUtf8(keyEnd);
body.skip(1); // Equals.
long valueEnd = body.indexOf((byte) '&');
String value = valueEnd == -1 ? body.readUtf8() : body.readUtf8(valueEnd);
if (valueEnd != -1) body.skip(1); // Ampersand.
parameters.put(key, value);
}
Buffer base = new Buffer();
String method = request.method();
base.writeUtf8(method);
base.writeByte('&');
base.writeUtf8(ESCAPER.escape(request.httpUrl().newBuilder().query(null).build().toString()));
base.writeByte('&');
boolean first = true;
for (Map.Entry<String, String> entry : parameters.entrySet()) {
if (!first) base.writeUtf8(ESCAPER.escape("&"));
first = false;
base.writeUtf8(ESCAPER.escape(entry.getKey()));
base.writeUtf8(ESCAPER.escape("="));
base.writeUtf8(ESCAPER.escape(entry.getValue()));
}
String signingKey =
ESCAPER.escape(consumerSecret) + "&" + ESCAPER.escape(accessSecret);
SecretKeySpec keySpec = new SecretKeySpec(signingKey.getBytes(), "HmacSHA1");
Mac mac;
try {
mac = Mac.getInstance("HmacSHA1");
mac.init(keySpec);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new IllegalStateException(e);
}
byte[] result = mac.doFinal(base.readByteArray());
String signature = ByteString.of(result).base64();
String authorization = "OAuth "
+ OAUTH_CONSUMER_KEY + "=\"" + consumerKeyValue + "\", "
+ OAUTH_NONCE + "=\"" + oauthNonce + "\", "
+ OAUTH_SIGNATURE + "=\"" + ESCAPER.escape(signature) + "\", "
+ OAUTH_SIGNATURE_METHOD + "=\"" + OAUTH_SIGNATURE_METHOD_VALUE + "\", "
+ OAUTH_TIMESTAMP + "=\"" + oauthTimestamp + "\", "
+ OAUTH_ACCESS_TOKEN + "=\"" + accessTokenValue + "\", "
+ OAUTH_VERSION + "=\"" + OAUTH_VERSION_VALUE + "\"";
return request.newBuilder()
.addHeader("Authorization", authorization)
.build();
}
public static final class Builder {
private String consumerKey;
private String consumerSecret;
private String accessToken;
private String accessSecret;
private Random random = new SecureRandom();
private Clock clock = Clock.systemUTC();
public Builder consumerKey(String consumerKey) {
if (consumerKey == null) throw new NullPointerException("consumerKey = null");
this.consumerKey = consumerKey;
return this;
}
public Builder consumerSecret(String consumerSecret) {
if (consumerSecret == null) throw new NullPointerException("consumerSecret = null");
this.consumerSecret = consumerSecret;
return this;
}
public Builder accessToken(String accessToken) {
if (accessToken == null) throw new NullPointerException("accessToken == null");
this.accessToken = accessToken;
return this;
}
public Builder accessSecret(String accessSecret) {
if (accessSecret == null) throw new NullPointerException("accessSecret == null");
this.accessSecret = accessSecret;
return this;
}
public Builder random(Random random) {
if (random == null) throw new NullPointerException("random == null");
this.random = random;
return this;
}
public Builder clock(Clock clock) {
if (clock == null) throw new NullPointerException("clock == null");
this.clock = clock;
return this;
}
public Oauth1SigningInterceptor build() {
if (consumerKey == null) throw new IllegalStateException("consumerKey not set");
if (consumerSecret == null) throw new IllegalStateException("consumerSecret not set");
if (accessToken == null) throw new IllegalStateException("accessToken not set");
if (accessSecret == null) throw new IllegalStateException("accessSecret not set");
return new Oauth1SigningInterceptor(consumerKey, consumerSecret, accessToken, accessSecret, random,
clock);
}
}
}
/*
* Copyright (C) 2015 Jake Wharton
*
* Licensed 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.
*/
import com.squareup.okhttp.FormEncodingBuilder;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.RequestBody;
import java.io.IOException;
import java.time.Clock;
import java.time.Instant;
import java.util.Random;
import okio.ByteString;
import org.junit.Test;
import static com.google.common.truth.Truth.assertThat;
import static java.time.ZoneOffset.UTC;
public final class Oauth1SigningInterceptorTest {
@Test public void litmus() throws IOException {
// Data from https://dev.twitter.com/oauth/overview/authorizing-requests.
Random notRandom = new Random() {
@Override public void nextBytes(byte[] bytes) {
if (bytes.length != 32) throw new AssertionError();
ByteString hex = ByteString.decodeBase64("kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4c+g");
byte[] nonce = hex.toByteArray();
System.arraycopy(nonce, 0, bytes, 0, nonce.length);
}
};
Clock clock = Clock.fixed(Instant.ofEpochMilli(1318622958), UTC);
Oauth1SigningInterceptor oauth1 = new Oauth1SigningInterceptor.Builder()
.consumerKey("xvz1evFS4wEEPTGEFPHBog")
.consumerSecret("kAcSOqF21Fu85e7zjz7ZN2U4ZRhfV3WpwPAoE3Z7kBw")
.accessToken("370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb")
.accessSecret("LswwdoUaIvS8ltyTt5jkRh4J50vUPVVHtR2YPi5kE")
.random(notRandom)
.clock(clock)
.build();
RequestBody body = new FormEncodingBuilder()
.add("status", "Hello Ladies + Gentlemen, a signed OAuth request!")
.build();
Request request = new Request.Builder()
.url("https://api.twitter.com/1/statuses/update.json?include_entities=true")
.post(body)
.build();
Request signed = oauth1.signRequest(request);
assertThat(signed.header("Authorization")).isEqualTo("OAuth "
+ "oauth_consumer_key=\"xvz1evFS4wEEPTGEFPHBog\", "
+ "oauth_nonce=\"kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg\", "
+ "oauth_signature=\"tnnArxj06cWHq44gCs1OSKk%2FjLY%3D\", "
+ "oauth_signature_method=\"HMAC-SHA1\", "
+ "oauth_timestamp=\"1318622958\", "
+ "oauth_token=\"370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb\", "
+ "oauth_version=\"1.0\"");
}
}
@dgyuri

This comment has been minimized.

dgyuri commented Jul 25, 2015

NPE when request's body is null (HTTP GET).

 RequestBody requestBody = request.body();
 Buffer body = new Buffer();
 requestBody.writeTo(body);
@dgyuri

This comment has been minimized.

dgyuri commented Aug 4, 2015

I've got wrong signature when the request URL has the same query parameter multiple time. Oauth1SigningInterceptor puts query parameters in a map that cannot contain duplicate keys.

Example URL:
{base_url}/repositories/{user}/{repo}/issues?status=new&status=open

Retrofit:

@GET("/repositories/{user}/{repo}/issues")
Call<IssueFilterResult> issues(
        @Path("user") String user,
        @Path("slug") String repo,
        @Query("status") Iterable status);

Note: status is an iterable.

@serj-lotutovici

This comment has been minimized.

serj-lotutovici commented Oct 19, 2015

If somebody is interested here is a fork of this gist targeting Java 7 (no Guava required).

@JakeWharton, thank you for this code, I just removed a monolith dependency from our code base that was used only for this.

@CosminMihuMDC

This comment has been minimized.

CosminMihuMDC commented May 11, 2017

Here is a fork that uses Percent Encoding method for URL. (https://tools.ietf.org/html/rfc5849#page-29)

1. Without Percent Encoding (https://gist.github.com/JakeWharton/f26f19732f0c5907e1ab)

URL Encoded:

POST https://example.com/search?keyword=cosm+mi&start=1&limit=10

base string with space encoded as '+':

POST&https%3A%2F%2Fexample.com%2Fsearch&keyword%3Dcosm%2Bmi%26limit%3D10%26oauth_consumer_key%3Ddebf8ca8-416f-4f2d-bf7d-ea045083a643%26oauth_nonce%3DYNxfWmf0IxCwTIY8n9wXbUvGcnJlScUqhrZ53hr1w3Y%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1494508387%26oauth_token%3D8700b3a7-b68a-4225-88ec-6c9117f7828c%26oauth_version%3D1.0%26start%3D1

2. Using Percent Encoding (https://gist.github.com/CosminMihuMDC/03b5396367f8dbe6b52cf89d6b88bcce)

URL Encoded:

POST https://example.com/search?keyword=cosm%20mi&start=1&limit=100

base string with space encoded as '%20':

POST&https%3A%2F%2Fexample.com%2Fsearch&keyword%3Dcosm%2520mi%26limit%3D10%26oauth_consumer_key%3Ddebf8ca8-416f-4f2d-bf7d-ea045083a643%26oauth_nonce%3DYNxfWmf0IxCwTIY8n9wXbUvGcnJlScUqhrZ53hr1w3Y%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1494508387%26oauth_token%3D8700b3a7-b68a-4225-88ec-6c9117f7828c%26oauth_version%3D1.0%26start%3D1

Thanks @JakeWharton, @serj-lotutovici.

@DaarSingh

This comment has been minimized.

DaarSingh commented Nov 12, 2017

UrlEscapers.urlFormParameterEscaper causes crash when sending a post multipart image upload. I have added more detail on stackoverflow https://stackoverflow.com/questions/47253666/java-lang-illegalargumentexception-unexpected-low-surrogate-character-with . Any idea how to fix this ?

@Zoha131

This comment has been minimized.

Zoha131 commented Jul 18, 2018

I am a very beginner developer. One thing I don't get yet that I have only cosumer_key and consumer_secret but the builder also requires accessToken and accessSecret. Then how can I use this interceptor? It will be very helpful if you can show me an example.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment