Tuesday, 13 May 2025

Secure API Integration in D365FO Using OAuth2 and Azure Key Vault (X++)

 

Secure API Integration in D365FO Using OAuth2 and Azure Key Vault (X++)

Integrating with external APIs securely is a common requirement in Microsoft Dynamics 365 Finance and Operations (D365FO) projects. In this post, we will walk through a complete, secure, and reusable approach for integrating a third-party API using OAuth2 and Azure Key Vault to retrieve secrets like the client secret.

Use Case

You want to:

  • Authenticate using OAuth2 client credentials flow.

  • Securely retrieve the client secret from Azure Key Vault.

  • Send a JSON-based POST request to an external API.

  • Build this in a reusable way using standard .NET classes in X++.

Solution Overview

We'll create a reusable class NarviAPIClient with two core methods:

  1. getOAuthTokenFromKeyVault() - Retrieves an access token securely.

  2. sendPostRequest() - Sends a POST request with Authorization header and JSON payload.

Step-by-Step Implementation

Step 1: Create NarviAPIClient Class in AOT

class NarviAPIClient
{
    public static str getOAuthTokenFromKeyVault(str clientId, str tenantId, str resource, str secretName)
    {
        str tokenEndpoint, clientSecret, formData, responseText, accessToken;
        System.Net.HttpWebRequest tokenRequest;
        System.Net.HttpWebResponse tokenResponse;
        System.IO.Stream responseStream;
        System.IO.StreamReader streamReader;
        System.Text.UTF8Encoding encoding;
        System.Byte[] bytes;
        Map tokenMap;
        KeyVaultParameters keyVaultParameters = KeyVaultParameters::construct();

        clientSecret = KeyVaultCertificateHelper::getSecretValueFromKeyVault(secretName, keyVaultParameters);

        tokenEndpoint = strFmt("https://login.microsoftonline.com/%1/oauth2/token", tenantId);
        formData = strFmt("grant_type=client_credentials&client_id=%1&client_secret=%2&resource=%3",
                          clientId, clientSecret, resource);

        try
        {
            tokenRequest = System.Net.WebRequest::Create(tokenEndpoint) as System.Net.HttpWebRequest;
            tokenRequest.set_Method("POST");
            tokenRequest.set_ContentType("application/x-www-form-urlencoded");

            encoding = new System.Text.UTF8Encoding();
            bytes = encoding.GetBytes(formData);
            tokenRequest.get_RequestStream().Write(bytes, 0, bytes.Length);

            tokenResponse = tokenRequest.GetResponse();
            responseStream = tokenResponse.GetResponseStream();
            streamReader = new System.IO.StreamReader(responseStream);
            responseText = streamReader.ReadToEnd();

            tokenMap = RetailCommonWebAPI::parseJSON(responseText);
            accessToken = tokenMap.lookup("access_token");

            return accessToken;
        }
        catch (Exception::CLRError)
        {
            error(strFmt("OAuth token error: %1", AifUtil::getClrErrorMessage()));
            return "";
        }
    }

    public static str sendPostRequest(str url, str jsonBody, str accessToken)
    {
        System.Net.HttpWebRequest httpRequest;
        System.Net.HttpWebResponse httpResponse;
        System.IO.Stream requestStream, responseStream;
        System.IO.StreamReader reader;
        System.Text.UTF8Encoding encoding;
        System.Byte[] bytes;
        str responseText;

        try
        {
            httpRequest = System.Net.WebRequest::Create(url) as System.Net.HttpWebRequest;
            httpRequest.set_Method("POST");
            httpRequest.set_ContentType("application/json");
            httpRequest.Headers.Add("Authorization", strFmt("Bearer %1", accessToken));

            encoding = new System.Text.UTF8Encoding();
            bytes = encoding.GetBytes(jsonBody);
            requestStream = httpRequest.GetRequestStream();
            requestStream.Write(bytes, 0, bytes.Length);

            httpResponse = httpRequest.GetResponse();
            responseStream = httpResponse.GetResponseStream();
            reader = new System.IO.StreamReader(responseStream);
            responseText = reader.ReadToEnd();

            return responseText;
        }
        catch (Exception::CLRError)
        {
            return strFmt("POST error: %1", AifUtil::getClrErrorMessage());
        }
    }
}

Step 2: Use the Class from a Job or Service

static void CallNarviAPIClient(Args _args)
{
    str tenantId     = "<your-tenant-id>";
    str clientId     = "<your-client-id>";
    str resource     = "<your-api-resource-uri>";
    str secretName   = "your-client-secret-keyvault-name";
    str apiUrl       = "https://your-api-endpoint.com/api/post";
    str jsonPayload, accessToken, response;

    System.IO.StringWriter writer = new System.IO.StringWriter();
    Newtonsoft.Json.JsonTextWriter jsonWriter = new Newtonsoft.Json.JsonTextWriter(writer);

    jsonWriter.WriteStartObject();
    jsonWriter.WritePropertyName("ProjId");
    jsonWriter.WriteValue("000123");
    jsonWriter.WritePropertyName("TransDate");
    jsonWriter.WriteValue("2025-05-13");
    jsonWriter.WriteEndObject();

    jsonPayload = writer.ToString();

    accessToken = NarviAPIClient::getOAuthTokenFromKeyVault(clientId, tenantId, resource, secretName);

    if (!accessToken)
    {
        error("Token not retrieved.");
        return;
    }

    response = NarviAPIClient::sendPostRequest(apiUrl, jsonPayload, accessToken);
    info(strFmt("API Response: %1", response));
}

Benefits of This Approach

  • Secure: Client secrets aren't hardcoded.

  • Reusable: Centralized logic for token generation and POST request.

  • Modular: Easily extendable for GET requests, token caching, retries, etc.

  • Production Ready: Compatible with batch and service classes.

Prerequisites

  • Azure Key Vault configured in LCS.

  • The app must have permissions to read secrets.

  • Secrets must be stored in Key Vault.

Secure API Integration in D365FO Using OAuth2 and Azure Key Vault (X++)

  Secure API Integration in D365FO Using OAuth2 and Azure Key Vault (X++) Integrating with external APIs securely is a common requirement in...