Service to service authentication in Business Central 18.3 – How to use in AL

4 Aug

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!

One thought on “Service to service authentication in Business Central 18.3 – How to use in AL

  1. Pingback: Service to service authentication in Business Central 18.3 – How to use in AL – Kauffmann @ Dynamics 365 Business Central – Thinking Enterprise Solutions https://www.vizorsol.com

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.