An Azure Function that writes orders into Dataverse on a schedule. A deployment pipeline that imports solutions without a human signed in. A Logic App that reads customer data for enrichment and pushes it back. All three need an identity that is not a human user.
Dataverse supports this via application users - entries in the system user table that are linked to an Azure AD service principal. The setup has six steps and four places something can go wrong. Here is the flow we use, and the failure modes we debug most often.
The vocabulary
Two concepts, same identity:
- Service principal (SP): the Azure AD-side representation. An SP is created when you register an application in Azure AD. It has a client ID (app ID) and a client secret (or certificate) used for authentication.
- Application user: the Dataverse-side representation. A row in the systemuser table with the applicationid column set to the SP's client ID. Has a security role, a business unit - just like a human user, minus the human.
Creating one without the other gets you half an auth story. Both must exist and be linked.
The setup, end to end
Step 1: register the application in Azure AD
In the Azure portal, App Registrations → New registration.
- Name: something meaningful. sapota-corp-alm-deploy-prod, not TestApp1.
- Supported account types: typically "single tenant."
- Redirect URI: not needed for service-to-service, skip.
Grab the Application (client) ID and Directory (tenant) ID from the Overview page. You'll need both in several places downstream.
Step 2: create a client secret (or certificate)
In the App Registration, Certificates & secrets → New client secret. Set an expiration (6 months to 2 years, project-dependent). Copy the Value immediately - it is only shown once.
For higher security, use a certificate instead. Certificate-based auth rotates less frequently and is harder to leak accidentally into logs.
Step 3: grant Dataverse API permissions to the app
In the App Registration, API permissions → Add a permission → Dynamics CRM → Delegated permissions → check user_impersonation. Grant admin consent.
(For application-level access rather than delegated, the permission name and flow differ. Most service-to-service scenarios use delegated with user_impersonation because Dataverse has its own per-user security model.)
Step 4: create the application user in Dataverse
In the Power Platform Admin Center, select the target environment → Settings → Users + permissions → Application users → New app user.
Pick the Azure AD app you registered, pick a business unit, assign security roles. Save.
The security roles determine what the SP can do. For a deployment pipeline, a custom role acme_pipeline_deploy with the privileges for solution import (and nothing else) is the right scope. For an integration that writes orders, a role scoped to the tables it needs.
Step 5: verify with a token request
From any machine with the client ID, client secret, and tenant ID, request a token:
You get back an access token. Use it to hit the Web API:
If this returns an account row, the chain works end-to-end. If it fails, read the next section.
Step 6: wire the credentials into the consumer
For an Azure Function:
- Store client ID, secret, and tenant ID in the Function App's Application Settings (with Key Vault references for the secret).
- Use the Microsoft Identity SDK (Microsoft.Identity.Client) to acquire tokens.
- Use tokens to authenticate to Dataverse either via the SDK or the Web API.
For a pipeline:
- Azure DevOps: service connection of type "Power Platform," configured with the SP credentials.
- GitHub Actions: federated credentials (preferred) or secrets with the SP credentials.
Failure mode 1: app user not provisioned
Symptom: token request succeeds, Web API call returns 401 Unauthorized with message "User is not authorized to access this service."
Cause: the SP is a valid Azure AD identity, but Dataverse has never heard of it. Step 4 (create application user) was skipped or pointed at the wrong app.
Fix: create the application user as in Step 4, linking the SP's client ID.
Failure mode 2: wrong business unit
Symptom: authenticated calls succeed but return fewer rows than expected. Or: writes succeed but show up in an unexpected BU.
Cause: the application user was provisioned in the wrong business unit. Every record the SP creates defaults to the SP's BU.
Fix: change the app user's business unit to the correct one (typically root BU for admin-like SPs, specific country BU for country-scoped integrations).
Failure mode 3: expired client secret
Symptom: everything worked last week; this week, all calls fail with auth errors.
Cause: the client secret reached its expiration. Azure AD rejects requests with an expired secret.
Fix: short term, generate a new secret and update the consumer's configuration. Long term, set calendar reminders 30 days before secret expiration to rotate. Longer term, migrate to certificate auth (fewer rotations) or to managed identities (no secrets at all).
Failure mode 4: security role gaps
Symptom: authenticated calls succeed on read operations but fail on writes with "Insufficient privileges."
Cause: the role assigned to the app user doesn't include the specific privilege needed. Dataverse privileges are granular (Create, Read, Write, Delete, Append, Append To, Assign, Share, plus scope: User/BU/Parent:Child/Org).
Fix: audit what the SP does and assign a role with exactly those privileges plus a small buffer. The "System Administrator" role works for development but is massive over-grant for production.
The managed identity alternative
Azure services (Function App, App Service, VMs) can have a managed identity - an SP that Azure manages for you. No client secret. Authentication from the service to Azure AD is handled by the host automatically.
For a Function App calling Dataverse:
- Enable system-assigned managed identity on the Function App (Settings → Identity).
- In Dataverse, create an application user linked to the managed identity's object ID (same process as step 4, but the ID comes from the managed identity rather than a separate App Registration).
- In code, use DefaultAzureCredential or ManagedIdentityCredential from the Azure Identity SDK. No secrets in config.
Advantages: no secrets to rotate, no secrets to leak, auth is automatic. Disadvantages: only works for compute running in Azure; cannot be used from a developer's local machine for debugging (hence DefaultAzureCredential which falls back to interactive auth locally).
We use managed identity for anything that runs in Azure permanently. SP with client secret for pipelines (Azure DevOps supports federated credentials to avoid the secret, when configured correctly). Certificate-based SP for legacy on-prem scenarios where managed identity isn't available.
Audit: who has an application user in Prod?
Every quarter, we list application users in each Prod environment and confirm each one is still in use:
For every result:
- What service uses this SP?
- Where is the secret stored?
- When does the secret expire?
- What role does the app user have?
Stale SPs (nothing is using them anymore) get their app user soft-deleted. Active ones that are over-privileged get their role scope reduced. SPs with secrets approaching expiration get rotation scheduled.
This is boring hygiene. It is also the reason we have not had an auth-related production incident in fourteen months.