Simple Oauth 1.0 Xero Adapter in Python
Contents
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…