Friday, November 14, 2014

Discogs OAuth Authorization Headers

Update Feb 1, 2016

In case the original post wasn't clear, I've used CURLOPT_VERBOSE to record the actual headers sent in the 'https://api.discogs.com/oauth/identity' request. Most valus are redacted as xxxxxxxx except for oauth_signature which is compsed of consumer_secret and user_secret where consumer_secret is redacted as aaaa and consumer_secret is redacted as bbbb. Note the ampersand. This means you can't blanket escape the values you put into your headers, since most escape routines would mangle the ampersand.

> GET /oauth/identity HTTP/1.1
Host: api.discogs.com
Accept: */*
Referer: https://holtstrom.com/xxxxxxxx.php
Content-Type: application/x-www-form-urlencoded
Authorization: OAuth oauth_consumer_key="xxxxxxxx", oauth_nonce="1454382050", oauth_token="xxxxxxxx", oauth_signature="aaaa&bbbb", oauth_signature_method="PLAINTEXT", oauth_timestamp="1454382050"
User-Agent: HoltstromLifeCounter/0.1 +http://holtstrom.com/michael

< HTTP/1.1 200 OK
< Server: nginx/1.8.0
< Date: Tue, 02 Feb 2016 03:00:50 GMT
< Content-Type: application/json
< Content-Length: 143
< Connection: keep-alive
< X-Discogs-Media-Type: discogs.v2
< Cache-Control: public, must-revalidate
< Access-Control-Allow-Origin: *
<
* Connection #0 to host api.discogs.com left intact

Update Jan 17, 2016

Your server may return error code 0 on your API calls. This could be due to SSL negotiation. You can discover this by turning on verbose CURL logs, like the following:

$options = array( CURLOPT_CUSTOMREQUEST => 'GET'
                 , CURLOPT_URL => 'https://api.discogs.com/oauth/identity' // this url gives info on the user that is authorized
                 , CURLOPT_HTTPHEADER => $headers // the complicated stuff
                 , CURLOPT_REFERER => $referer // some APIs reject requests that don't supply a referer
                 , CURLOPT_RETURNTRANSFER => 1 // means output will be a return value from curl_exec() instead of simply echoed
                 , CURLOPT_TIMEOUT => 4 // max seconds to wait
                 , CURLOPT_VERBOSE => true
                 , CURLOPT_STDERR => fopen("/tmp/curl.log", "w+")
                 );          

Or you can hit the URL directly from the command line:

$ curl --verbose https://api.discogs.com/database/search?q=Nirvana
*   Trying 216.151.17.131...
* Connected to api.discogs.com (216.151.17.131) port 443 (#0)
* Initializing NSS with certpath: sql:/etc/pki/nssdb
*   CAfile: /etc/pki/tls/certs/ca-bundle.crt
  CApath: none
* NSS error -12286 (SSL_ERROR_NO_CYPHER_OVERLAP)
* Cannot communicate securely with peer: no common encryption algorithm(s).
* Closing connection 0

Fixing that problem is tricky. One option is to just use http:// instead of https:// but then everyone can see your secret token.

Original Post

I've been using the Discogs API for a while now in my Life Counter App, and the /database/search function recently stopped working, apparently because they now require authentication. The discogs documentation isn't half bad, but it doesn't give the full solution, and as can be seen from the drupal post has left many users frustrated.

I won't comment on the security of OAuth, but just fill in the blanks where the discogs documentation falls short.

When discogs says that some APIs (like search) require authentication, they're just saying that you have to include some extra headers with each request. Here's a PHP example.

$headers = array( 'Content-Type: application/x-www-form-urlencoded'
                 , 'Authorization: OAuth '
                 .   'oauth_consumer_key="'.$consumer_key.'"'
                 . ', oauth_nonce="'.time().'"'
                 . ', oauth_token="'.$user_key.'"'
                 . ', oauth_signature="'.$consumer_secret.'&'.$user_secret.'"'
                 . ', oauth_signature_method="PLAINTEXT"' // plaintext means the signature is just the consumer and user secret
                 . ', oauth_timestamp="'.time().'"'
                 , 'User-Agent: '.$agent // discogs requres an application specific user agent
                 );
$options = array( CURLOPT_CUSTOMREQUEST => 'GET'
                 , CURLOPT_URL => 'https://api.discogs.com/oauth/identity' // this url gives info on the user that is authorized
                 , CURLOPT_HTTPHEADER => $headers // the complicated stuff
                 , CURLOPT_REFERER => $referer // some APIs reject requests that don't supply a referer
                 , CURLOPT_RETURNTRANSFER => 1 // means output will be a return value from curl_exec() instead of simply echoed
                 , CURLOPT_TIMEOUT => 4 // max seconds to wait
                 );                                                     
$curl = curl_init();
curl_setopt_array( $curl, $options );
$response = curl_exec($curl);
$http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);  

The above would generate a request like the following:

GET https://api.discogs.com/oauth/identity 
Content-Type: application/x-www-form-urlencoded
Authorization: OAuth oauth_consumer_key="aaaa", oauth_nonce="1416023008", oauth_token="xxxx", oauth_signature="bbbb&yyyy", oauth_signature_method="PLAINTEXT", oauth_timestamp="1416023008"
User-Agent: YourApp/0.1 +http://yoursite.com/yourapp

And a response like the following:

code = 200
response = {"username": "yourusername", "resource_url": "https://api.discogs.com/users/yourusername", "consumer_name": "Your App", "id": 123456}

You can just make up the $agent. But you need to register an app to get the $consumer_key and $consumer_secret. And you need a user to authorize your app to get the $user_key and $user_secret.

If you haven't done so already, you need to create a new discogs account, then in the developer settings you can create an application which will give you a $consumer_key and $consumer_secret. These identify your application.

To use the API, you need a user that has authorized your application to use the API with their account. The request/authorize/access URLs let you establish this authorization which result in a key and secret that never expire (until the user revokes them). Luckily you have a discogs user account and can authorize your own app.

Here's how it works. First you need to ask discogs for a temporary token.

$headers = array( 'Content-Type: application/x-www-form-urlencoded'
                 , 'Authorization: OAuth '
                 .   'oauth_consumer_key="'.$consumer_key.'"'
                 . ', oauth_nonce="'.time().'"'
                 . ', oauth_signature="'.$consumer_secret.'&"' // don't forget the trailing &
                 . ', oauth_signature_method="PLAINTEXT"' // plaintext means the signature is just the consumer and user secret
                 . ', oauth_timestamp="'.time().'"'
                 , 'User-Agent: '.$agent // discogs requres an application specific user agent
                 );
$options = array( CURLOPT_CUSTOMREQUEST => 'GET'
                 , CURLOPT_URL => 'https://api.discogs.com/oauth/request_token'
                 , CURLOPT_HTTPHEADER => $headers // the complicated stuff
                 , CURLOPT_REFERER => $referer // some APIs reject requests that don't supply a referer
                 , CURLOPT_RETURNTRANSFER => 1 // means output will be a return value from curl_exec() instead of simply echoed
                 , CURLOPT_TIMEOUT => 4 // max seconds to wait
                 );                                                     
$curl = curl_init();
curl_setopt_array( $curl, $options );
$response = curl_exec($curl);
$http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);  

That request looks like this:

GET https://api.discogs.com/oauth/request_token
Content-Type: application/x-www-form-urlencoded
Authorization: OAuth oauth_consumer_key="aaaa", oauth_nonce="1416023008", oauth_signature="bbbb&", oauth_signature_method="PLAINTEXT", oauth_timestamp="1416023008"
User-Agent: YourApp/0.1 +http://yoursite.com/yourapp

And results in a temporary token:

code = 200
response = oauth_token_secret=tttt&oauth_token=ssss

Then, in a browser, visit http://discogs.com/oauth/authorize?oauth_token=your_oauth_token (i.e. http://discogs.com/oauth/authorize?oauth_token=ssss), which (once you've logged in as your discogs user) will ask if you're willing to authorize your app. Click the authorize button, and it will display a code like zzzz.

Then you need to ask discogs for a permanent key and secret so that your app can act on behalf of the user.

$headers = array( 'Content-Type: application/x-www-form-urlencoded'
                 , 'Content-Length: 0'
                 , 'Authorization: OAuth '
                 .   'oauth_consumer_key="'.$consumer_key.'"'
                 . ', oauth_nonce="'.time().'"'
                 . ', oauth_token="'.$oauth_token_from_request_token_api.'"'
                 . ', oauth_signature="'.$consumer_secret.'&'.$oauth_token_secret_from_request_token_api.'"' // don't forget the trailing &
                 . ', oauth_signature_method="PLAINTEXT"' // plaintext means the signature is just the consumer and user secret
                 . ', oauth_timestamp="'.time().'"'
                 . ', oauth_verifier="'.$code_from_discogs_webpage.'"'
                 , 'User-Agent: '.$agent // discogs requres an application specific user agent
                 );
$options = array( CURLOPT_CUSTOMREQUEST => 'POST'
                 , CURLOPT_URL => 'https://api.discogs.com/oauth/access_token'
                 , CURLOPT_HTTPHEADER => $headers // the complicated stuff
                 , CURLOPT_REFERER => $referer // some APIs reject requests that don't supply a referer
                 , CURLOPT_RETURNTRANSFER => 1 // means output will be a return value from curl_exec() instead of simply echoed
                 , CURLOPT_TIMEOUT => 4 // max seconds to wait
                 );                                                     
$curl = curl_init();
curl_setopt_array( $curl, $options );
$response = curl_exec($curl);
$http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);  

That request looks like this:

POST https://api.discogs.com/oauth/access_token
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
Authorization: OAuth oauth_consumer_key="aaaa", oauth_nonce="1416023008", oauth_token="ssss", oauth_signature="bbbb&tttt", oauth_signature_method="PLAINTEXT", oauth_timestamp="1416023008" oauth_verifier="zzzz"
User-Agent: YourApp/0.1 +http://yoursite.com/yourapp

And results in a permanent key and secret:

code = 200
response = oauth_token_secret=yyyy&oauth_token=xxxx

Now you have everything you need for the identity example a the top of this post.

Note

While trying to get this working I found an Example OAuth access to the Discogs API on GitHub which used OAuthSimple which didn't work for me. The discogs documentation suggests you use oauth_signature_method="PLAINTEXT" and as you can see from my above examples that means no crypto is performed at all, you just pass back and forth a few secrets. This means its is essential that you only use https connections and never put the secret tokens in a query string as those often end up in server logs.

I think the reason the github example failed was that I created the tokens with oauth_signature_method="PLAINTEXT" as directed by the discogs documentation, but and then tried to use them with the default OAuthSimple oauth_signature_method, which being something else, wasn't compatible with the tokens.

web
{ "loggedin": false, "owner": false, "avatar": "", "render": "nothing", "trackingID": "UA-36983794-1", "description": "", "page": { "blogIds": [ 518 ] }, "domain": "holtstrom.com", "base": "\/michael", "url": "https:\/\/holtstrom.com\/michael\/", "frameworkFiles": "https:\/\/holtstrom.com\/michael\/_framework\/_files.4\/", "commonFiles": "https:\/\/holtstrom.com\/michael\/_common\/_files.3\/", "mediaFiles": "https:\/\/holtstrom.com\/michael\/media\/_files.3\/", "tmdbUrl": "http:\/\/www.themoviedb.org\/", "tmdbPoster": "http:\/\/image.tmdb.org\/t\/p\/w342" }