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:
- getOAuthTokenFromKeyVault()- Retrieves an access token securely.
 
- 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.