This project is for XeroAPI developers who are migrating their Partner Application to the new standard OAuth2.0 authorization flow. It is a low dependency script that sets up the required OAuth1.0a headers, POST's to Xero's migration endpoint, and returns a JSON array of newly minted OAuth2.0 tokens.
To use this script to migrate your OAuth1.0a XeroAPI tokens you need to:
Replace 2 files
- Rename
privatekey.pem.sample
toprivatekey.pem
and replace with your own private key - Replace
oauth1_tokens.json
with an array of valid OAuth1.0a tokens
Replace 4 variables in
migration.rb
- CONSUMER_KEY
- SCOPES
- OAUTH2_CLIENT_ID
- OAUTH2_CLIENT_SECRET
Read more about Xero scopes to ensure you are only requiring the neccesary user permissions to your new access_token.
Requires ruby 2.7.0
ruby migration.rb
Running the script should output your new OAuth2.0 token_sets in a file oauth2_tokens.json
and print out JSON of your converted tokens to STDOUT.
With 1 recently refreshed OAuth1.0a token:
[
{ "token": "xxxxxxxxxxxxxxxxxxxxx" }
]
Should write to oauth2_tokens.json
and return an array of OAuth2.0 token_sets
when parsed look like:
[
{
"access_token": "xxxxxxxxxx.xxxxxxxxxx",
"refresh_token": "xxxxxxxxxx",
"expires_in": "1800",
"token_type": "Bearer",
"xero_tenant_id": "xxxx-xxxx-xxxx-xxxx-xxxxxxxxx"
}
]
If you have multiple valid OAuth1.0a tokens you want to migrate simply add to the input file.
{ "token": "xxxxxxxxxxxxxxxxxxxxx" },
{ "token": "xxxxxxxxxxxxxxxxxxxxx" }
Get the new token_sets
into your system and make sure to configure your new API calls to use the new access_token
& xero_tenant_id
header. We have a set of supported SDK's to help make OAuth2.0 API calls and new user authentication easier.
š„³
If you want to see a step through explanation of everything in the migrate.rb
file i've outlined every step so you could know how to build up OAuth1.0a headers in any other language.
You will need to track down the following variables from your OAuth1.0a, and OAuth2.0 applications.
require 'securerandom'
TOKEN = 'VALID_OAUTH_10A_ACCESS_TOKEN'
CONSUMER_KEY = 'YOUR_CONSUMER_KEY'
SCOPES = 'offline_access accounting.transactions accounting.settings'
OAUTH2_CLIENT_ID = 'YOUR_OAUTH20_CLIENT_ID'
OAUTH2_CLIENT_SECRET = 'YOUR_OAUTH20_CLIENT_SECRET'
SIGNATURE_METHOD = 'RSA-SHA1'
ENDPOINT = 'https://api.xero.com/oauth/migrate'
NONCE = SecureRandom.uuid
TIMESTAMP = Time.now.getutc.to_i.to_s
Interpolate params into the base string that will be encrypted, encoded and passed to the API to validate our API call. I've already lexicographically ordered / alphabetized all our required parameters.
base_params = "oauth_consumer_key=#{CONSUMER_KEY}" +
"&oauth_nonce=#{NONCE}" +
"&oauth_signature_method=#{SIGNATURE_METHOD}" +
"&oauth_timestamp=#{TIMESTAMP}" +
"&oauth_token=#{TOKEN}" +
"&oauth_version=1.0" +
"&tenantType=ORGANISATION"
base_params
=> "oauth_consumer_key=YOUR_CONSUMER_KEY&oauth_nonce=1cbf3d69-d478-4956-b574-a4c6c4a4b2c4&oauth_signature_method=RSA-SHA1&oauth_timestamp=1591214409&oauth_token=VALID_OAUTH_10A_ACCESS_TOKEN&oauth_version=1.0&tenantType=ORGANISATION"
We can now finalize the format of our base_signature_string which is built up with 3 main components:
- Uppercase HTTP Method
- URL encoded Base URI
- URL encoded base_params
require 'uri'
signature_base_string = "POST&" + # Uppercase HTTP method
"#{URI.encode_www_form_component(ENDPOINT)}&" + # Base URI
URI.encode_www_form_component(base_params).to_s # OAuth parameter string
Returns the following structured partially url encoded stringsignature_base_string
signature_base_string
=> "POST&https%3A%2F%2Fapi.xero.com%2Foauth%2Fmigrate&oauth_consumer_key%3DVCQSO0TYNV3I33Z4LOHD4UXGVKZNPQ%26oauth_nonce%3D1cbf3d69-d478-4956-b574-a4c6c4a4b2c%26oauth_signature_method%3DRSA-SHA1%26oauth_timestamp%1591214409%26oauth_token%TOKEN%26oauth_version%3D1.0%26tenantType%3DORGANISATION"
We now have the signature_base_string
we can sign using our private key that is associated with OAuth1.0a Partner app's public cert that we previously uploaded to our Partner app https://developer.xero.com/myapps/details?appId= dashboard.
If you are unable to track this down you can always regenerate a new set, re-upload public cert to the Xero app dash & put the private key on your server / in this script.
We then sign our base_signature_string with the SHA-1
digest and our privatekey.pem
.
The resulting signature being Base64
then URL
encoded.
require 'uri'
require 'base64'
require 'openssl'
require 'digest/sha1'
signing_key = File.read('./privatekey.pem')
rsa_key = OpenSSL::PKey::RSA.new signing_key
digest = OpenSSL::Digest::SHA1.new
signature = rsa_key.sign(digest, signature_base_string)
base64_signature = Base64.strict_encode64(signature).chomp.gsub(/\n/, '')
oauth_signature = URI.encode_www_form_component(base64_signature)
oauth_signature
=> "dkkQVrBsTfWqCatt4xxxxxe3Aitmje5jtjjoWxxxZl%2BuriiCjY%2Fe%2FgM6B0ogG%f4LKCJVPaS9Y6atX8734xxxz0hLhVREIDtNFEb%2BpxxxejeI%3D"
Now we can format our final API call header & body parameters
authorization_headers = "OAuth oauth_consumer_key='#{CONSUMER_KEY}',
oauth_nonce='#{NONCE}',
oauth_signature_method='#{SIGNATURE_METHOD}',
oauth_timestamp='#{TIMESTAMP}',
oauth_token='#{TOKEN}',
oauth_version='1.0',
tenantType='ORGANISATION',
oauth_signature='#{oauth_signature}'".gsub("\n",' ')
params = {
"scope": "#{SCOPES}",
"client_id": "#{OAUTH2_CLIENT_ID}",
"client_secret": "#{OAUTH2_CLIENT_SECRET}"
}
authorization_headers
=> "OAuth oauth_consumer_key='YOUR_CONSUMER_KEY', oauth_nonce='1cbf3d69-d478-4956-b574-a4c6c4a4b2c4', oauth_signature_method='RSA-SHA1', oauth_timestamp='1591214409', oauth_token='VALID_OAUTH_10A_ACCESS_TOKEN', oauth_version='1.0', tenantType='ORGANISATION', oauth_signature='dkkQVrBsTfWqCatt4xxxxxe3Aitmje5jtjjoWxxxZl%2BuriiCjY%2Fe%2FgM9M9XW75DeRXX1xxxI6B0ogGylF9myRTv6KhDpEpxxxtdaJ0b2LdcWbODHhLP98%f4LKCJVPaS9Y6atX8734xxxz0hLhVREIDtNFEb%2BpxxxejeI%3D'"
params
=> { scope: "offline_access accounting.transactions accounting.settings", client_id: "YOUR_OAUTH20_CLIENT_ID", client_secret: "YOUR_OAUTH20_CLIENT_SECRET" }
Finally we are ready to exchange our OAuth1.0a token for an OAuth2.0 token_set
- Format the
Authorization: header
- Add the POST body in json format
- Make the API call
require 'uri'
require 'json'
require 'net/http'
uri = URI.parse(ENDPOINT)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
headers = {'Content-Type' =>'application/json', 'Authorization': authorization_header}
request = Net::HTTP::Post.new(uri.request_uri, headers)
request.body = params.to_json
response = http.request(request)
puts JSON.parse(response.body)
{
"access_token":"xxxxxx.xxxxxx.xxxxxx-xxxxx-xxxxx-xxxxx",
"refresh_token":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"expires_in":"1800",
"token_type":"Bearer",
"xero_tenant_id":"xxxxxx-xxxx-xxxx-xxxx-xxxxxxxx"
}
You are now the proud owner of OAuth2.0 token_set(s) for your XeroAPI connections!
If you were to head over to https://jwt.io/ and decode the new access_token you can see some interesting info regarding your new connection.
You will also see there is a new very important field returned in the OAuth2.0 token_set: "xero_tenant_id": "xxx-xxx-xxx-xxx"
.
This is the largest difference between the two authorization gateways. We now have the ability to have multiple organisations authenticated by a user under the same "access_token". Due to this enhancement each API call will need to have the xero_tenant_id specified in the header. Fortunately we have a suite of Xero supported SDK's that make this easy. They also include tooling for your new signups to authorize and return valid token_sets back to your application.