Go back to the main page

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",
]
Minimul says —

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
Minimul says —

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.

  1. $ cd ~/Library/LaunchAgents
  2. $ vim com.minimul.bsmash.oauth2.renew.plist
  3. Example plist
  4. <?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>
    
  5. Save the .plist task.
  6. $ launchctl load com.minimul.bsmash.oauth2.renew.plist
Minimul says —

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.