Because I got the question if it was possible to use the client credentials flow in Business Central I decided to write a quick blog post about that as well. Codeunit OAuth2 provides a number of functions to acquire access tokens with different authorization flows, including the client credentials flow. The function AcquireTokenWithClientCredentials can be used for this purpose. Read more about the OAuth2 module here: https://github.com/microsoft/ALAppExtensions/tree/master/Modules/System/OAuth2.
Let’s just dive into the code. If you have read the PowerShell examples or C# example code, then the code below should be familiar.
codeunit 50100 BCConnector { var ClientIdTxt: Label '3870c15c-5700-4704-8b1b-e020052cc860'; ClientSecretTxt: Label '~FJRgS5q0YsAEefkW-_pA4ENJ_vIh-5RV9'; AadTenantIdTxt: Label 'kauffmann.nl'; AuthorityTxt: Label 'https://login.microsoftonline.com/{AadTenantId}/oauth2/v2.0/token'; BCEnvironmentNameTxt: Label 'sandbox'; BCCompanyIdTxt: Label '64d41503-fcd7-eb11-bb70-000d3a299fca'; BCBaseUrlTxt: Label 'https://api.businesscentral.dynamics.com/v2.0/{BCEnvironmentName}/api/v2.0/companies({BCCompanyId})'; AccessToken: Text; AccesTokenExpires: DateTime; trigger OnRun() var Customers: Text; Items: Text; begin Customers := CallBusinessCentralAPI(BCEnvironmentNameTxt, BCCompanyIdTxt, 'customers'); Items := CallBusinessCentralAPI(BCEnvironmentNameTxt, BCCompanyIdTxt, 'items'); Message(Customers); Message(Items); end; procedure CallBusinessCentralAPI(BCEnvironmentName: Text; BCCompanyId: Text; Resource: Text) Result: Text var Client: HttpClient; Response: HttpResponseMessage; Url: Text; begin if (AccessToken = '') or (AccesTokenExpires = 0DT) or (AccesTokenExpires > CurrentDateTime) then GetAccessToken(AadTenantIdTxt); Client.DefaultRequestHeaders.Add('Authorization', GetAuthenticationHeaderValue(AccessToken)); Client.DefaultRequestHeaders.Add('Accept', 'application/json'); Url := GetBCAPIUrl(BCEnvironmentName, BCCompanyId, Resource); if not Client.Get(Url, Response) then if Response.IsBlockedByEnvironment then Error('Request was blocked by environment') else Error('Request to Business Central failed\%', GetLastErrorText()); if not Response.IsSuccessStatusCode then Error('Request to Business Central failed\%1 %2', Response.HttpStatusCode, Response.ReasonPhrase); Response.Content.ReadAs(Result); end; local procedure GetAccessToken(AadTenantId: Text) var OAuth2: Codeunit OAuth2; Scopes: List of [Text]; begin Scopes.Add('https://api.businesscentral.dynamics.com/.default'); if not OAuth2.AcquireTokenWithClientCredentials(ClientIdTxt, ClientSecretTxt, GetAuthorityUrl(AadTenantId), '', Scopes, AccessToken) then Error('Failed to retrieve access token\', GetLastErrorText()); AccesTokenExpires := CurrentDateTime + (3599 * 1000); end; local procedure GetAuthenticationHeaderValue(AccessToken: Text) Value: Text; begin Value := StrSubstNo('Bearer %1', AccessToken); end; local procedure GetAuthorityUrl(AadTenantId: Text) Url: Text begin Url := AuthorityTxt; Url := Url.Replace('{AadTenantId}', AadTenantId); end; local procedure GetBCAPIUrl(BCEnvironmentName: Text; BCCOmpanyId: Text; Resource: Text) Url: Text; begin Url := BCBaseUrlTxt; Url := Url.Replace('{BCEnvironmentName}', BCEnvironmentName) .Replace('{BCCompanyId}', BCCOmpanyId); Url := StrSubstNo('%1/%2', Url, Resource); end; }
Remarks
I’ve tried to keep the code close to the C# example. There is definitely room to improve and JSON handling should be added here as well. Secrets should not be stored in code like this. For the Business Central SaaS environment, I would definitely go with Azure Key Vault storage.
The only thing I was really missing is handling the lifetime of the access token. The OAuth2 codeunit just returns an access token without any information about the expiration. In the code above I’ve added that myself by assuming the default lifetime of 60 minutes (access tokens are usually returned with expires in = 3599 seconds).
Another small thing I noticed is the RedirectURL parameter for the function AcquireTokenWithClientCredentials. That doesn’t make sense, there is no redirect URL used during the client credentials flow. So I passed in an empty string, and luckily that worked. The parameter could be completely removed in my opinion.
That’s it! With this blog post I finish the series about using the client credentials flow with Business Central APIs. But I’m not done with OAuth, not by far! I’d like to write something about setting up OAuth for on-prem installations as well. And I’m open for suggestions, just shoot me a message!