In this blog, I discuss retrieving an OAuth2.0 access token from a Business Central AL extension.
Let’s Get Started
Create an App Registration in Azure Active Directory
Get started by creating your App Registration in the Azure Active Directory. Be sure to set up the Azure AD application in Business Central afterward with the necessary permissions sets.
You can find documentation from Microsoft here.
Create an AL extension
You’ll be adding code to an AL extension. In case you haven’t built one before, I wanted to supply a link to the documentation.
Now Let’s Code!
Okay, I’m writing this on 11/16/22. Why does that matter? Well, I’ll assume you’ve already been browsing for a solution, and you’ve already tried out a few things written on other blogs, spent hours and hours, AND YOU HAVEN’T GOTTEN IT TO WORK!
That’s what I went through anyway. And we know the Business Central framework changes, and what worked two years ago might not work anymore.
You’ll find the complete procedure that’s working for me below, but first, I’ll point out the key details that were missing from the examples I tried previously.
Don’t use a JSON node in your content
I found a few examples sending JSON in the Content of the HttpRequestMessage. That didn’t work, but this did. I used a Label variable and defined it like this:
ContentLbl: Label 'grant_type=%1&scope=%2&client_id=%3&client_secret=%4', Comment = '%1 is grant_type, %2 is scope, %3 is the client id, %4 is the client secret';
URL Encoding
Your Content-Type needs to be application/x-www-form-urlencoded.
HttpHeaders.Add('Content-Type', 'application/x-www-form-urlencoded');
Also, you will need to URL Encode the variables you’re assigning to the ContentLbl string. Use the URLEncode procedure in Type Helper codeunit to encode the variables in your string.
Remove the opening and closing brackets from your guids
You might be using a setup table to send variables to your function that stores the Tenant ID and Client ID as guids. If so, remove the brackets when creating your URLs and content, so they don’t end up in your strings.
TenantId := DelChr(INVCWebhookSubscription."Tenant Id", '=', '{}');
Remove the quotes from the Access Token
After connecting successfully and retrieving a non-error response, you can grab your token. I stuff the response into a JsonObject. If you do that, you can retrieve the “access_token” node. However, the value comes back with surrounding quotes. Remove these quotes.
AccessToken := DelChr(AccessToken, '<>', '"');
Full Procedure
Here’s a sample procedure in full (The INVCWebhookSubscription is a custom table I’m using to store my credential variables related to a Business Central Webhook Subscription):
local procedure GetAccessToken(var INVCWebhookSubscription: Record "INVC Webhook Subscription") AccessToken: Text
var
TypeHelper: Codeunit "Type Helper";
HttpResponseMessage: HttpResponseMessage;
HttpRequestMessage: HttpRequestMessage;
HttpClient: HttpClient;
HttpContent: HttpContent;
HttpHeaders: HttpHeaders;
JsonObject: JsonObject;
JsonToken: JsonToken;
JsonValue: JsonValue;
ContentText: Text;
ResponseText: Text;
GrantType: Text;
Scope: Text;
TenantId: Text;
ClientId: Text;
ClientSecret: Text;
ContentJsonLbl: Label 'grant_type=%1&scope=%2&client_id=%3&client_secret=%4', Comment =
'%1 is grant_type, %2 is scope, %3 is the client id, %4 is the client secret';
URL: Text;
URLLbl: Label 'https://login.microsoftonline.com/%1/oauth2/v2.0/token', Comment = '%1 is the tenant id';
HttpErrorCodeErr: Label '%1. Error Status Code: %2', Comment = '%1 is error. %2 is status code';
AccessTokenErr: Label 'Cannot get Access Token.';
begin
//set url
TenantId := DelChr(INVCWebhookSubscription."Tenant Id", '=', '{}');
URL := StrSubstNo(URLLbl, TenantId);
//set content body
GrantType := 'client_credentials';
Scope := 'https://api.businesscentral.dynamics.com/.default';
ClientId := DelChr(INVCWebhookSubscription."Client Id", '=', '{}');
ClientSecret := INVCWebhookSubscription.” Client Secret”;
ContentText := StrSubstNo(ContentJsonLbl, TypeHelper.UrlEncode(GrantType), TypeHelper.UrlEncode(Scope), TypeHelper.UrlEncode(ClientId), TypeHelper.UrlEncode(ClientSecret));
HttpContent.WriteFrom(ContentText);
// Retrieve the contentHeaders associated with the content
HttpContent.GetHeaders(HttpHeaders);
HttpHeaders.Clear();
HttpHeaders.Add('Content-Type', 'application/x-www-form-urlencoded');
// Assign content to request.Content
HttpRequestMessage.Content := HttpContent;
HttpRequestMessage.SetRequestUri(URL);
HttpRequestMessage.Method := 'POST';
HttpClient.Send(HttpRequestMessage, HttpResponseMessage);
if not HttpResponseMessage.IsSuccessStatusCode() then
error(HttpErrorCodeErr, AccessTokenErr, HttpResponseMessage.HttpStatusCode);
// Read the response content as json.
HttpResponseMessage.Content().ReadAs(ResponseText);
if not JsonObject.ReadFrom(ResponseText) then
error(AccessTokenErr);
if not JsonObject.Contains('access_token') then
error(AccessTokenErr);
JsonObject.Get('access_token', JsonToken);
JsonValue := JsonToken.AsValue();
JsonToken.WriteTo(AccessToken);
AccessToken := DelChr(AccessToken, '<>', '"');
INVCWebhookSubscription.SetBlobText(AccessToken, INVCWebhookSubscription.FieldNo("Access Token"));
INVCWebhookSubscription.Modify();
end;
Calling the Procedure
In case you’re not familiar with using the token, I’m providing another sample procedure that calls the procedure above and uses the token. (As a bonus, you can glean now to subscribe to a Business Central Webhook in this procedure.):
procedure PostPatchSubscription(var INVCWebhookSubscription: Record "INVC Webhook Subscription");
var
HttpClient: HttpClient;
HttpRequestMessage: HttpRequestMessage;
HttpResponseMessage: HttpResponseMessage;
HttpHeaders: HttpHeaders;
HttpRequestHeaders: HttpHeaders;
HttpContent: HttpContent;
JsonObject: JsonObject;
JsonToken: JsonToken;
JsonValue: JsonValue;
SubscriptionId: Text;
RequestText: Text;
RequestUrl: Text;
ResponseText: Text;
RequestUrlLbl: Label '%1/%2/%3', Comment = '%1 is the base url, %2 is the environment name, %3 is the Subscription url.';
SubscriptionLbl: Label '%1(''%2'')', Comment = '%1 is url, %2 is subscriptionId';
ResponseMsg: Label 'Url: %1; Method: %2; Status Code: %3; Reason Phrase: %4; Response: %5', Comment = '%1 url, %2 Method, %3 - Status, %4 = Phrase, %5 is response text.';
begin
INVCWebhookSubscription.TestField("Notification Url");
INVCWebhookSubscription.TestField("Resource Url");
RequestText := INVCWebhookSubscription.GetSubscriptionRequestJsonText();
INVCWebhookSubscription.SetBlobText(RequestText, INVCWebhookSubscription.FieldNo("Subscription Request"));
INVCWebhookSubscription.SetBlobText('', INVCWebhookSubscription.FieldNo("Subscription Response"));
INVCWebhookSubscription.Modify();
Commit();
INVCWebhookSubscription.Get(INVCWebhookSubscription.Code);
// Add the payload to the content
HttpContent.WriteFrom(RequestText);
// Retrieve the contentHeaders associated with the content
HttpContent.GetHeaders(HttpHeaders);
HttpHeaders.Clear();
HttpHeaders.Add(‘Content-Type’, ‘application/json’);
// Assigning content to request.Content will actually create a copy of the content and assign it.
// After this line, modifying the content variable or its associated headers will not reflect in
// the content associated with the request message
HttpRequestMessage.Content := HttpContent;
RequestUrl := StrSubstNo(RequestUrlLbl, INVCWebhookSubscription.GetBlobText(INVCWebhookSubscription.FieldNo(“Base Url”)), INVCWebhookSubscription.” Environment Name”, INVCWebhookSubscription.GetBlobText(INVCWebhookSubscription.FieldNo("Subscription Url")));
if INVCWebhookSubscription.” Subscription Id" = '' then
HttpRequestMessage.Method := 'POST'
else begin
HttpRequestMessage.GetHeaders(HttpRequestHeaders);
HttpRequestHeaders.Add('If-Match', '*');
HttpRequestMessage.Method := 'PATCH';
RequestUrl := StrSubstNo(SubscriptionLbl, RequestUrl, INVCWebhookSubscription."Subscription Id");
end;
HttpRequestMessage.SetRequestUri(RequestUrl);
HttpClient.DefaultRequestHeaders.Add('Authorization', 'Bearer ' + GetAccessToken(INVCWebhookSubscription));
HttpClient.Send(HttpRequestMessage, HttpResponseMessage);
if (HttpResponseMessage.HttpStatusCode = 404) and (HttpRequestMessage.Method = 'PATCH') then begin
INVCWebhookSubscription.” Subscription Id" := '';
INVCWebhookSubscription.Modify();
Commit();
INVCWebhookSubscription.Get(INVCWebhookSubscription.Code);
INVCWebhookSubscription.SetRecFilter();
PostPatchSubscription(INVCWebhookSubscription);
exit;
end;
// Read the response content as json.
HttpResponseMessage.Content().ReadAs(ResponseText);
if JsonObject.ReadFrom(ResponseText) then begin
if JsonObject.Contains(‘expirationDateTime’) then begin
JsonObject.Get('expirationDateTime', JsonToken);
JsonValue := JsonToken.AsValue();
INVCWebhookSubscription.” Expiration Date Time”:= JsonValue.AsDateTime();
end;
if INVCWebhookSubscription.” Subscription Id" = '' then
if JsonObject.Contains(‘subscriptionId’) then begin
JsonObject.Get('subscriptionId', JsonToken);
JsonToken.WriteTo(SubscriptionId);
INVCWebhookSubscription.” Subscription Id" := CopyStr(DelChr(SubscriptionId, '=', '"'), 1, MaxStrLen(INVCWebhookSubscription."Subscription Id"));
end;
end;
INVCWebhookSubscription.SetBlobText(StrSubstNo(ResponseMsg, HttpRequestMessage.GetRequestUri(), HttpRequestMessage.Method, HttpResponseMessage.HttpStatusCode, HttpResponseMessage.ReasonPhrase, ResponseText), INVCWebhookSubscription.FieldNo("Subscription Response"));
INVCWebhookSubscription.Modify();
end;
And there you have it. That is how you can get an OAuth2.0 access token in Business Central. I hope I was able to help cut through some of the confusion out there for you and get you to where you need to be. If you have any questions, contact me by clicking the button below. Thank you, and happy coding.