---
# === IDENTITY ===
id: business/erp-integration/dynamics-365-authentication/2026
canonical_question: "How does Dynamics 365 authentication work - Azure AD OAuth 2.0, service-to-service, app registration?"
aliases:
  - "How to authenticate with Dynamics 365 Dataverse API using OAuth 2.0"
  - "Dynamics 365 S2S server-to-server authentication setup"
  - "Microsoft Entra ID app registration for Dynamics 365 client credentials"
  - "Dynamics 365 certificate vs client secret authentication"
entity_type: erp_integration
domain: business > erp-integration > dynamics-365-authentication
region: global
jurisdiction: global
temporal_scope: 2024-2026

# === SYSTEM PROFILE ===
systems:
  - name: "Microsoft Dynamics 365 (Dataverse)"
    vendor: "Microsoft"
    version: "Web API v9.2"
    edition: "All editions (Sales, Service, Marketing, F&O, Business Central)"
    deployment: "cloud"
    api_surface: "OData v4 (Web API)"

# === VERIFICATION ===
last_verified: 2026-03-01
confidence: 0.92
version: 1.0
first_published: 2026-03-01

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: volatile
  last_breaking_change: "2022-06 — ADAL deprecated, MSAL required"
  next_review: 2026-08-28
  change_sensitivity: high

# === CONSTRAINTS ===
constraints:
  - "Access tokens expire after ~60-75 minutes (configurable by tenant admin via token lifetime policy)"
  - "Client secrets have a maximum lifetime of 2 years; certificates up to 3 years"
  - "Application users cannot perform interactive logins - S2S only"
  - "Managed identities for Dataverse are limited to plug-in scenarios (GA), not general API access"
  - "Multi-tenant app registrations require admin consent in each tenant"
  - "Application users are not counted toward the 7 non-interactive user limit, but have API request limits (Power Platform Request limits)"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "User needs Dynamics 365 Business Central authentication specifically"
    use_instead: "business/erp-integration/dynamics-365-bc-authentication/2026"
  - condition: "User needs Dynamics 365 Finance & Operations (F&O) authentication"
    use_instead: "business/erp-integration/dynamics-365-fo-authentication/2026"
  - condition: "User needs general Microsoft Graph API authentication"
    use_instead: "N/A - see Microsoft identity platform docs"

# === AGENT HINTS ===
inputs_needed:
  - key: auth_flow
    question: "What authentication flow do you need?"
    type: choice
    options:
      - "server-to-server (S2S, no user context - client credentials)"
      - "user-context (delegated, authorization code flow)"
      - "managed identity (Azure-hosted services)"
  - key: credential_type
    question: "Which credential type do you prefer?"
    type: choice
    options:
      - "client secret (simpler setup, less secure)"
      - "certificate (more secure, recommended for production)"
      - "managed identity (zero-credential, Azure services only)"
  - key: environment
    question: "What is the deployment environment?"
    type: choice
    options:
      - "Azure-hosted (App Service, Functions, AKS)"
      - "On-premise / non-Azure server"
      - "Desktop / mobile application"

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/business/erp-integration/dynamics-365-authentication/2026"
suggested_citation: "Source: knowledgelib.io - AI Knowledge Library (verified 2026-03-01)"

# === RELATED UNITS ===
related_kos:
  depends_on: []
  related_to:
    - id: "business/erp-integration/dynamics-365-rest-api/2026"
      label: "Dynamics 365 Dataverse Web API capabilities and rate limits"
  solves:
    - id: "business/erp-integration/salesforce-dynamics-365-integration/2026"
      label: "Salesforce to Dynamics 365 integration playbook"
  alternative_to:
    - id: "business/erp-integration/dynamics-365-bc-authentication/2026"
      label: "Dynamics 365 Business Central S2S authentication (different flow)"
  often_confused_with:
    - id: "business/erp-integration/microsoft-graph-authentication/2026"
      label: "Microsoft Graph API authentication (different scope and permissions model)"

# === SOURCES ===
sources:
  - id: src1
    title: "Use OAuth authentication with Microsoft Dataverse"
    author: Microsoft
    url: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/authenticate-oauth
    type: official_docs
    published: 2026-01-07
    reliability: authoritative
  - id: src2
    title: "Register an app with Microsoft Entra ID for Dataverse"
    author: Microsoft
    url: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/walkthrough-register-app-azure-active-directory
    type: official_docs
    published: 2026-01-07
    reliability: authoritative
  - id: src3
    title: "Refresh tokens in the Microsoft identity platform"
    author: Microsoft
    url: https://learn.microsoft.com/en-us/entra/identity-platform/refresh-tokens
    type: official_docs
    published: 2025-06-15
    reliability: authoritative
  - id: src4
    title: "Power Platform managed identity overview"
    author: Microsoft
    url: https://learn.microsoft.com/en-us/power-platform/admin/managed-identity-overview
    type: official_docs
    published: 2025-09-18
    reliability: authoritative
  - id: src5
    title: "Certificate-Based Authentication with Dynamics 365"
    author: Dynamics Community
    url: https://community.dynamics.com/blogs/post/?postid=507631dd-7508-48f6-951a-01addd72d6cd
    type: technical_blog
    published: 2024-08-15
    reliability: moderate_high
  - id: src6
    title: "Best Practices for Managing Client Secrets in Dynamics 365"
    author: ServerSys
    url: https://www.serversys.com/insights/best-practices-for-managing-client-secrets-in-dynamics-365/
    type: technical_blog
    published: 2025-03-10
    reliability: moderate_high
  - id: src7
    title: "Step by Step: D365 S2S Authentication Using Azure AD and Application User"
    author: Rajeev Pentyala
    url: https://rajeevpentyala.com/2018/03/24/step-by-step-d-365-server-to-server-s2s-authentication-using-azure-ad-and-application-user/
    type: technical_blog
    published: 2024-01-15
    reliability: moderate_high
  - id: src8
    title: "Dynamics 365 Application Users - No Additional License Required"
    author: David Robertson (Capgemini)
    url: https://medium.com/capgemini-microsoft-team/dynamics-365-application-users-no-additional-license-required-and-no-limit-on-quantity-4bdc49f8ce2d
    type: technical_blog
    published: 2024-05-20
    reliability: moderate
---

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

## TL;DR

- **Bottom line**: Dynamics 365 (Dataverse) uses Microsoft Entra ID (formerly Azure AD) as its identity provider with OAuth 2.0. For integrations, use the client credentials flow with an application user; for user-context operations, use the authorization code flow with delegated permissions.
- **Key limit**: Access tokens expire in ~60-75 minutes; client secrets max out at 2 years, certificates at 3 years. Plan automated rotation. [src1]
- **Watch out for**: ADAL is fully deprecated since June 2022 - you must use MSAL (Microsoft Authentication Library). Any code still using ADAL will fail. [src1]
- **Best for**: Any application integrating with Dynamics 365 Customer Engagement (Sales, Service, Marketing), Dataverse, or Power Platform APIs.
- **Authentication**: OAuth 2.0 via Microsoft Entra ID. Client credentials (S2S) for unattended integrations; authorization code for user-context; managed identity for Azure-hosted plug-ins. [src1, src2]

## 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 (formerly Azure Active Directory). 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]

| Property | Value |
|---|---|
| **Vendor** | Microsoft |
| **System** | Dynamics 365 (Dataverse) Web API v9.2 |
| **API Surface** | OData v4 (REST) |
| **Current API Version** | v9.2 (2026) |
| **Editions Covered** | Sales, Service, Marketing, Customer Insights, Power Apps (all Dataverse-based) |
| **Deployment** | Cloud (Dynamics 365 Online) |
| **API Docs URL** | [Microsoft Dataverse Web API](https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/overview) |
| **Status** | GA |

## API Surfaces & Capabilities

Dynamics 365 Dataverse exposes a single primary API surface (Web API) for CRUD and business logic, with the Organization Service as the SDK-based alternative for .NET applications. [src1]

| API Surface | Protocol | Best For | Auth Flows Supported | Real-time? | Notes |
|---|---|---|---|---|---|
| Web API (OData v4) | HTTPS/JSON | REST integrations, any language | Client credentials, auth code, ROPC, managed identity | Yes | Primary API for all Dataverse operations |
| Organization Service (SDK) | .NET SDK | .NET applications, plug-ins | Client credentials, auth code, certificate | Yes | Requires Microsoft.PowerPlatform.Dataverse.Client NuGet |
| Dataverse Search API | HTTPS/JSON | Full-text search across entities | Same as Web API | Yes | Separate endpoint: `{org}/api/search/v1.0/query` |

## Rate Limits & Quotas

### Per-Request Limits

| Limit Type | Value | Applies To | Notes |
|---|---|---|---|
| Max records per query | 5,000 | Web API OData query | Use `$top` and `@odata.nextLink` for pagination |
| Max request body size | 128 MB | Web API | Larger payloads should use file/image column upload |
| Max batch subrequests | 1,000 | `$batch` endpoint | Each subrequest counts as a separate API request |
| Max concurrent connections | Per-tenant throttling | All API calls | Returns 429 when exceeded |

[src1]

### Rolling / Daily Limits

| Limit Type | Value | Window | Edition Differences |
|---|---|---|---|
| Power Platform API requests | Varies by license | 24h rolling | Enterprise: 20K/user/day; per-app: 1K/user/day; application user: 250K/tenant/day (pooled) |
| Concurrent batch requests | Throttled per-org | Per-org | Returns 429 Too Many Requests |
| ExecuteMultiple max | 1,000 requests per call | Per-request | Organization Service SDK only |

[src1]

## 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]

| Flow | Use When | Token Lifetime | Refresh? | Notes |
|---|---|---|---|---|
| Client Credentials (S2S) | Server-to-server, no user context, unattended integrations | Access: ~60-75 min | No refresh token; request new token before expiry | Recommended for integrations. Requires application user in Dataverse. [src1, src7] |
| Authorization Code | User-context operations, interactive apps, delegated permissions | Access: ~60-75 min; Refresh: up to 90 days | Yes | Requires redirect URI. User must consent or admin must pre-consent. [src2] |
| Authorization Code + PKCE | SPAs and mobile apps | Access: ~60-75 min; Refresh: 24h (SPA) | Yes (limited for SPA) | Required for public clients. SPA refresh tokens expire after 24h. [src3] |
| ROPC (username/password) | Legacy testing only | Access: ~60-75 min | No | Do NOT use in production. No MFA support. Microsoft discourages this flow. [src1] |
| Managed Identity | Azure-hosted Dataverse plug-ins | Managed by Azure | Automatic | GA for Dataverse plug-ins only. Not for general API access. [src4] |
| Certificate-based | Production S2S where secrets are insufficient | Access: ~60-75 min | No refresh; new assertion per request | More secure than client secrets. Store certs in Azure Key Vault. [src5] |

[src1, src2, src3]

### Authentication Gotchas

- **ADAL is dead**: Microsoft Authentication Library (ADAL) was deprecated in June 2022 and no longer receives security patches. All code must use MSAL. The `CrmServiceClient` class internally used ADAL; migrate to `ServiceClient` which uses MSAL. [src1]
- **Scopes differ by client type**: Public clients use `{org_url}/user_impersonation` scope; confidential clients (S2S) use `{org_url}/.default` scope. Using the wrong scope is a common source of "invalid_grant" errors. [src1]
- **Application users don't need Dataverse API permissions in Entra ID**: For S2S with the application user pattern, you do NOT need to add "Dynamics CRM" or "Dataverse" API permissions to the app registration. The application user's security roles in Dataverse control access, not Entra ID API permissions. Adding the API permission and granting admin consent is only needed for delegated (user-context) flows. [src2, src7]
- **Token lifetime is configurable but has limits**: Tenant admins can modify access token lifetime via token lifetime policies in Entra ID, but Microsoft enforces a minimum of 10 minutes and maximum of 1 day. Default is 60-75 minutes. [src3]
- **Multi-tenant registrations require per-tenant admin consent**: If your app registration is multi-tenant, each target tenant's admin must grant consent before the app can authenticate against their Dataverse instance. [src2]

## Constraints

- **Application users cannot log in interactively** - they are S2S only and cannot use the D365 web UI or perform operations that require interactive context (e.g., certain dialogs). [src7, src8]
- **Client secrets expire** - maximum 2-year lifetime. You must build automated rotation or your integration will silently break. Certificates have a 3-year maximum. [src6]
- **Application users have separate API request limits** - they draw from the tenant's non-licensed user pool (250,000 requests/24h for most plans), not individual user entitlements. Exceeding this returns 429 errors. [src8]
- **ROPC flow does not support MFA** - if the user account has MFA enabled (increasingly common with Conditional Access policies), username/password authentication will fail with AADSTS50076. [src1]
- **Managed identity is limited to plug-in scenarios** - you cannot use managed identity for general Dataverse Web API calls from Azure Functions or App Services. It only works for Dataverse plug-in code accessing other Azure resources. [src4]
- **On-premises D365 Customer Engagement uses claims-based auth, not OAuth 2.0** - this card covers online/cloud only. On-premises uses IFD (Internet-Facing Deployment) with ADFS. [src1]

## Integration Pattern Decision Tree

```
START - Authenticate with Dynamics 365 Dataverse
|-- What type of application?
|   |-- Server/daemon (no user present)
|   |   |-- Running on Azure? (App Service, Functions, AKS)
|   |   |   |-- YES, and accessing other Azure resources from plug-in
|   |   |   |   --> Managed Identity [src4]
|   |   |   |-- YES, general Dataverse API access
|   |   |   |   --> Client Credentials + Certificate (store cert in Key Vault) [src5]
|   |   |   |-- NO (on-premise server)
|   |   |       --> Client Credentials + Certificate or Client Secret [src1]
|   |   |-- What credential type?
|   |       |-- Production --> Certificate (more secure, longer lifetime) [src5]
|   |       |-- Dev/test --> Client Secret (simpler setup) [src6]
|   |-- Interactive web application
|   |   --> Authorization Code flow with PKCE [src2]
|   |-- SPA (single-page app)
|   |   --> Authorization Code + PKCE (public client) [src3]
|   |-- Mobile / desktop app
|   |   --> Authorization Code + PKCE with native redirect [src2]
|   |-- Legacy integration (cannot be updated)
|       --> ROPC (username/password) - ONLY if MFA is disabled [src1]
|-- Multi-tenant or single-tenant?
|   |-- Single-tenant --> Simpler: register in your tenant only
|   |-- Multi-tenant --> Each tenant admin must consent; use /common authority
|-- How to handle token refresh?
    |-- S2S (client credentials) --> No refresh token; request new token proactively before expiry
    |-- Delegated (auth code) --> Use MSAL token cache; refresh tokens last up to 90 days [src3]
    |-- SPA --> Refresh tokens expire in 24h; must re-authenticate interactively [src3]
```

## Quick Reference

| Operation | Endpoint / Config | Value | Notes |
|---|---|---|---|
| Token endpoint (v2.0) | `https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token` | POST | Use v2.0 endpoint for MSAL [src1] |
| Scope (confidential client) | `{org_url}/.default` | e.g., `https://myorg.crm.dynamics.com/.default` | S2S client credentials [src1] |
| Scope (public client) | `{org_url}/user_impersonation` | e.g., `https://myorg.crm.dynamics.com/user_impersonation` | Delegated permissions [src1] |
| API base URL | `https://{org}.crm.dynamics.com/api/data/v9.2/` | GET/POST/PATCH/DELETE | Regional suffixes vary: crm.dynamics.com (NA), crm4.dynamics.com (EMEA), crm5.dynamics.com (APAC) [src1] |
| WhoAmI test | `GET /api/data/v9.2/WhoAmI` | Returns UserId, BusinessUnitId, OrganizationId | Best endpoint to verify auth is working [src1] |
| Application user creation | Power Platform Admin Center > Environments > S2S | Bind app registration to Dataverse user | Assign security roles after creation [src2, src7] |
| MSAL NuGet package | `Microsoft.Identity.Client` | Latest stable | Replaces deprecated `Microsoft.IdentityModel.Clients.ActiveDirectory` [src1] |
| MSAL Python package | `msal` | Latest stable | `pip install msal` [src1] |

## 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 (single-tenant for most integrations), and register. Copy the Application (client) ID and Directory (tenant) ID from the Overview page. [src2]

```bash
# No CLI command needed - use Azure Portal
# Record these values:
# - 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**: Navigate to the App registrations list in Entra ID and confirm your app appears.

### 2. Configure credentials (secret or certificate)

For client secret: Go to Certificates & secrets > Client secrets > New client secret. Set description and expiry (max 24 months). Copy the secret value immediately - it cannot be retrieved later. For certificates: Upload a .cer, .pem, or .crt file under the Certificates tab. [src2, src5]

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

# For production: use Azure Key Vault to generate and manage certificates
# az keyvault certificate create --vault-name MyVault --name D365Cert \
#   --policy "$(az keyvault certificate get-default-policy)"
```

**Verify**: The secret or certificate thumbprint appears under Certificates & secrets in the app registration.

### 3. Create application user in Dataverse

For S2S: Go to Power Platform Admin Center > Environments > select your environment > S2S (under Access) > New app user. Select your app registration, choose the business unit, and assign security roles. The application user is automatically created in Dataverse. [src2, src7]

```plaintext
Power Platform Admin Center steps:
1. Navigate to admin.powerplatform.microsoft.com
2. Select Environments > {your environment}
3. Click "S2S" link under Access section
4. Click "+ New app user"
5. Click "+ Add app" and search for your app registration name
6. Select Business unit
7. Assign security roles (e.g., custom role with appropriate entity permissions)
8. Click "Save" then "Create"
```

**Verify**: In D365, go to Settings > Security > Users > switch to "Application Users" view. Your app user should appear with the Application ID.

### 4. Acquire access token and call API

Use MSAL to acquire a token using client credentials, then include it as a Bearer token in the Authorization header. [src1]

```python
# Input:  tenant_id, client_id, client_secret, org_url
# Output: Access token for Dataverse Web API

import msal
import requests

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

authority = f"https://login.microsoftonline.com/{tenant_id}"
scope = [f"{org_url}/.default"]

app = msal.ConfidentialClientApplication(
    client_id,
    authority=authority,
    client_credential=client_secret
)

# Acquire token (MSAL caches internally)
result = app.acquire_token_for_client(scopes=scope)

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())
else:
    print(f"Error: {result.get('error')}: {result.get('error_description')}")
```

**Verify**: `WhoAmI` returns a JSON object with `UserId`, `BusinessUnitId`, `OrganizationId`. The UserId should match your application user's system user ID.

## Code Examples

### Python: Client Credentials (S2S) with Certificate

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

import msal
import requests

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

# Load certificate (PEM format with private key)
with open("cert.pem", "r") as f:
    cert_data = f.read()
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": "CERTIFICATE_THUMBPRINT_HEX",  # SHA-1 thumbprint
    }
)

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"
    }
    # Example: query accounts
    resp = requests.get(
        f"{org_url}/api/data/v9.2/accounts?$top=10&$select=name",
        headers=headers
    )
    print(resp.json())
else:
    print(f"Auth failed: {result.get('error_description')}")
```

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

```javascript
// Input:  AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET env vars, org URL
// Output: Authenticated Dataverse Web API call

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

const tenantId = process.env.AZURE_TENANT_ID;
const clientId = process.env.AZURE_CLIENT_ID;
const clientSecret = process.env.AZURE_CLIENT_SECRET;
const orgUrl = "https://yourorg.crm.dynamics.com";

async function callDataverse() {
  const credential = new ClientSecretCredential(tenantId, clientId, clientSecret);

  // Request token with Dataverse scope
  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",
    },
  });

  const data = await response.json();
  console.log("UserId:", data.UserId);
  console.log("OrganizationId:", data.OrganizationId);
}

callDataverse().catch(console.error);
```

### C#/.NET: ServiceClient with Client Secret

```csharp
// Input:  Connection string with AuthType=ClientSecret
// Output: Authenticated Dataverse connection via ServiceClient

// NuGet: Microsoft.PowerPlatform.Dataverse.Client (latest stable)
using Microsoft.PowerPlatform.Dataverse.Client;
using Microsoft.Crm.Sdk.Messages;

string orgUrl = "https://yourorg.crm.dynamics.com";
string clientId = "YOUR_CLIENT_ID";
string clientSecret = "YOUR_CLIENT_SECRET";

string connectionString = $@"
    AuthType=ClientSecret;
    Url={orgUrl};
    ClientId={clientId};
    Secret={clientSecret};
    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}");
}
else
{
    Console.WriteLine($"Connection failed: {service.LastError}");
}
```

### cURL: Quick token acquisition and API test

```bash
# Input:  tenant_id, client_id, client_secret, org_url
# Output: Access token + WhoAmI response

# Step 1: Acquire access token
TOKEN=$(curl -s -X POST \
  "https://login.microsoftonline.com/YOUR_TENANT_ID/oauth2/v2.0/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET" \
  -d "scope=https://yourorg.crm.dynamics.com/.default" \
  -d "grant_type=client_credentials" \
  | jq -r '.access_token')

# Step 2: Call WhoAmI to verify
curl -s "https://yourorg.crm.dynamics.com/api/data/v9.2/WhoAmI" \
  -H "Authorization: Bearer $TOKEN" \
  -H "OData-MaxVersion: 4.0" \
  -H "OData-Version: 4.0" \
  -H "Accept: application/json" | jq .

# Expected output:
# {
#   "@odata.context": "https://yourorg.crm.dynamics.com/api/data/v9.2/$metadata#Microsoft.Dynamics.CRM.WhoAmIResponse",
#   "BusinessUnitId": "...",
#   "UserId": "...",
#   "OrganizationId": "..."
# }
```

## Data Mapping

### Authentication Configuration Reference

| Configuration Field | Client Credentials | Auth Code | Certificate | Managed Identity |
|---|---|---|---|---|
| `grant_type` | `client_credentials` | `authorization_code` | `client_credentials` | N/A (automatic) |
| `scope` | `{org_url}/.default` | `{org_url}/user_impersonation` | `{org_url}/.default` | `{org_url}/.default` |
| `client_id` | Required | Required | Required | Not needed |
| `client_secret` | Required | Optional (with PKCE) | Not used | Not needed |
| `client_assertion_type` | Not used | Not used | `urn:ietf:params:oauth:client-assertion-type:jwt-bearer` | Not used |
| `redirect_uri` | Not needed | Required | Not needed | Not needed |
| Token endpoint | v2.0 | v2.0 | v2.0 | `169.254.169.254` (IMDS) |

[src1, src2]

### Data Type Gotchas

- **Regional API endpoints differ**: North America uses `crm.dynamics.com`, EMEA uses `crm4.dynamics.com`, APAC uses `crm5.dynamics.com`, and others. The scope URL must match the org's actual endpoint. Using `crm.dynamics.com` when the org is on `crm4.dynamics.com` will return an "invalid_resource" error. [src1]
- **Tenant ID vs Organization ID**: The tenant ID (Entra ID) is for authentication; the Organization ID (Dataverse) identifies the specific D365 instance. Multi-org tenants have one tenant ID but multiple organization IDs. [src1]
- **Application ID URI auto-generated**: When creating an application user in Dataverse, the Application ID URI is auto-populated from the Entra ID app registration. Do not manually modify it. [src7]

## Error Handling & Failure Points

### Common Error Codes

| Code | Meaning | Cause | Resolution |
|---|---|---|---|
| AADSTS700016 | Application not found | Client ID is wrong or app registration doesn't exist in target tenant | Verify client ID; for multi-tenant, ensure admin consent was granted in target tenant [src2] |
| AADSTS7000215 | Invalid client secret | Secret expired or was incorrectly copied | Rotate the client secret; copy the Value (not the ID) from Entra ID [src6] |
| AADSTS50076 | MFA required | Using ROPC flow with MFA-enabled account | Switch to authorization code flow or use S2S with application user [src1] |
| AADSTS65001 | Consent not granted | User or admin has not consented to the required permissions | Grant admin consent in Entra ID > App registrations > API permissions [src2] |
| AADSTS700027 | Certificate validation failed | Wrong thumbprint, expired cert, or private key mismatch | Verify certificate thumbprint (SHA-1 hex); ensure private key matches uploaded public key [src5] |
| 401 Unauthorized | Invalid or expired token | Access token is expired, malformed, or for wrong audience | Check token expiry; verify scope matches org URL; acquire new token [src1] |
| 403 Forbidden | Insufficient privileges | Application user lacks required security roles in Dataverse | Assign appropriate security roles to the application user via PPAC [src7] |
| 429 Too Many Requests | Rate limit exceeded | Too many API calls from this user/org | Implement exponential backoff; respect Retry-After header [src1] |

[src1, src2, src5, src6]

### Failure Points in Production

- **Client secret expiration breaks integrations silently**: Secrets expire after their configured lifetime (max 2 years). There is no automatic notification. Fix: `Implement a secret rotation process. Use Azure Key Vault with notification webhooks, or set calendar reminders 30 days before expiry. Better yet, use certificates stored in Key Vault with auto-rotation.` [src6]
- **Token cache misuse causes unnecessary token requests**: Requesting a new token for every API call wastes time and may hit rate limits. Fix: `Use MSAL's built-in token cache. ConfidentialClientApplication.acquire_token_for_client() returns cached tokens automatically if still valid.` [src1]
- **Wrong scope causes "invalid_grant"**: Using `user_impersonation` scope with client credentials flow, or `.default` scope with delegated flow. Fix: `Client credentials always use {org_url}/.default; delegated flows use {org_url}/user_impersonation.` [src1]
- **Application user deleted or disabled in Dataverse**: If someone removes the application user from Dataverse, the app can still get tokens from Entra ID but all API calls return 403. Fix: `Monitor application user status in Dataverse. Set up alerts for security role changes.` [src7]
- **Conditional Access policies block service principals**: Some organizations apply Conditional Access policies that require compliant devices or specific locations, which block S2S app-only authentication. Fix: `Exclude the service principal from location/device-based Conditional Access policies, or use a named location exception.` [src1]

## Anti-Patterns

### Wrong: Hardcoding credentials in source code

```python
# BAD - credentials in source code, visible in version control
client_id = "51f81489-12ee-4a9e-aaae-a2591f45987d"
client_secret = "myS3cretV@lue!"
```

### Correct: Use environment variables or key vault

```python
# GOOD - credentials from environment or Azure Key Vault
import os
client_id = os.environ["AZURE_CLIENT_ID"]
client_secret = os.environ["AZURE_CLIENT_SECRET"]

# Even better: use DefaultAzureCredential which chains multiple auth methods
from azure.identity import DefaultAzureCredential
credential = DefaultAzureCredential()  # tries managed identity, then env vars, then CLI
```

### Wrong: Requesting a new token for every API call

```python
# BAD - acquires a new token on every request, wastes time and risks throttling
for record in records:
    result = app.acquire_token_for_client(scopes=scope)
    headers = {"Authorization": f"Bearer {result['access_token']}"}
    requests.post(url, headers=headers, json=record)
```

### Correct: Use MSAL token cache, acquire once

```python
# GOOD - MSAL caches the token internally; returns cached token if still valid
result = app.acquire_token_for_client(scopes=scope)
headers = {"Authorization": f"Bearer {result['access_token']}"}

for record in records:
    # Token is reused from cache until near expiry
    # MSAL auto-refreshes when token is within 5 min of expiry
    requests.post(url, headers=headers, json=record)
```

### Wrong: Using ROPC (username/password) for production integrations

```python
# BAD - ROPC breaks when MFA is enabled, doesn't support Conditional Access
result = app.acquire_token_by_username_password(
    scopes=scope,
    username="admin@contoso.com",
    password="P@ssw0rd!"
)
```

### Correct: Use client credentials with application user

```python
# GOOD - S2S with application user, no interactive login needed, supports MFA orgs
app = msal.ConfidentialClientApplication(
    client_id,
    authority=f"https://login.microsoftonline.com/{tenant_id}",
    client_credential=client_secret  # or certificate dict
)
result = app.acquire_token_for_client(scopes=[f"{org_url}/.default"])
```

## Common Pitfalls

- **Using the v1.0 token endpoint instead of v2.0**: The v1.0 endpoint (`/oauth2/token`) uses `resource` parameter; the v2.0 endpoint (`/oauth2/v2.0/token`) uses `scope`. MSAL uses v2.0 by default. Mixing them causes "unsupported_grant_type" errors. Fix: `Always use the v2.0 endpoint with MSAL. If using raw HTTP requests, use scope={org_url}/.default, not resource={org_url}.` [src1]
- **Forgetting to assign security roles to the application user**: The app registration and token work fine, but every Dataverse API call returns 403 Forbidden. Fix: `Assign at least one security role to the application user in the Power Platform Admin Center.` [src7]
- **Client secret value vs ID confusion**: The Entra ID portal shows both a "Secret ID" (GUID) and a "Value" (the actual secret string). Using the Secret ID as the credential fails with AADSTS7000215. Fix: `Copy the Value column immediately after creation - it is only shown once.` [src6]
- **Testing with admin account, deploying with restricted application user**: Everything works in dev because the test account has System Administrator role, but fails in production because the application user has a custom role missing key privileges. Fix: `Test with the actual application user security role before deploying. Use the WhoAmI and RetrieveMultiple endpoints to validate access patterns.` [src7]
- **Not handling token expiry in long-running processes**: A batch job running >1 hour uses the original token, which expires mid-process. Fix: `Check token expiry before each batch of requests. MSAL's acquire_token_for_client automatically returns a fresh token from cache or re-acquires if expired.` [src1]
- **Ignoring regional endpoints**: North America orgs use crm.dynamics.com, EMEA uses crm4.dynamics.com, etc. Using the wrong regional endpoint in the scope causes authentication to succeed but API calls to fail. Fix: `Use the org's actual URL from the environment details in Power Platform Admin Center.` [src1]

## Diagnostic Commands

```bash
# Acquire token and inspect claims (useful for debugging permission issues)
TOKEN=$(curl -s -X POST \
  "https://login.microsoftonline.com/YOUR_TENANT_ID/oauth2/v2.0/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "client_id=YOUR_CLIENT_ID&client_secret=YOUR_SECRET&scope=https://yourorg.crm.dynamics.com/.default&grant_type=client_credentials" \
  | jq -r '.access_token')

# Decode token payload (base64, no verification) 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 'YOUR_CLIENT_ID'&\$select=fullname,isdisabled&\$expand=systemuserroles_association(\$select=name)" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Accept: application/json" | jq .

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

# List app registration certificates and secrets with expiry
az ad app credential list --id YOUR_CLIENT_ID -o table
```

## Version History & Compatibility

| Change | Date | Impact | Migration Notes |
|---|---|---|---|
| MSAL required (ADAL deprecated) | 2022-06 | Breaking | Replace all `Microsoft.IdentityModel.Clients.ActiveDirectory` references with `Microsoft.Identity.Client`. `CrmServiceClient` -> `ServiceClient`. [src1] |
| Azure AD renamed to Microsoft Entra ID | 2023-07 | Branding only | No code changes needed. Portal URLs and documentation updated. Old URLs redirect. |
| Power Platform managed identity GA | 2024-11 | New feature | Available for Dataverse plug-ins only. Uses federated identity credentials. [src4] |
| ServiceClient replaces CrmServiceClient | 2022-03 | Recommended migration | `ServiceClient` uses MSAL internally. Connection string format unchanged but uses updated auth libraries. [src1] |
| v2.0 token endpoint recommended | 2020-06 | Best practice | v1.0 endpoint still works but v2.0 is the standard for MSAL. Use `scope` parameter instead of `resource`. [src1] |

[src1, src4]

### Deprecation Policy

Microsoft follows a minimum 12-month deprecation notice for authentication library changes (e.g., ADAL deprecation was announced 18 months before end-of-support). Token endpoint versions (v1.0 vs v2.0) coexist indefinitely, but new features are only added to v2.0. Always check the Microsoft Entra ID "What's new" page for upcoming changes. [src1]

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Building server-to-server integrations with Dataverse-based D365 apps (Sales, Service, Marketing) | Integrating with D365 Business Central | Business Central's own S2S flow (different API permissions: API.ReadWrite.All, Automation.ReadWrite.All) |
| Need unattended daemon/service access to D365 data | Need user-context operations where audit trail must show the actual human user | Authorization code flow with delegated permissions |
| Want to avoid consuming a paid D365 license for integration accounts | Integrating with D365 Finance & Operations (Unified API) | F&O uses different resource URI and separate Entra ID app config |
| Production integration requiring certificate-based security | Quick ad-hoc testing or prototyping | Client secret with short expiry for dev/test |
| Azure-hosted Dataverse plug-ins accessing Azure resources | Azure Functions calling Dataverse Web API directly | Client credentials (managed identity is plug-in only for Dataverse) [src4] |

## Important Caveats

- Authentication infrastructure is shared across all Dataverse-based D365 apps (Sales, Service, Marketing, Customer Insights), but D365 Business Central and D365 Finance & Operations have different authentication patterns and token endpoints. Do not mix them. [src1]
- Token lifetime policies applied at the Entra ID tenant level affect all applications in that tenant, including D365 integrations. A change by another team can unexpectedly shorten your token lifetimes. [src3]
- Application users created in one Dataverse environment are not automatically available in other environments within the same tenant. You must create the application user separately in each environment where access is needed. [src7]
- Microsoft's Conditional Access policies are increasingly aggressive with defaults. New tenants may have "Security Defaults" enabled, which can interfere with service principal authentication. Verify Conditional Access exclusions for your app's service principal. [src1]
- Rate limits and API request quotas for application users are pooled at the tenant level (not per-environment), meaning heavy usage in one environment can affect availability in another. [src8]

## Related Units

- [Dynamics 365 Dataverse Web API capabilities and rate limits](/business/erp-integration/dynamics-365-rest-api/2026)
- [Salesforce to Dynamics 365 integration playbook](/business/erp-integration/salesforce-dynamics-365-integration/2026)
- [Dynamics 365 Business Central S2S authentication](/business/erp-integration/dynamics-365-bc-authentication/2026)
