Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
OAuth v1 Authorization Builder
/*
* Copyright (c) 2019, FusionAuth, All Rights Reserved
*
* 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 javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author Daniel DeGroff
*/
public class OAuth1AuthorizationHeaderBuilder {
private static final char[] HEX = "0123456789ABCDEF".toCharArray();
// https://tools.ietf.org/html/rfc3986#section-2.3
private static final Set<Character> UnreservedChars = new HashSet<>(Arrays.asList(
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'-', '_', '.', '~'));
public String consumerSecret;
public String method;
public String parameterString;
public Map<String, String> parameters = new LinkedHashMap<>();
public String signature;
public String signatureBaseString;
public String signingKey;
public String tokenSecret;
public String url;
/***
* Replaces any character not specifically unreserved to an equivalent percent sequence.
*
* @param s the string to encode
* @return and encoded string
*/
public static String encodeURIComponent(String s) {
StringBuilder o = new StringBuilder();
for (byte b : s.getBytes(StandardCharsets.UTF_8)) {
if (isSafe(b)) {
o.append((char) b);
} else {
o.append('%');
o.append(HEX[((b & 0xF0) >> 4)]);
o.append(HEX[((b & 0x0F))]);
}
}
return o.toString();
}
private static boolean isSafe(byte b) {
return UnreservedChars.contains((char) b);
}
public String build() {
// For testing purposes, only add the timestamp if it has not yet been added
if (!parameters.containsKey("oauth_timestamp")) {
parameters.put("oauth_timestamp", "" + Instant.now().getEpochSecond());
}
// Boiler plate parameters
parameters.put("oauth_signature_method", "HMAC-SHA1");
parameters.put("oauth_version", "1.0");
// Build the parameter string after sorting the keys in lexicographic order per the OAuth v1 spec.
parameterString = parameters.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.map(e -> encodeURIComponent(e.getKey()) + "=" + encodeURIComponent(e.getValue()))
.collect(Collectors.joining("&"));
// Build the signature base string
signatureBaseString = method.toUpperCase() + "&" + encodeURIComponent(url) + "&" + encodeURIComponent(parameterString);
// If the signing key was not provided, build it by encoding the consumer secret + the token secret
if (signingKey == null) {
signingKey = encodeURIComponent(consumerSecret) + "&" + (tokenSecret == null ? "" : encodeURIComponent(tokenSecret));
}
// Sign the Signature Base String
signature = generateSignature(signingKey, signatureBaseString);
// Add the signature to be included in the header
parameters.put("oauth_signature", signature);
// Build the authorization header value using the order in which the parameters were added
return "OAuth " + parameters.entrySet().stream()
.map(e -> encodeURIComponent(e.getKey()) + "=\"" + encodeURIComponent(e.getValue()) + "\"")
.collect(Collectors.joining(", "));
}
public String generateSignature(String secret, String message) {
try {
byte[] bytes = secret.getBytes(StandardCharsets.UTF_8);
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(new SecretKeySpec(bytes, "HmacSHA1"));
byte[] result = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(result);
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
/**
* Set the Consumer Secret
*
* @param consumerSecret the Consumer Secret
* @return this
*/
public OAuth1AuthorizationHeaderBuilder withConsumerSecret(String consumerSecret) {
this.consumerSecret = consumerSecret;
return this;
}
/**
* Set the requested HTTP method
*
* @param method the HTTP method you are requesting
* @return this
*/
public OAuth1AuthorizationHeaderBuilder withMethod(String method) {
this.method = method;
return this;
}
/**
* Add a parameter to the be included when building the signature.
*
* @param name the parameter name
* @param value the parameter value
* @return this
*/
public OAuth1AuthorizationHeaderBuilder withParameter(String name, String value) {
parameters.put(name, value);
return this;
}
/**
* Set the OAuth Token Secret
*
* @param tokenSecret the OAuth Token Secret
* @return this
*/
public OAuth1AuthorizationHeaderBuilder withTokenSecret(String tokenSecret) {
this.tokenSecret = tokenSecret;
return this;
}
/**
* Set the requested URL in the builder.
*
* @param url the URL you are requesting
* @return this
*/
public OAuth1AuthorizationHeaderBuilder withURL(String url) {
this.url = url;
return this;
}
}
@felipegeorge

This comment has been minimized.

Copy link

@felipegeorge felipegeorge commented Sep 4, 2019

hi man, it was necessary to add the parameter NONCE (is a random string generated by the client), because the internal server error.

@schoenobates

This comment has been minimized.

Copy link

@schoenobates schoenobates commented Jun 30, 2020

Hi Dan,

I think the encodeURIComponent needs updating to the following, I did a test with the twitter set (https://developer.twitter.com/en/docs/basics/authentication/oauth-1-0a/percent-encoding-parameters) and the above produces incorrect output for the snowman character. Works fine if I do the following:

private static final String HEX_CHAR = "0123456789ABCDEF";
private static final Set<Byte> SAFE_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~"
        .chars()
        .mapToObj(c -> (byte) c)
        .collect(toSet());

static String encode(@NonNull String s) {
        StringBuilder o = new StringBuilder();
        for (byte b : s.getBytes(StandardCharsets.UTF_8)) {
            if (SAFE_CHARS.contains(b)) {
                o.append((char) b);
                continue;
            }

            o.append('%');
            o.append(HEX_CHAR.charAt((b & 0xF0) >> 4));
            o.append(HEX_CHAR.charAt((b & 0x0F)));
        }
        return o.toString();
    }
@robotdan

This comment has been minimized.

Copy link
Owner Author

@robotdan robotdan commented Jun 30, 2020

Thanks @schoenobates ! I've added the update.

@bariis

This comment has been minimized.

Copy link

@bariis bariis commented Aug 28, 2020

Hi Dan, I am dealing with this "creating signature" for days. For the encode method can we use the algorithm below?

private static String percentEncoding(String originalString) {
        String encodedString = Base64.getUrlEncoder().encodeToString(originalString.getBytes());
        return encodedString;
    }

Another thing is, I want to fetch my followers list so my URL is like that. Where should I put my cursor=-1 thing in your code?
https://api.twitter.com/1.1/followers/list.json?cursor=-1

and here is my request.

JSONObject response = Unirest.get("https://api.twitter.com/1.1/followers/list.json?cursor=-1")
                .header("Content-Type", "application/x-www-form-urlencoded")
                .header("Authorization", "OAuth oauth_consumer_key=\"fHkdJVy3x1fKE1Yop9qraJyCp\"," +
                        "oauth_token=" + "\"" + userAcessToken +"\""+ "," +
                        "oauth_signature_method=" + "\"HMAC-SHA1\"," +
                        "oauth_timestamp=" + "\""+timeStamp + "\"" + "," +
                        "oauth_nonce="     + "\""+nonce     +  "\"" + "," +
                        "oauth_version=\"1.0\"," +
                        "oauth_signature=" + "\"" +oauth_signature + "\"")
                .asJson().getBody().getObject();

Any advice can you recommend for me? thanks.

@robotdan

This comment has been minimized.

Copy link
Owner Author

@robotdan robotdan commented Sep 2, 2020

@bariis re: encoding. What are you encoding? Is that a replacement for the signature encoding? So is the question can you use getUrlEncoder() instead of getEncoder()?

Re: the cursor=-1, this looks to be a request parameter on the list API? If so, I think you'd need to add it using the withParameter method so that it is included in the signature calculation.

Maybe something like:

    OAuth1AuthorizationHeaderBuilder builder = new OAuth1AuthorizationHeaderBuilder()
        .withMethod("GET")
        .withURL("https://api.twitter.com/1.1/followers/list.json")
        .withConsumerSecret("fHkdJVy3x1fKE1Yop9qraJyCp")
        .withTokenSecret("<token secret>")
        .withParameter("cursor", "-1");

I haven't tested with this API, if that doesn't work, I can try to see if I can recreate.

@mahdix

This comment has been minimized.

Copy link

@mahdix mahdix commented Oct 17, 2020

FYI Set is not imported.

@robotdan

This comment has been minimized.

Copy link
Owner Author

@robotdan robotdan commented Oct 19, 2020

Thanks @mahdix, corrected.

@MichZem

This comment has been minimized.

Copy link

@MichZem MichZem commented Dec 29, 2020

This code didn't work well for me, while sending a POST request with queryParam.
For some reason, the queryParam were added into the signatureBaseString (within the build() implementation) at the beginning , while my backend server code, which verified the signature, did add the query parameters at the end of the signatureBaseString used for building the signature. I dont know if my backend server code used a standard code for generating the expected signature but it worked for me and that was the important thing ...

So this is the change I've made. Not very robust / resilient but it was enough for me (used it for my test which needed oauth1 flow)

/** new data member **/
String queryParameters = "";

/** withURL - modified method **/
public OAuth1AuthorizationHeaderBuilder withURL(String url) {

**if(url.contains("?")) {
  handleQueryParam(url);
}**
else {
  this.url = url;
}
return this;

}

/** build - modified method **/
public String build() {
[...]
// Build the signature base string
signatureBaseString = method.toUpperCase() + "&" + encodeURIComponent(url) + "&" + encodeURIComponent(parameterString) + encodeURIComponent(queryParameters);

}

/** handleQueryParam new method **/
private void handleQueryParam(String url) {
if (url.contains("?")) {
String queryParam = url.substring(url.indexOf("?"));
if(queryParam.length() > 1) {
queryParam = queryParam.substring(1);

    String[] paramTokens = queryParam.split("&");
    if (paramTokens != null) {
      for (String paramToken : paramTokens) {
        String[] paramNameAndValue = paramToken.split("=");
        if (paramNameAndValue != null) {
          this.withURLQueryParameter(paramToken);
        }
      }
    }
  }
  this.url = url.substring(0, url.indexOf("?"));

}

/** withURLQueryParameter new method **/
public OAuth1AuthorizationHeaderBuilder withURLQueryParameter(String queryParameters) {
if(queryParameters == null || queryParameters.isEmpty()) {
this.queryParameters = "";
}
else {
this.queryParameters = "&" + queryParameters;
}
return this;
}

@robotdan

This comment has been minimized.

Copy link
Owner Author

@robotdan robotdan commented Dec 29, 2020

Can you format this code a bit further so I can see what the changes are - perhaps I can update this Gist to make it work better.

@MichZem

This comment has been minimized.

Copy link

@MichZem MichZem commented Dec 30, 2020

This is the class, with my fix ...
the method that were touched. are :

  • withURL(String url)
  • build () ,
  • handleQueryParam(String url)
  • withURLQueryParameter(String queryParameters)

Please tell me if this fit what you've asked .



/*
 *
 * Michael note: Freely copied and enriched from https://gist.github.com/robotdan/33f5834399b6b30fea2ae59e87823e1d  and https://stackoverflow.com/questions/25418764/how-to-call-api-oauth-1-0 <br/>
 *
 * Copyright (c) 2019, FusionAuth, All Rights Reserved
 *
 * 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 javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @author Daniel DeGroff
 */
public class OAuth1AuthorizationHeaderBuilder {
  private static final char[] HEX = "0123456789ABCDEF".toCharArray();

  // https://tools.ietf.org/html/rfc3986#section-2.3
  private static final Set<Character> UnreservedChars = new HashSet<>(Arrays.asList(
      'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
      'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
      '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
      '-', '_', '.', '~'));

  public String consumerSecret;

  public String method;

  public String parameterString;

  String queryParameters = "";

  public Map<String, String> parameters = new LinkedHashMap<>();

  public String signature;

  public String signatureBaseString;

  public String signingKey;

  public String tokenSecret;

  public String url;

  /***
   * Replaces any character not specifically unreserved to an equivalent percent sequence.
   *
   * @param s the string to encode
   * @return and encoded string
   */
  public static String encodeURIComponent(String s) {
    StringBuilder o = new StringBuilder();
    for (byte b : s.getBytes(StandardCharsets.UTF_8)) {
      if (isSafe(b)) {
        o.append((char) b);
      } else {
        o.append('%');
        o.append(HEX[((b & 0xF0) >> 4)]);
        o.append(HEX[((b & 0x0F))]);
      }
    }
    return o.toString();
  }

  private static boolean isSafe(byte b) {
    return UnreservedChars.contains((char) b);
  }

  public String build() {
    // For testing purposes, only add the timestamp if it has not yet been added
    if (!parameters.containsKey("oauth_timestamp")) {
      parameters.put("oauth_timestamp", "" + Instant.now().getEpochSecond());
    }

    //Boiler plate parameters
    parameters.put("oauth_signature_method", "HMAC-SHA1");
    parameters.put("oauth_version", "1.0");

    //Build the parameter string after sorting the keys in lexicographic order per the OAuth v1 spec.
    parameterString = parameters.entrySet().stream()
                                .sorted(Map.Entry.comparingByKey())
                                .map(e -> encodeURIComponent(e.getKey()) + "=" + encodeURIComponent(e.getValue()))
                                .collect(Collectors.joining("&"));

    // Build the signature base string
    signatureBaseString = method.toUpperCase() + "&" + encodeURIComponent(url) + "&" + encodeURIComponent(parameterString) + encodeURIComponent(queryParameters);

    // If the signing key was not provided, build it by encoding the consumer secret + the token secret
    if (signingKey == null) {
      signingKey = encodeURIComponent(consumerSecret) + "&" + (tokenSecret == null ? "" : encodeURIComponent(tokenSecret));
    }

    // Sign the Signature Base String
    signature = generateSignature(signingKey, signatureBaseString);

    // Add the signature to be included in the header
    parameters.put("oauth_signature", signature);

    // Build the authorization header value using the order in which the parameters were added
    return "OAuth " + parameters.entrySet().stream()
                                .map(e -> encodeURIComponent(e.getKey()) + "=\"" + encodeURIComponent(e.getValue()) + "\"")
                                .collect(Collectors.joining(", "));
  }

  public String generateSignature(String secret, String message) {
    try {
      byte[] bytes = secret.getBytes(StandardCharsets.UTF_8);
      Mac mac = Mac.getInstance("HmacSHA1");
      mac.init(new SecretKeySpec(bytes, "HmacSHA1"));
      byte[] result = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
      return Base64.getEncoder().encodeToString(result);
    } catch (InvalidKeyException | NoSuchAlgorithmException e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Set the Consumer Secret
   *
   * @param consumerSecret the Consumer Secret
   * @return this
   */
  public OAuth1AuthorizationHeaderBuilder withConsumerSecret(String consumerSecret) {
    this.consumerSecret = consumerSecret;
    return this;
  }

  /**
   * Set the requested HTTP method
   *
   * @param method the HTTP method you are requesting
   * @return this
   */
  public OAuth1AuthorizationHeaderBuilder withMethod(String method) {
    this.method = method;
    return this;
  }

  /**
   * Add a parameter to the be included when building the signature.
   *
   * @param name  the parameter name
   * @param value the parameter value
   * @return this
   */
  public OAuth1AuthorizationHeaderBuilder withParameter(String name, String value) {
    parameters.put(name, value);
    return this;
  }


  public OAuth1AuthorizationHeaderBuilder withURLQueryParameter(String queryParameters) {
    if(queryParameters == null || queryParameters.isEmpty()) {
      this.queryParameters = "";
    }
    else {
      this.queryParameters = "&" + queryParameters;
    }
    return this;
  }

  /**
   * Set the OAuth Token Secret
   *
   * @param tokenSecret the OAuth Token Secret
   * @return this
   */
  public OAuth1AuthorizationHeaderBuilder withTokenSecret(String tokenSecret) {
    this.tokenSecret = tokenSecret;
    return this;
  }

  /**
   * Set the requested URL in the builder.
   *
   * @param url the URL you are requesting
   * @return this
   */
  public OAuth1AuthorizationHeaderBuilder withURL(String url) {

    if(url.contains("?")) {
      handleQueryParam(url);
    }
    else {
      this.url = url;
    }
    return this;
  }

  /**
   * If url contains queryParam , extract them from the this.url data member
   * and assign the, to the this.queryParameters data member <br/>
   *
   * For ex: <br/>
   * URL: <i>https://lp-agentmngworkspace-qa.dev.lprnd.net/manager_workspace/api/account/le61691980/agent_view?version=v1</i>
   * will lead to : <br/>
   * url = <b>https://lp-agentmngworkspace-qa.dev.lprnd.net/manager_workspace/api/account/le61691980/agent_view</b> <br/>
   * queryParameters = <b>&version=v1</b> <br/>
   * @param url
   */
  private void handleQueryParam(String url) {
    if (url.contains("?")) {
      String queryParam = url.substring(url.indexOf("?"));
      if(queryParam.length() > 1) {
        queryParam = queryParam.substring(1);

        String[] paramTokens = queryParam.split("&");
        if (paramTokens != null) {
          for (String paramToken : paramTokens) {
            String[] paramNameAndValue = paramToken.split("=");
            if (paramNameAndValue != null) {
              this.withURLQueryParameter(paramToken);
            }
          }
        }
      }
      this.url = url.substring(0, url.indexOf("?"));

    }
  }

}

@robotdan

This comment has been minimized.

Copy link
Owner Author

@robotdan robotdan commented Mar 11, 2021

Thanks for the all help on this!! I am moving this gist here -> https://github.com/FusionAuth/fusionauth-oauth1 so I can write some tests and more easily accepts PRs when any issues come up. I'll add some tests and then merge in the suggestions made by @MichZem

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