Am I generating the OAuth signature incorrectly or is there another issue with this Perl / OAuth client code?

Viewed 1476

OK, I'm going nuts here. I've spent the better part of a week working on this with Fiddler, Rest API Log, Apache logs, Postman, etc. and am no closer to a solution. Hopefully this will provide someone with a good laugh (as it's easy) and me with a solution (for my sanity).

I have a Wordpress 4.9.8 install hosted on hostgator. I have the WP REST API - OAuth 1.0a Server plugin installed and am attempting to authenticate with OAuth from a Perl script. I've been successful connecting with Postman using the credential generated by registering an application in the UI (Wordpress > Users > Applications) but cannot seem to crack the code using curl or a perl script (or php or anything else I've tried). As Postman works (whenever it does not generate a space in the OAuth signature) I believe the plugin does, in fact, work. Below is my code where I've attempted to follow the spec regarding parameters, sorting, URI encoding, utf8 encoding, etc. Everything 'looks' ok when examining headers/query params in either Fiddle or the REST API Log plugin (pretty great little tool, that) and yet, no love.

Thanks for any advice or direction.

Client code:

#!/usr/bin/perl

use strict;
use warnings;
use LWP::UserAgent;
use JSON;
use URI::Escape;
use Digest::HMAC_SHA1 qw( hmac_sha1 );
use MIME::Base64 qw( encode_base64 );
use Encode;
use Data::Dumper;

my $timestamp = time();
my $method    = 'GET';
my $url       = "https://hoppingmadmonkey.com/wp-json/myapiplugin/v2/greeting";

my $client_secret = "GelrnAAr1nCe5fzmTqzrU82PsfKCmKlDOZrLPakQRkH4sizJ";
my $token_secret  = "mdeVuJnrs8VSDJNJfMNPQRqBkG8xadfK0jAYjDGhmKOeaY7O";

#my $nonce = $ARGV[0];          # test with Postman generated parameters
#$timestamp = $ARGV[1];         # test with Postman generated parameters
#my $signature = $ARGV[2];      # test with Postman generated parameters

my %params = (
        oauth_consumer_key => "NCo8bflKU9LI",
        oauth_signature_method => "HMAC-SHA1",
        oauth_realm        => "https://hoppingmadmonkey.com",
        oauth_timestamp    => $timestamp,
        oauth_token        => "2GeFG7MkXliq2OBOSSCSRBPX",
        oauth_version      => "1.0",
);

$params{oauth_nonce}    = create_nonce();
#$params{oauth_nonce}    = $nonce;              # test with Postman generated parameters
$params{oauth_timestamp} = $timestamp;

my $key            = create_key($client_secret,$token_secret);
my ($params,$base) = build_base_string($method, $url, \%params);
my $signature      = create_signature($base, $key);


#for (sort keys %params) { print "$_=$params{$_}", "\n"; }
print "params: $params\n\n";
print "basestring: $base\n\n";
print "key: $key\n\n";
print "signature [$signature]\n\n";

$params{oauth_signature} = $signature;          # -- set signature for GET query string

my $request_string = build_request_string($url,\%params);
print "\nrequest_string [$request_string]\n\n";

my $ua = LWP::UserAgent->new();
$ua->default_header("Authorization", "Basic user:pass");
my $response = $ua->get($request_string);
print Dumper $response, "\n";
exit;

my $curlcmd = qq{/usr/bin/curl -i -X GET "$request_string"};
#`$curlcmd 2>&1`;


sub create_signature {
        my ($t,$k) = @_;
        my $str = encode_base64(hmac_sha1($t,$k));
        chomp $str;
        return $str;
}


# Create unique nonce
#
sub create_nonce {
        my $str = `/bin/cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 11 | head -n 1`;
        chomp $str;
        return $str;
}

# Create oauth key for generating hmac-sha1 signature
#
sub create_key {
        my ($cs,$ts) = @_;
        $cs = encode('utf8',uri_escape($cs));
        $ts = encode('utf8',uri_escape($ts));
        return "$cs&$ts";
}


# Build basestring for generating hmac-sha1 signature
#
sub build_request_string {
        my ($u,$p) = @_;
        my %params = %$p;

        my $str = $u . '?';
        my @tmp;
        for (sort keys %params) {
                $p = uri_escape($_) . '=' . uri_escape($params{$_});
                push @tmp, $p;
        }
        $str .= join '&', @tmp;
        return $str;
}


# build the base string parameter for creating the signature
#
sub build_base_string {
        my ($m,$u,$phash) = @_;
        my %params = %$phash;

        my $str = $method;
        $str .= '&' . uri_escape($u) . '&';
        my @tmp;
        for (sort keys %params) {
                push @tmp, uri_escape("$_=$params{$_}");
        }
        $params = join '&', @tmp;
        $str .= join '&', @tmp;
        return $params,$str;

}

1;

The result of running this from the command line is:

dnu [test] ::>./oauth-perl.pl
params: oauth_consumer_key%3DNCo8bflKU9LI&oauth_nonce%3DJrGdlVLXpB4&oauth_realm%3Dhttps%3A%2F%2Fhoppingmadmonkey.com&oauth_signature_method%3DHMAC-SHA1&oauth_timestamp%3D1542134239&oauth_token%3D2GeFG7MkXliq2OBOSSCSRBPX&oauth_version%3D1.0

basestring: GET&https%3A%2F%2Fhoppingmadmonkey.com%2Fwp-json%2Fmyapiplugin%2Fv2%2Fgreeting&oauth_consumer_key%3DNCo8bflKU9LI&oauth_nonce%3DJrGdlVLXpB4&oauth_realm%3Dhttps%3A%2F%2Fhoppingmadmonkey.com&oauth_signature_method%3DHMAC-SHA1&oauth_timestamp%3D1542134239&oauth_token%3D2GeFG7MkXliq2OBOSSCSRBPX&oauth_version%3D1.0

key: GelrnAAr1nCe5fzmTqzrU82PsfKCmKlDOZrLPakQRkH4sizJ&mdeVuJnrs8VSDJNJfMNPQRqBkG8xadfK0jAYjDGhmKOeaY7O

signature [2pJRR1fz0uUdUWHlUjHFWlXFbL4=]


request_string [https://hoppingmadmonkey.com/wp-json/myapiplugin/v2/greeting?oauth_consumer_key=NCo8bflKU9LI&oauth_nonce=JrGdlVLXpB4&oauth_realm=https%3A%2F%2Fhoppingmadmonkey.com&oauth_signature=2pJRR1fz0uUdUWHlUjHFWlXFbL4%3D&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1542134239&oauth_token=2GeFG7MkXliq2OBOSSCSRBPX&oauth_version=1.0]

$VAR1 = bless( {
                 '_protocol' => 'HTTP/1.1',
                 '_content' => '
{"code":"json_oauth1_signature_mismatch","message":"OAuth signature does not match","data":{"status":401}}',
                 '_rc' => '401',
                 '_headers' => bless( {
                                        'connection' => 'close',
                                        'cache-control' => 'no-cache, must-revalidate, max-age=0',
                                        'date' => 'Tue, 13 Nov 2018 18:37:19 GMT',
                                        'client-ssl-cert-issuer' => '/C=GB/ST=Greater Manchester/L=Salford/O=COMODO CA Limited/CN=COMODO RSA Domain Validation Secure Server CA',
                                        'client-ssl-cipher' => 'ECDHE-RSA-AES256-GCM-SHA384',
                                        'client-peer' => '192.185.236.193:443',
                                        'access-control-expose-headers' => 'X-WP-Total, X-WP-TotalPages',
                                        'x-robots-tag' => 'noindex',
                                        'client-warning' => 'Missing Authenticate header',
                                        'client-date' => 'Tue, 13 Nov 2018 18:37:22 GMT',
                                        'client-ssl-warning' => 'Peer certificate not verified',
                                        'content-type' => 'application/json; charset=UTF-8',
                                        'client-transfer-encoding' => [
                                                                        'chunked'
                                                                      ],
                                        'server' => 'Apache',
                                        'x-endurance-cache-level' => '2',
                                        'client-ssl-socket-class' => 'IO::Socket::SSL',
                                        'link' => '<https://hoppingmadmonkey.com/index.php/wp-json/>; rel="https://api.w.org/"',
                                        'access-control-allow-headers' => 'Authorization, Content-Type',
                                        'client-response-num' => 1,
                                        'x-content-type-options' => 'nosniff',
                                        'client-ssl-cert-subject' => '/OU=Domain Control Validated/OU=Hosted by HostGator.com, LLC./OU=PositiveSSL Wildcard/CN=*.hostgator.com',
                                        'expires' => 'Wed, 11 Jan 1984 05:00:00 GMT'
                                      }, 'HTTP::Headers' ),
                 '_msg' => 'Unauthorized',
                 '_request' => bless( {
                                        '_content' => '',
                                        '_uri' => bless( do{\(my $o = 'https://hoppingmadmonkey.com/wp-json/myapiplugin/v2/greeting?oauth_consumer_key=NCo8bflKU9LI&oauth_nonce=JrGdlVLXpB4&oauth_realm=https%3A%2F%2Fhoppingmadmonkey.com&oauth_signature=2pJRR1fz0uUdUWHlUjHFWlXFbL4%3D&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1542134239&oauth_token=2GeFG7MkXliq2OBOSSCSRBPX&oauth_version=1.0')}, 'URI::https' ),
                                        '_headers' => bless( {
                                                               'user-agent' => 'libwww-perl/5.833',
                                                               'authorization' => 'Basic user:pass'
                                                             }, 'HTTP::Headers' ),
                                        '_method' => 'GET',
                                        '_uri_canonical' => bless( do{\(my $o = 'https://hoppingmadmonkey.com/wp-json/myapiplugin/v2/greeting?oauth_consumer_key=NCo8bflKU9LI&oauth_nonce=JrGdlVLXpB4&oauth_realm=https%3A%2F%2Fhoppingmadmonkey.com&oauth_signature=2pJRR1fz0uUdUWHlUjHFWlXFbL4%3D&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1542134239&oauth_token=2GeFG7MkXliq2OBOSSCSRBPX&oauth_version=1.0')}, 'URI::https' )
                                      }, 'HTTP::Request' )
               }, 'HTTP::Response' );
$VAR2 = '
';

In the interests of expedience, I've included my real secrets and website. It's all test at the moment. Feel free to try a solution if you have that kind of time on your hands. I can always invalidate the tokens if/when they get abused and will obviously change once/if I can understand the problem.

The obvious thought is that I am not generating the signature correctly (Big Hint:

{"code":"json_oauth1_signature_mismatch","message":"OAuth signature does not match","data":{"status":401}}',

)

I think I've ready everything on the entire Internet to no avail. Thanks in advance for any thoughts. And if you are near Atlanta, GA and help me out, the beer's on me.

Cheers.

2 Answers

Try using Net::OAuth

If you really need to fix your implementation then try removing all uri_escape and testing with data that doesn't require any escaping. It looks like you have some unnecessary or double escaping. Start with build_base_string function. I thinks that = sign should be unescaped.

I could not get things working with Net::OAuth so I tried writing my code again from scratch and explicitly following the guidelines at http://lti.tools/oauth/ until my code output matched theirs. Hallelujah! It's now working.

One thing I had to add, and Postman fails to do this as well, is to url encode the signature. Makes sense, of course, but odd that Postman misses it and sends an occasional unescaped '+'.

Related