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.