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!

11 thoughts 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

  2. Hi , I need to pull data from one Business central company to another using webservices. so i think I need to do something like above code right?

  3. Hi Ajk,

    Did you try to use this to access custom APIs? For me it works fine for standard API pages, but if I try to use it for a custom API page I get this error:

    “You do not have access to this object using an application as authentication.”

    Already set permissions for that page and table (actually is just a Custom API page based on Customer table. I can access standard BC Customers Api, but not my custom API page for Customers table which includes 50xxx range fields)..

    • I tested an online sandbox with a custom API and client credentials flow. It works without any problem.
      You may need to set the correct permissions. I’ve tested with permission set D365 BUS FULL ACCESS

  4. Hi AJK, I have been experimenting with this. There is a small typo in your code on line 29: ‘AccesTokenExpires > CurrentDateTime’ should be ‘<' I think.

  5. I would love to see a working example of a BC22 accessing a web service, running on another BC22 onprem. I can access an API on BC22 onprem by Postman. And if I copy the token-value from Postman to my test-app in BC, I can access the web service. But using the codeunit oauth2, gives me a different token and my API-reguests gets a “401 The server has rejected the client credentials.”.

    So now I wonder if this is even possible.

Leave a Reply

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