Refreshing the QuickBooks OAuth2 access token
The 1 Hour Problem
With QuickBooks OAuth 1a the access token was valid for 6 months. With respect to development, your experience might go something like this. Every 6 months or so when sandbox testing some QuickBooks API feature you would scratch your head a bit but then eventually figure out your development access token expired. You'd clear out some stuff in the database so that the QuickBooks Connect button would reappear and then reconnect to get a new access token. Rinse and repeat every 6 months.
Sometimes I felt like automating this token renewal process (like in production) but with the 6 month expiry grace period the return on investment for automation was always too low.
However, with OAuth 2, access tokens are only valid for 1 hour. This basically forces your hand to come up with development and production environment automated token renewing solutions ‐ immediately.
Proper persistence
In multi-tenant apps I usually use a qbo_accounts
table (that belongs to an accounts
table) to store credentials for an account.
Here are the minimum columns needed.
$ ap QboAccount.columns.map(&:name) [ [ 0] "id", [ 1] "access_token", [ 2] "refresh_token", [ 3] "companyid", [ 4] "account_id", [ 5] "created_at", [ 6] "updated_at", [ 7] "token_expires_at", [ 8] "reconnect_token_at", ]
You should be encrypting the access_token
, refresh_token
, and the companyid
columns at the database level for which I leverage the attr_encrypted gem.
Persisting a OAuth2 response
When the OAuth2 response comes back persist it being sure to set the token_expires_at
attribute for 60 minutes and the reconnect_token_at
attribute for 50 minutes.
account.create_qbo_account( access_token: response.access_token, refresh_token: response.refresh_token, companyid: params[:realmId], token_expires_at: 60.minutes.from_now, reconnect_token_at: 50.minutes.from_now )
Renewing Rake task
namespace :quickbooks do task :renew_oauth2_tokens => :environment do if (qbo_accounts = QboAccount.where('reconnect_token_at <= NOW() AND token_expires_at >= NOW()')).empty? p "OAUTH2_RENEW_TOKEN: nothing to do" else qbo_accounts.each do |q| begin client = oauth2_client client.refresh_token = q.refresh_token if resp = client.access_token! duration_attrs = { reconnect_token_at: 1.hour.from_now, token_expires_at: 50.minutes.from_now } attrs = { token: resp.access_token, refresh_token: resp.refresh_token }.merge(duration_attrs) if q.update(attrs) p "SUCCESS_OAUTH2_RENEW_TOKEN: qbo_account: #{q.id}" else p "FAILED_OAUTH2_RENEW_TOKEN: qbo_account: #{q.id} error_message: #{resp}" end end rescue => e p "FAILED_OAUTH2_RENEW_TOKEN: qbo_account: #{q.id} error_message: #{e.message}" end end end end def oauth2_client Rack::OAuth2::Client.new( identifier: ENV['QBO_API_CLIENT_ID'], secret: ENV['QBO_API_CLIENT_SECRET'], redirect_uri: 'http://localhost:3000/accounts/oauth2_callback', authorization_endpoint: "https://appcenter.intuit.com/connect/oauth2", token_endpoint: "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer" ) end end end
The reason I set the reconnect_token_at
duration at 50 minutes is so to NOT run the refresh requests excessively. Moreover, we don't want to keep running refresh requests if there is no chance of renewal because the access_token
expiry date is past. Therefore, I am only executing a refresh request when inside of this 10 minute window.
The access_token
remains static on a successful refresh, it does not change.
The renew_oauth2_tokens
rake task uses the rack-oauth2 gem.
Hook Up The Renew Rake Task in Development
For development I use OSX so I will detail making a LaunchD task but for Linux you, of course, have Cron, which I use in production. I am not up to speed on Windows OS but leave a recipe in the comments and I will include it in the article.
$ cd ~/Library/LaunchAgents
$ vim com.minimul.bsmash.oauth2.renew.plist
- Example plist
- Save the .plist task.
$ launchctl load com.minimul.bsmash.oauth2.renew.plist
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>Label</key> <string>com.minimul.bsmash.oauth2.renew</string> <key>ProgramArguments</key> <array> <string>/bin/bash</string> <string>-l</string> <string>-c</string> <string> cd /Users/minimul/www/projects/bordersmash; bin/rake quickbooks:renew_oauth2_tokens </string> </array> <key>StartInterval</key> <integer>300</integer> <key>RunAtLoad</key> <true/> </dict> </plist>
This task will run at boot and then every 5 minutes.
Note: In development I use chruby and ruby-install for Ruby management so the above plist may not work when using RVM or rbenv but the /bin/bash -l -c
prefix should properly load the intended Ruby environment for all managers.
Production
In production I use the Whenever gem in combination with Cron. So here is an example of my config/schedule.rb
set :output, "/var/log/cron" every 5.minutes do rake "quickbooks:renew_oauth2_tokens" end
Problem Solved, Sort of
The beauty of this setup is that you'll never have to worry about your QuickBooks API tokens expiring. However, that presupposes that your local development environment is always running. Have your laptop shutdown for over an hour and your access tokens will all be invalidated.
You also must have a production renew job in place from a day one (of your integration), which with OAuth 1a you could have waited a few months before setting that up. Moreover, do you plan on having a maintenance outage for over an hour on your production app? Great all of your app's OAuth 2 access tokens just became invalid. What if Intuit itself has even a brief outage on their OAuth 2 server(s)?
For these reasons I suspect Intuit will raise the expiry duration at some point. Until then, however, if you, your company or client has an existing (created before July 17th) Intuit developer account that supports OAuth 1a don't be quick to create a new Intuit Developer account meaning to switch your integration to OAuth 2 as this 60 minute expiry time will surely come back to bite you.
Lastly, if you insist on implementing OAuth 2 or have a new Developer account and must implement it, consider purchasing my book as I have this OAuth 2 integration in much more detail and you'll be helping to support this educational resource site.
- Pushed on 08/19/2017 by Christian
- QuickBooks Integration Consulting