Migrating a QuickBooks Online App from OAuth1 to OAuth2 using the qbo_api gem - Step 1
OAuth2 Renewal Strategy
The QuickBooks Online API OAuth2 access token's are only good for 1 hour. This is a radical departure from OAuth1's 6 month expiry duration. With that short a window probably the best way to ensure uninterrupted usage is to utilize a code pattern for all QBO request that does the following:
- Catches "Unauthorized" errors (status code 401 — i.e. an invalid access token).
- Next, automatically renews and persists the access token.
- Lastly, replays the failed 401 request.
In the screencast, I'll be focusing on this approach. See the show notes below for an example code pattern that will make OAuth2 renewal and persistence automatic. Note: The code pattern below requires customization for your particular situation.
Scheduled Renewal Job?
With OAuth1's one month renewal window you could rely on a background scheduler like Cron but with the OAuth2's super short renewal duration, relying solely on Cron may not be best because in the case that your app or the QBO API has an outage when either service returns it may take considerably longer to get QBO requests flowing again. I talk more about going with a background scheduler strategy here.
Show Notes:
- Update the
qbo_api
gem to the latest. - Refactor your QuickBooks Online API requests in to the "request_handler" pattern as seen below. The qbo_account, record, and payload ideas are specific to my app that I'm switching over to OAuth2. Please watch the screencast to get the proper context.
module Qbo class Base def self.request_handler(qbo_account_id:, payload: false, record: false) retries ||= 0 qbo_account = QboAccount.find(qbo_account_id) api = Qbo.init(qbo_account) yield(api) rescue QboApi::Unauthorized if qbo_account.access_token.present? Qbo::OAuth2.renew(qbo_account) retry if (retries += 1) < 3 else register_error(qbo_account: qbo_account, error: '401 Authentication Error', payload: payload) end rescue QboApi::Error => e if err = e.fault if record fix_duplicate_customer(qbo_account: qbo_account, record: record, error: err, payload: payload, qbo_api: api) else final_err = error_body(err) register_error(qbo_account: qbo_account, error: final_err, payload: payload) end end rescue => e final_err = e.to_s[0..255] qbo_account.account.qbo_errors.create(body: final_err, request_json: payload.to_json, account: qbo_account.account) raise e end def self.entity_handler(qbo_account_id:, entity:, record:, payload:) request_handler(qbo_account_id: qbo_account_id, record: record, payload: payload) do |api| if id = record.qbo_id res = api.update(entity, id: id, payload: payload) else res = api.create(entity, payload: payload) record.update(qbo_id: res['Id']) end res end end def self.fix_duplicate_customer(qbo_account:, record:, error:, payload:, qbo_api: api) if record.class.name == "Customer" && error[:error_body][0][:error_message] == 'Duplicate Name Exists Error' if res = qbo_api.get(:customer, ["DisplayName", record.display_name]) record.update(qbo_id: res['Id']) else register_error(qbo_account: qbo_account, error: error, payload: payload, resource: record) end else register_error(qbo_account: qbo_account, error: error, payload: payload, resource: record) end end def self.register_error(qbo_account:, error:, payload:, resource: false) qbo_account.account.qbo_errors.create(body: error[:error_body], method: error[:method], resource: resource, url: error[:url], status: error[:status], request_json: payload.to_json) end def self.error_body(err) if body = err.try(:[], :error_body) result = body.map{ |b| b.try(:[], :error_detail) || b.try(:[], :error_message) }.join(" -- ") else "" end end end end
module Qbo def self.init(qbo_account) if qbo_account.access_token.present? attrs = { access_token: qbo_account.access_token, realm_id: qbo_account.companyid } else attrs = { token: qbo_account.token, token_secret: qbo_account.secret, realm_id: qbo_account.companyid, consumer_key: Rails.application.secrets.qbo_app_consumer_key, consumer_secret: Rails.application.secrets.qbo_app_consumer_secret } end QboApi.new(attrs) end end
- Pushed on 10/24/2019 by Christian
- QuickBooks Integration Consulting