Interfacing to Xero using Oauth 1.0 without a big wrapper

Simple Oauth 1.0 Xero Adapter in Python

So I ported my Lua oauth 1.0 example over to python on Friday morning just to complete things. I think this is fastest way you have of getting a working example with the Xero web API for a private application with the least amount of dependencies to install. Keep in mind though that if you are looking for a complete wrapper library that this isn’t it. It’s the simplest example possible of querying the Xero API and is intended just to provide a transparent jumping off point for understanding how the Xero API works.

This script doesn’t use python’s native libraries to do the certificate signing or URL invocation. Instead it makes use of two command line tools:

  • openssl.
  • curl

If you are running on OS X (Apple) or Linux then you’re in luck because these tools are pre-installed. In fact you also have python pre-installed.

To check for these dependencies you can do this at the command line – this shows what to expect if you have a mac (OS X):

eliotmac:xero2 eliot2$ openssl version
OpenSSL 0.9.8zg 14 July 2015
eliotmac:xero2 eliot2$ curl --version
curl 7.43.0 (x86_64-apple-darwin14.0) libcurl/7.43.0 SecureTransport zlib/1.2.5
Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtsp smb smbs smtp smtps telnet tftp 
Features: AsynchDNS IPv6 Largefile GSS-API Kerberos SPNEGO NTLM NTLM_WB SSL libz UnixSockets 
eliotmac:xero2 eliot2$ python --version
Python 2.7.10
eliotmac:xero2 eliot2$

These tools are not pre-loaded on windows so you will have to find them and install them. If you manage to get them working validly then this is what you should be able to do from the command line:

Y:\xero2>openssl version
OpenSSL 1.0.1k 8 Jan 2015 
Y:\xero2>curl --version
curl 7.45.0 (x86_64-pc-win32) libcurl/7.45.0 OpenSSL/1.0.2d zlib/1.2.8 WinIDN libssh2/1.6.0 Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtsp scp sftp smtp smtps telnet tftp Features: AsynchDNS IDN IPv6 Largefile SSPI Kerberos SPNEGO NTLM SSL libz 
Y:\xero2>python --version
Python 2.7.9 

Follow Steps 1 and 2 as I documented earlier:

Step 1

Use openssl to generate the public and private key as described by Xero’s documentation:

https://developer.xero.com/documentation/api-guides/create-publicprivate-key

Only the PEM files are needed so one can skip the third step to make a pfx file since this isn’t needed.

Step 2

Add a private application to the Xero instance that the API will access. It’s necessary to upload the public key as described by Xero’s documentation:

https://developer.xero.com/documentation/auth-and-limits/private-applications

And then in the same directory you created the private key PEM file from Step 1, create a python file like test.py with this contents:

#!/usr/bin/python 
import time, os, base64, urllib, random, collections; 
def OutputTable(Name, Table): 
   print Name; 
   for key, value in Table.iteritems(): 
      print " ", key, ": ", value;
 
def OauthEscape(Value):
   Result = urllib.quote_plus(Value);
   Result = Result.replace("+", "%20"); # cheap hack
   return Result; 

def ConcatenateSigParams(Params): 
   X = ""; 
   for K,V in Params.iteritems(): 
      X = X+K+'='+OauthEscape(V)+'&' 
   return X[:-1]; # remove the last character 

def AuthInfo(Auth): 
   R = '' for K,V in Auth.iteritems(): 
   R = R+' '+K+'="'+V+'",' 
   return R 

CONSUMER_KEY='C00WGMXDTS5QSXWVN5WDOAJ1JHBRKA'
Url = 'https://api.xero.com/api.xro/2.0/Contacts'
Params = {} 
Params['where'] = 'Name == "Espresso 31"'

Headers = {}
Headers['Accept'] = 'application/json'

Auth = {} 
Auth['oauth_nonce'] = str(int(time.time())) + str(int(random.random()*1000000)); 
Auth['oauth_timestamp'] = str(int(time.time())); 
Auth['oauth_version'] = '1.0'; 
Auth['oauth_signature_method'] = 'RSA-SHA1'; 
Auth['oauth_consumer_key'] = CONSUMER_KEY; 
Auth['oauth_token'] = CONSUMER_KEY; 

# Merge GET params with oauth params
AllParams = {} 
for key, value in Params.iteritems(): 
   AllParams[key] = value; 
for key, value in Auth.iteritems(): 
   AllParams[key] = value; 

OrderedParams = collections.OrderedDict(sorted(AllParams.items())); 
SortedParamAuthString = ConcatenateSigParams(OrderedParams); 
SignatureText = 'GET&'+OauthEscape(Url)+"&"+OauthEscape(SortedParamAuthString);
 
temp = open("text.txt", "wb"); 
temp.write(SignatureText); 
temp.close(); 
os.system("cat text.txt | openssl dgst -sha1 -sign privatekey.pem -binary > signature.bin") 
SignatureBinary = open( "signature.bin", 'r' ).read() 
Signature=base64.b64encode(SignatureBinary); 

Headers['Authorization'] = 'OAuth' + AuthInfo(Auth) + ' oauth_signature="' + OauthEscape(Signature) + '"' 
print "==========================================================================" 
OutputTable('Params', Params); 
print "=========================================================================="
OutputTable('Oauth Headers', Auth); 
print "==========================================================================" 
OutputTable('All Parameters', AllParams); 
print "==========================================================================" 
OutputTable('Ordered Parameters', OrderedParams); 
print "==========================================================================" 
OutputTable('Headers', Headers); 
print "==========================================================================" 
print "Sorted Parmam String: ", SortedParamAuthString; 
print "==========================================================================" 
print "Signature Text: ", SignatureText; 
print "==========================================================================" 
print "Signature : ", Signature; 
print "==========================================================================" 
CurlCommand = "curl" 

# We have to escape the " characters to \" for the windows command line
for key, value in Headers.iteritems(): 
   CurlCommand += ' --header "' + key + ": " + value.replace('"', '\"') + "'" 

CurlCommand += " " + Url + "?"; 
# Add the GET variables 
for key, value in Params.iteritems(): 
   CurlCommand += key + "=" + urllib.quote_plus(value) + "&"; 
# Strip off the last & character 
CurlCommand = CurlCommand[:-1] 

print CurlCommand; 
os.system(CurlCommand);

You should be able to run the script with:

python test.py

And it should produce something like this output:

eliotmac:xero2 eliot2$ python test.py
==========================================================================
Params
 where : Name == "Espresso 31"
==========================================================================
Oauth Headers
 oauth_nonce : 144682653117482
 oauth_timestamp : 1446826531
 oauth_consumer_key : C00WGMXDTS5QSXWVN5WDOAJ1JHBRKA
 oauth_signature_method : RSA-SHA1
 oauth_version : 1.0
 oauth_token : C00WGMXDTS5QSXWVN5WDOAJ1JHBRKA
==========================================================================
All Parameters
 oauth_nonce : 144682653117482
 oauth_timestamp : 1446826531
 oauth_consumer_key : C00WGMXDTS5QSXWVN5WDOAJ1JHBRKA
 oauth_signature_method : RSA-SHA1
 oauth_version : 1.0
 oauth_token : C00WGMXDTS5QSXWVN5WDOAJ1JHBRKA
 where : Name == "Espresso 31"
==========================================================================
Ordered Parameters
 oauth_consumer_key : C00WGMXDTS5QSXWVN5WDOAJ1JHBRKA
 oauth_nonce : 144682653117482
 oauth_signature_method : RSA-SHA1
 oauth_timestamp : 1446826531
 oauth_token : C00WGMXDTS5QSXWVN5WDOAJ1JHBRKA
 oauth_version : 1.0
 where : Name == "Espresso 31"
==========================================================================
Headers
 Authorization : OAuth oauth_nonce="144682653117482", oauth_timestamp="1446826531", oauth_consumer_key="C00WGMXDTS5QSXWVN5WDOAJ1JHBRKA", oauth_signature_method="RSA-SHA1", oauth_version="1.0", oauth_token="C00WGMXDTS5QSXWVN5WDOAJ1JHBRKA", oauth_signature="gTGkyfwAatHNRvCH%2BTBatee%2B2Y%2BhRf867Lf8KBUcR2AKX3wb%2Fn5natMtA49CGa182MrKhS%2Br%2BMcdLiMAUFWTuEwkctwPNPFgKaPicvuEkHgqiEEIjtoR%2FrGuuIyMxkyYOaxezVgvjW4ZrbPhJ1j3SetVFtnq0zCcgguja7WiXWY%3D"
 Accept : application/json
==========================================================================
Sorted Parmam String: oauth_consumer_key=C00WGMXDTS5QSXWVN5WDOAJ1JHBRKA&oauth_nonce=144682653117482&oauth_signature_method=RSA-SHA1&oauth_timestamp=1446826531&oauth_token=C00WGMXDTS5QSXWVN5WDOAJ1JHBRKA&oauth_version=1.0&where=Name%20%3D%3D%20%22Espresso%2031%22
==========================================================================
Signature Text: GET&https%3A%2F%2Fapi.xero.com%2Fapi.xro%2F2.0%2FContacts&oauth_consumer_key%3DC00WGMXDTS5QSXWVN5WDOAJ1JHBRKA%26oauth_nonce%3D144682653117482%26oauth_signature_method%3DRSA-SHA1%26oauth_timestamp%3D1446826531%26oauth_token%3DC00WGMXDTS5QSXWVN5WDOAJ1JHBRKA%26oauth_version%3D1.0%26where%3DName%2520%253D%253D%2520%2522Espresso%252031%2522
==========================================================================
Signature : gTGkyfwAatHNRvCH+TBatee+2Y+hRf867Lf8KBUcR2AKX3wb/n5natMtA49CGa182MrKhS+r+McdLiMAUFWTuEwkctwPNPFgKaPicvuEkHgqiEEIjtoR/rGuuIyMxkyYOaxezVgvjW4ZrbPhJ1j3SetVFtnq0zCcgguja7WiXWY=
==========================================================================
curl --header 'Authorization: OAuth oauth_nonce="144682653117482", oauth_timestamp="1446826531", oauth_consumer_key="C00WGMXDTS5QSXWVN5WDOAJ1JHBRKA", oauth_signature_method="RSA-SHA1", oauth_version="1.0", oauth_token="C00WGMXDTS5QSXWVN5WDOAJ1JHBRKA", oauth_signature="gTGkyfwAatHNRvCH%2BTBatee%2B2Y%2BhRf867Lf8KBUcR2AKX3wb%2Fn5natMtA49CGa182MrKhS%2Br%2BMcdLiMAUFWTuEwkctwPNPFgKaPicvuEkHgqiEEIjtoR%2FrGuuIyMxkyYOaxezVgvjW4ZrbPhJ1j3SetVFtnq0zCcgguja7WiXWY%3D"' --header 'Accept: application/json' https://api.xero.com/api.xro/2.0/Contacts?where=Name+%3D%3D+%22Espresso+31%22
{
 "Id": "6d5f6c79-0fad-45aa-a4f3-76319ee706fa",
 "Status": "OK",
 "ProviderName": "test",
 "DateTimeUTC": "\/Date(1446826532272)\/",
 "Contacts": [
 {
 "ContactID": "f5cb9bf0-7aa2-473e-8115-87863dee95f3",
 "ContactStatus": "ACTIVE",
 "Name": "Espresso 31",
 "Addresses": [
 {
 "AddressType": "POBOX"
 },
 {
 "AddressType": "STREET"
 }
 ],
 "Phones": [
 {
 "PhoneType": "DDI"
 },
 {
 "PhoneType": "DEFAULT"
 },
 {
 "PhoneType": "FAX"
 },
 {
 "PhoneType": "MOBILE"
 }
 ],
 "UpdatedDateUTC": "\/Date(1445676447587+1300)\/",
 "ContactGroups": [],
 "IsSupplier": false,
 "IsCustomer": false,
 "ContactPersons": [],
 "HasAttachments": false,
 "HasValidationErrors": false
 }
 ]

I put print statements through the whole script to make it transparent what is happening. To be clear this is not a wrapper library – this is merely a code example to try and transparently show how to authenticate with the Xero API. You’d need to be competent in your language of choice to pick this code up and run with it – but hopefully it’s useful in showing clearly how everything works with as little abstraction as possible.

Now since the reason I am doing this is to get data into excel I made the unwise decision to port this code over into Visual Basic for Applications…

Tagged: