Dynamics 365 Authentication: Azure AD/Entra ID OAuth 2.0, S2S, and App Registration

Type: ERP Integration System: Microsoft Dynamics 365 (Dataverse) (Web API v9.2) Confidence: 0.92 Sources: 8 Verified: 2026-03-01 Freshness: 2026-03-01

TL;DR

System Profile

This card covers authentication for Microsoft Dynamics 365 Customer Engagement apps (Sales, Service, Marketing, Customer Insights) that use Microsoft Dataverse as their data platform. All Dataverse-based D365 apps share the same authentication infrastructure through Microsoft Entra ID. This card does NOT cover Dynamics 365 Business Central (which has its own S2S flow) or Dynamics 365 Finance & Operations (which uses a different token endpoint pattern). [src1, src2]

PropertyValue
VendorMicrosoft
SystemDynamics 365 (Dataverse) Web API v9.2
API SurfaceOData v4 (REST)
Current API Versionv9.2 (2026)
Editions CoveredSales, Service, Marketing, Customer Insights, Power Apps (all Dataverse-based)
DeploymentCloud (Dynamics 365 Online)
API DocsMicrosoft Dataverse Web API
StatusGA

API Surfaces & Capabilities

API SurfaceProtocolBest ForAuth Flows SupportedReal-time?Notes
Web API (OData v4)HTTPS/JSONREST integrations, any languageClient credentials, auth code, ROPC, managed identityYesPrimary API for all Dataverse operations
Organization Service (SDK).NET SDK.NET applications, plug-insClient credentials, auth code, certificateYesRequires Microsoft.PowerPlatform.Dataverse.Client NuGet
Dataverse Search APIHTTPS/JSONFull-text search across entitiesSame as Web APIYesSeparate endpoint: {org}/api/search/v1.0/query

Rate Limits & Quotas

Per-Request Limits

Limit TypeValueApplies ToNotes
Max records per query5,000Web API OData queryUse $top and @odata.nextLink for pagination
Max request body size128 MBWeb APILarger payloads should use file/image column upload
Max batch subrequests1,000$batch endpointEach subrequest counts as a separate API request
Max concurrent connectionsPer-tenant throttlingAll API callsReturns 429 when exceeded

Rolling / Daily Limits

Limit TypeValueWindowEdition Differences
Power Platform API requestsVaries by license24h rollingEnterprise: 20K/user/day; per-app: 1K/user/day; application user: 250K/tenant/day (pooled)
Concurrent batch requestsThrottled per-orgPer-orgReturns 429 Too Many Requests
ExecuteMultiple max1,000 requests per callPer-requestOrganization Service SDK only

Authentication

All Dynamics 365 Dataverse authentication flows go through Microsoft Entra ID (formerly Azure AD) using OAuth 2.0. The token endpoint is https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token. [src1, src2]

FlowUse WhenToken LifetimeRefresh?Notes
Client Credentials (S2S)Server-to-server, no user context, unattended integrationsAccess: ~60-75 minNo refresh token; request new before expiryRecommended for integrations. Requires application user in Dataverse.
Authorization CodeUser-context operations, interactive appsAccess: ~60-75 min; Refresh: up to 90 daysYesRequires redirect URI. User or admin must consent.
Authorization Code + PKCESPAs and mobile appsAccess: ~60-75 min; Refresh: 24h (SPA)Yes (limited for SPA)Required for public clients.
ROPC (username/password)Legacy testing onlyAccess: ~60-75 minNoDo NOT use in production. No MFA support.
Managed IdentityAzure-hosted Dataverse plug-insManaged by AzureAutomaticGA for Dataverse plug-ins only.
Certificate-basedProduction S2S where secrets are insufficientAccess: ~60-75 minNo refresh; new assertion per requestMore secure than client secrets. Store certs in Azure Key Vault.

Authentication Gotchas

Constraints

Integration Pattern Decision Tree

START - Authenticate with Dynamics 365 Dataverse
|-- What type of application?
|   |-- Server/daemon (no user present)
|   |   |-- Running on Azure?
|   |   |   |-- YES, Dataverse plug-in accessing Azure resources --> Managed Identity
|   |   |   |-- YES, general API access --> Client Credentials + Certificate (Key Vault)
|   |   |   |-- NO (on-premise server) --> Client Credentials + Certificate or Secret
|   |   |-- Credential type?
|   |       |-- Production --> Certificate (more secure, longer lifetime)
|   |       |-- Dev/test --> Client Secret (simpler setup)
|   |-- Interactive web application --> Authorization Code flow with PKCE
|   |-- SPA (single-page app) --> Authorization Code + PKCE (public client)
|   |-- Mobile / desktop app --> Authorization Code + PKCE with native redirect
|   |-- Legacy (cannot be updated) --> ROPC (ONLY if MFA is disabled)
|-- Multi-tenant or single-tenant?
|   |-- Single-tenant --> Register in your tenant only
|   |-- Multi-tenant --> Each tenant admin must consent; use /common authority
|-- Token refresh strategy?
    |-- S2S --> No refresh token; request new proactively before expiry
    |-- Delegated --> Use MSAL cache; refresh tokens last up to 90 days
    |-- SPA --> Refresh tokens expire in 24h; re-authenticate interactively

Quick Reference

OperationEndpoint / ConfigValueNotes
Token endpoint (v2.0)https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/tokenPOSTUse v2.0 endpoint for MSAL
Scope (confidential client){org_url}/.defaulte.g., https://myorg.crm.dynamics.com/.defaultS2S client credentials
Scope (public client){org_url}/user_impersonatione.g., https://myorg.crm.dynamics.com/user_impersonationDelegated permissions
API base URLhttps://{org}.crm.dynamics.com/api/data/v9.2/GET/POST/PATCH/DELETERegional suffixes: crm (NA), crm4 (EMEA), crm5 (APAC)
WhoAmI testGET /api/data/v9.2/WhoAmIReturns UserId, BusinessUnitId, OrganizationIdBest endpoint to verify auth
Application user creationPower Platform Admin Center > Environments > S2SBind app registration to Dataverse userAssign security roles after creation
MSAL NuGet packageMicrosoft.Identity.ClientLatest stableReplaces deprecated ADAL
MSAL Python packagemsalLatest stablepip install msal

Step-by-Step Integration Guide

1. Register application in Microsoft Entra ID

Navigate to Azure Portal > Microsoft Entra ID > App registrations > New registration. Provide a name, select the appropriate account type, and register. Copy the Application (client) ID and Directory (tenant) ID. [src2]

# Record these values from the Overview page:
# - Application (client) ID: {your-client-id}
# - Directory (tenant) ID: {your-tenant-id}
# For multi-tenant: set Supported account types to "Accounts in any organizational directory"

Verify: App appears in the App registrations list in Entra ID.

2. Configure credentials (secret or certificate)

For client secret: Certificates & secrets > Client secrets > New client secret. Copy the Value immediately. For certificates: Upload .cer, .pem, or .crt under the Certificates tab. [src2, src5]

# Self-signed certificate (dev/test only):
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes \
  -subj "/CN=D365Integration"

# Production: use Azure Key Vault
# az keyvault certificate create --vault-name MyVault --name D365Cert \
#   --policy "$(az keyvault certificate get-default-policy)"

Verify: Secret or certificate thumbprint appears under Certificates & secrets.

3. Create application user in Dataverse

Go to Power Platform Admin Center > Environments > select environment > S2S > New app user. Select your app registration, choose business unit, assign security roles. [src2, src7]

Verify: In D365, Settings > Security > Users > "Application Users" view shows your app user.

4. Acquire access token and call API

Use MSAL to acquire a token, include as Bearer token in Authorization header. [src1]

import msal, requests

tenant_id = "YOUR_TENANT_ID"
client_id = "YOUR_CLIENT_ID"
client_secret = "YOUR_CLIENT_SECRET"
org_url = "https://yourorg.crm.dynamics.com"

app = msal.ConfidentialClientApplication(
    client_id,
    authority=f"https://login.microsoftonline.com/{tenant_id}",
    client_credential=client_secret
)
result = app.acquire_token_for_client(scopes=[f"{org_url}/.default"])

if "access_token" in result:
    headers = {
        "Authorization": f"Bearer {result['access_token']}",
        "OData-MaxVersion": "4.0",
        "OData-Version": "4.0",
        "Accept": "application/json"
    }
    response = requests.get(f"{org_url}/api/data/v9.2/WhoAmI", headers=headers)
    print(response.json())

Verify: WhoAmI returns JSON with UserId, BusinessUnitId, OrganizationId.

Code Examples

Python: Client Credentials (S2S) with Certificate

# Input:  tenant_id, client_id, certificate path, org_url
# Output: Access token acquired via certificate assertion

import msal, requests

tenant_id = "YOUR_TENANT_ID"
client_id = "YOUR_CLIENT_ID"
org_url = "https://yourorg.crm.dynamics.com"

with open("key.pem", "r") as f:
    key_data = f.read()

app = msal.ConfidentialClientApplication(
    client_id,
    authority=f"https://login.microsoftonline.com/{tenant_id}",
    client_credential={"private_key": key_data, "thumbprint": "CERT_THUMBPRINT_HEX"}
)
result = app.acquire_token_for_client(scopes=[f"{org_url}/.default"])

if "access_token" in result:
    headers = {"Authorization": f"Bearer {result['access_token']}",
               "OData-MaxVersion": "4.0", "OData-Version": "4.0", "Accept": "application/json"}
    resp = requests.get(f"{org_url}/api/data/v9.2/accounts?$top=10&$select=name", headers=headers)
    print(resp.json())

JavaScript/Node.js: Client Credentials with @azure/identity

// Input:  AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET env vars
// Output: Authenticated Dataverse Web API call

// npm install @azure/identity node-fetch@2
const { ClientSecretCredential } = require("@azure/identity");
const fetch = require("node-fetch");

const orgUrl = "https://yourorg.crm.dynamics.com";
const credential = new ClientSecretCredential(
  process.env.AZURE_TENANT_ID,
  process.env.AZURE_CLIENT_ID,
  process.env.AZURE_CLIENT_SECRET
);

async function callDataverse() {
  const token = await credential.getToken(`${orgUrl}/.default`);
  const response = await fetch(`${orgUrl}/api/data/v9.2/WhoAmI`, {
    headers: { Authorization: `Bearer ${token.token}`,
               "OData-MaxVersion": "4.0", "OData-Version": "4.0", "Accept": "application/json" }
  });
  console.log(await response.json());
}
callDataverse().catch(console.error);

C#/.NET: ServiceClient with Client Secret

// NuGet: Microsoft.PowerPlatform.Dataverse.Client
using Microsoft.PowerPlatform.Dataverse.Client;
using Microsoft.Crm.Sdk.Messages;

string connectionString = @"AuthType=ClientSecret;
    Url=https://yourorg.crm.dynamics.com;
    ClientId=YOUR_CLIENT_ID;
    Secret=YOUR_CLIENT_SECRET;
    RequireNewInstance=true";

using var service = new ServiceClient(connectionString);
if (service.IsReady) {
    var response = (WhoAmIResponse)service.Execute(new WhoAmIRequest());
    Console.WriteLine($"Connected as UserId: {response.UserId}");
}

cURL: Quick token acquisition and API test

# Acquire access token
TOKEN=$(curl -s -X POST \
  "https://login.microsoftonline.com/TENANT_ID/oauth2/v2.0/token" \
  -d "client_id=CLIENT_ID&client_secret=SECRET&scope=https://yourorg.crm.dynamics.com/.default&grant_type=client_credentials" \
  | jq -r '.access_token')

# Test authentication
curl -s "https://yourorg.crm.dynamics.com/api/data/v9.2/WhoAmI" \
  -H "Authorization: Bearer $TOKEN" -H "Accept: application/json" | jq .

Data Mapping

Authentication Configuration Reference

Config FieldClient CredentialsAuth CodeCertificateManaged Identity
grant_typeclient_credentialsauthorization_codeclient_credentialsN/A (automatic)
scope{org_url}/.default{org_url}/user_impersonation{org_url}/.default{org_url}/.default
client_idRequiredRequiredRequiredNot needed
client_secretRequiredOptional (with PKCE)Not usedNot needed
client_assertion_typeNot usedNot usedurn:ietf:params:oauth:client-assertion-type:jwt-bearerNot used
redirect_uriNot neededRequiredNot neededNot needed

Data Type Gotchas

Error Handling & Failure Points

Common Error Codes

CodeMeaningCauseResolution
AADSTS700016Application not foundWrong client ID or app not in target tenantVerify client ID; for multi-tenant, ensure admin consent was granted
AADSTS7000215Invalid client secretSecret expired or copied incorrectlyRotate secret; copy the Value (not the ID) from Entra ID
AADSTS50076MFA requiredUsing ROPC with MFA-enabled accountSwitch to auth code flow or use S2S with application user
AADSTS65001Consent not grantedMissing user or admin consentGrant admin consent in Entra ID > API permissions
AADSTS700027Certificate validation failedWrong thumbprint, expired cert, or key mismatchVerify SHA-1 thumbprint; ensure private key matches uploaded public key
401 UnauthorizedInvalid/expired tokenToken expired, malformed, or wrong audienceVerify scope matches org URL; acquire new token
403 ForbiddenInsufficient privilegesApplication user lacks security rolesAssign security roles via Power Platform Admin Center
429 Too Many RequestsRate limit exceededToo many API callsImplement exponential backoff; respect Retry-After header

Failure Points in Production

Anti-Patterns

Wrong: Hardcoding credentials in source code

# BAD - credentials visible in version control
client_secret = "myS3cretV@lue!"

Correct: Use environment variables or key vault

# GOOD - credentials from environment or Azure Key Vault
import os
client_secret = os.environ["AZURE_CLIENT_SECRET"]
# Even better: DefaultAzureCredential chains managed identity, env vars, CLI
from azure.identity import DefaultAzureCredential
credential = DefaultAzureCredential()

Wrong: Requesting a new token for every API call

# BAD - wastes time, risks throttling
for record in records:
    result = app.acquire_token_for_client(scopes=scope)
    requests.post(url, headers={"Authorization": f"Bearer {result['access_token']}"}, json=record)

Correct: Use MSAL token cache, acquire once

# GOOD - MSAL caches internally, auto-refreshes near expiry
result = app.acquire_token_for_client(scopes=scope)
headers = {"Authorization": f"Bearer {result['access_token']}"}
for record in records:
    requests.post(url, headers=headers, json=record)

Wrong: Using ROPC for production integrations

# BAD - breaks when MFA enabled, no Conditional Access support
result = app.acquire_token_by_username_password(scopes=scope, username="[email protected]", password="P@ss!")

Correct: Use client credentials with application user

# GOOD - S2S with application user, supports MFA orgs
app = msal.ConfidentialClientApplication(client_id, authority=authority, client_credential=client_secret)
result = app.acquire_token_for_client(scopes=[f"{org_url}/.default"])

Common Pitfalls

Diagnostic Commands

# Acquire token and inspect claims
TOKEN=$(curl -s -X POST \
  "https://login.microsoftonline.com/TENANT_ID/oauth2/v2.0/token" \
  -d "client_id=CLIENT_ID&client_secret=SECRET&scope=https://yourorg.crm.dynamics.com/.default&grant_type=client_credentials" \
  | jq -r '.access_token')

# Decode token payload (base64) to check claims
echo $TOKEN | cut -d. -f2 | base64 -d 2>/dev/null | jq .

# Test authentication - WhoAmI
curl -s "https://yourorg.crm.dynamics.com/api/data/v9.2/WhoAmI" \
  -H "Authorization: Bearer $TOKEN" -H "Accept: application/json" | jq .

# Check application user's security roles
curl -s "https://yourorg.crm.dynamics.com/api/data/v9.2/systemusers?\$filter=applicationid eq 'CLIENT_ID'&\$expand=systemuserroles_association(\$select=name)" \
  -H "Authorization: Bearer $TOKEN" -H "Accept: application/json" | jq .

# Verify Entra ID app registration (requires Azure CLI)
az ad app show --id CLIENT_ID --query "{name:displayName,appId:appId}" -o table

# List secrets and certificates with expiry dates
az ad app credential list --id CLIENT_ID -o table

Version History & Compatibility

ChangeDateImpactMigration Notes
MSAL required (ADAL deprecated)2022-06BreakingReplace ADAL with MSAL. CrmServiceClient -> ServiceClient.
Azure AD renamed to Entra ID2023-07Branding onlyNo code changes. Old URLs redirect.
Managed identity GA for plug-ins2024-11New featurePlug-ins only. Uses federated identity credentials.
ServiceClient replaces CrmServiceClient2022-03RecommendedSame connection string format, MSAL internally.
v2.0 token endpoint recommended2020-06Best practiceUse scope instead of resource parameter.

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
S2S integrations with Dataverse-based D365 appsIntegrating with D365 Business CentralBC's own S2S flow (API.ReadWrite.All permissions)
Unattended daemon/service access to D365 dataNeed user-context audit trail showing the actual humanAuthorization code flow with delegated permissions
Want to avoid consuming a paid D365 licenseIntegrating with D365 Finance & OperationsF&O uses different resource URI and Entra ID config
Production integration requiring certificate securityQuick ad-hoc prototypingClient secret with short expiry for dev/test
Azure-hosted plug-ins accessing Azure resourcesAzure Functions calling Dataverse API directlyClient credentials (managed identity is plug-in only)

Important Caveats

Related Units