Working with OAuth Flow can be a double-edged sword. While it's crucial for securely accessing APIs, managing tokens, authorization state, and timeouts can be challenging. Get it wrong, and you may find yourself locked out of a resource or stuck with an expired token during an important operation.
QuickBooks Online (QBO) offers a comprehensive guide and an even better playground for developers, especially those just starting with OAuth. But, here comes the tricky part: you have to manage independent sets of authorization and refresh tokens for each authorized company (known as a realm) within your application.
This article breaks down how to use Perigee to effortlessly manage these tokens across multiple realms.
The Core of the Process
The following C# code snippet is executed immediately after receiving the RealmID and Code parameters from the API callback:
//Reach out to: https://developer.api.intuit.com/.well-known/openid_configuration// Get the token endpointThreadRegistry reg =ThreadRegistry.Instance;string TokenEndpoint ="";//Create a client pointing to the QBO Token Endpointusingvar authClient =newRestClient(newRestClientOptions(TokenEndpoint){ Authenticator =newRestSharp.Authenticators.HttpBasicAuthenticator(reg.GetValue<string>("AppSettings:client_id")!,reg.GetValue<string>("AppSettings:client_secret")!)});var authReq =newRestRequest("",Method.Post);//Add parametersauthReq.AddParameter("redirect_uri",reg.GetValue<string>("AppSettings:redirect"));authReq.AddParameter("grant_type","authorization_code");authReq.AddParameter("code", code);//Execute - Getting back an initial authorization code and refresh code. var rsp =authClient.ExecuteRetry<QBAuth.Tokens>(authReq,2);if (rsp.IsSuccessful){ //Each credential is assigned a name: QBA- + realmIDstring qbaName =$"QBA-{realmId}"; //Step 1) Persist an initial credential we received from the token callbackCredentialStore.PersistCredential(newRestSharpCredentialStoreItem(newRestSharp.Authenticators.JwtAuthenticator(rsp.Data?.AccessToken??""),DateTimeOffset.UtcNow.AddSeconds(rsp.Data?.ExpiresIn??3000)) { RefreshToken =rsp.Data?.RefreshToken, StoreA =rsp.Data?.IdToken, Environment = realmId, Name = qbaName }); //Step 2) Register realm refresh for future usage on expirationCredentialStore.RegisterRefresh(qbaName, (o) => {usingvar authClient =newRestClient(newRestClientOptions(TokenEndpoint) { Authenticator =newRestSharp.Authenticators.HttpBasicAuthenticator(reg.GetValue<string>("AppSettings:client_id")!,reg.GetValue<string>("AppSettings:client_secret")!) });var authReq =newRestRequest("",Method.Post);authReq.AddParameter("redirect_uri",reg.GetValue<string>("AppSettings:redirect")!);authReq.AddParameter("grant_type","refresh_token");authReq.AddParameter("refresh_token",CredentialStore.GetRefreshToken(qbaName));var rsp =authClient.ExecuteRetry<QBAuth.Tokens>(authReq, retries:1);if (rsp.IsSuccessful) { //Get previous expired credential using peek to pull environment from last run...var expCred =CredentialStore.PeekCredential(qbaName);returnnewRestSharpCredentialStoreItem(newRestSharp.Authenticators.JwtAuthenticator(rsp.Data?.AccessToken??""),DateTimeOffset.UtcNow.AddSeconds(rsp.Data?.ExpiresIn??3000)) { RefreshToken =rsp.Data?.RefreshToken, StoreA =rsp.Data?.IdToken, Environment =expCred?.Environment??"", }; }else {returnnewFaultedCredentialStoreItem($"Couldn't refresh token for realm {qbaName}, {rsp.Content}"); } });}
Re-Register the Callback on Application Startup 🔄
Since each realm has its own set of authorization details, it's important to re-register the callback functions for each realm when the application restarts:
foreach (var cred inCredentialStore.GetCredentialsByPredicate(f =>f.Name.StartsWith("QBA-"))){var QBAName =cred.Name; //TODO: Call the register function again listed above. It would be wise to encapsulate that mathod to use in multiple places!}
Making Authorized Realm Calls Is Simpler Than Ever
Thanks to our prior setup with Perigee, making authorized realm calls is now a walk in the park. The system ensures that you get a freshly refreshed authorization code each time you hit an endpoint. Plus, it authorizes the call pre-emptively, reducing the risk of a failure midway through a process.
var realmID =123456789;//New client (this is using sandbox, you *should* be querying the discovery endpoint for this)usingvar client =newRestClient(newRestClientOptions("https://sandbox-quickbooks.api.intuit.com") { //Use the credential authenticator here, with the QBA-Realm key! Voila!!! Authenticator =newCredentialAuthenticator($"QBA-{realmID}")});var rsp =client.ExecuteRetry<CompanyInfo.QueryResponse>($"/v3/company/{realmID}/query?query=select * from CompanyInfo", retries:3);
In conclusion, while OAuth can often feel like a labyrinth of tokens and authorizations, Perigee simplifies the process, making it far easier and more reliable.