Someone in your finance department installs a “smart invoicing” app from a vendor demo. The consent dialog says it wants to “Read and write financials data.” They click Accept because they always click Accept. Six weeks later that vendor has a persistent OAuth token that can read your entire chart of accounts, post journal entries, and pull every customer invoice — and it never touched a firewall, a VPN, or an admin.
That’s the actual story behind the Entra changelog line that reads “Added financials APIs for Dynamics 365 Business Central.” It’s filed under a Graph API reference and it looks like ERP plumbing that has nothing to do with identity. It has everything to do with identity. Every endpoint Microsoft adds to Graph is backed by an Entra permission, and permissions are the thing your users hand out for free.
What actually shipped
The Financials API for Dynamics 365 Business Central is now exposed under the Microsoft Graph v1.0 endpoint — general availability, not preview. It surfaces resources you’d expect from an ERP: companies, accounts, customers, vendors, salesInvoices, journals, generalLedgerEntries, and the rest of the ledger. The reference lives under the Graph dynamics-graph-reference resource set.
Who’s affected: any tenant with Dynamics 365 Business Central licensed and provisioned. If you don’t run Business Central, this API is inert in your tenant — the permission exists in the catalog but there’s nothing behind it to grant access to. If you do run Business Central, your app registration and consent posture just gained a new attack surface that reads money.
The permission that gates it is Financials.ReadWrite.All, published on the Microsoft Graph resource application (00000003-0000-0000-c000-000000000000) as both a delegated scope and an application role. There is no read-only variant. That’s the first thing Microsoft isn’t shouting about: it’s read/write or nothing. An app that only needs to display invoices on a dashboard still asks for the ability to post to your ledger, because Microsoft never split the scope.
Why this is better than the old way (and where it still bites)
The genuine improvement is real. Before this, integrating Business Central meant its own OData/SOAP web services, per-environment service accounts, and basic-auth API keys that got pasted into config files and never rotated. Moving Financials onto Graph means:
- One token model. The same OAuth 2.0 / Entra token that governs Exchange, SharePoint, and Teams now governs your ERP. Conditional Access, token lifetime, and revocation apply uniformly instead of Business Central being an island with its own auth.
- Workload identity governance. Because access runs through a service principal, you can put it under Conditional Access for workload identities, apply risk-based blocking, and audit it in the sign-in logs — none of which the legacy web-service keys supported.
- No standing secrets in code. App-only access uses certificates or federated identity credentials, so you can finally kill the basic-auth key that’s been in someone’s PowerShell script since 2021.
Against a competing stack — say, a third-party iPaaS bridging QuickBooks or SAP with bearer tokens minted outside your directory — the Graph model wins on one thing that matters: the token lives in your tenant and dies when you revoke it. You are never phoning a vendor to ask them to please stop having access.
The part that bites in six months: consent sprawl. The moment Financials is a Graph scope, it’s in the same consent prompt pool as every low-risk scope, and your default user consent settings decide whether a finance clerk can grant it to a random multi-tenant app without an admin ever seeing it.
The adjacent changes that make this urgent
This API doesn’t land in a vacuum. Three things in the current Entra surface change how you should treat it:
1. Azure AD Graph is gone — so this is your only ledger API path
The retirement of the legacy Azure AD Graph endpoints is complete, and Microsoft has been steadily forcing apps onto Microsoft Graph. If you were putting off consolidating ERP integrations, there’s no longer a legacy path to fall back on. Everything routes through Graph permissions now, which means your Graph permission hygiene is your integration security posture.
2. The Microsoft.Entra PowerShell module is GA
The Microsoft.Entra module reached general availability and is the intended successor to the deprecated AzureAD and MSOnline modules — both of which are retired and will stop authenticating. If your app-audit scripts still start with Connect-AzureAD, they’re on borrowed time. Everything below uses Microsoft.Graph, which is fully supported and interoperable.
3. App consent policies and the admin consent workflow
Microsoft’s recommended default — and the one you should already have on — restricts non-admin users to consenting only to apps from verified publishers requesting low-impact permissions. Financials.ReadWrite.All is not low impact. If your tenant still runs the old “users can consent to all apps” setting, a finance user can grant ledger access with two clicks. Check this before you do anything else.
Audit what can touch your ledger
Before you worry about new grants, find out what already holds Financials access. This script reports every service principal with the application permission and every delegated grant — with pagination and error handling, read-only.
#Requires -Modules Microsoft.Graph.Applications, Microsoft.Graph.Authentication
# Read-only audit of who can reach Business Central Financials via Graph
Connect-MgGraph -Scopes 'Application.Read.All','Directory.Read.All','DelegatedPermissionGrant.Read.All' -NoWelcome
$graphAppId = '00000003-0000-0000-c000-000000000000' # Microsoft Graph
$targetScope = 'Financials.ReadWrite.All'
try {
$graphSp = Get-MgServicePrincipal -Filter "appId eq '$graphAppId'" -ErrorAction Stop
}
catch {
Write-Error "Could not resolve the Microsoft Graph service principal: $($_.Exception.Message)"
return
}
# Resolve the app role (application permission) id for Financials.ReadWrite.All
$appRole = $graphSp.AppRoles | Where-Object { $_.Value -eq $targetScope }
if (-not $appRole) {
Write-Warning "'$targetScope' is not published in this tenant's Graph catalog. Nothing to audit."
return
}
Write-Host "=== Application (app-only) grants of $targetScope ===" -ForegroundColor Cyan
# App role assignments TO Microsoft Graph, filtered to the Financials role
$appGrants = Get-MgServicePrincipalAppRoleAssignedTo -ServicePrincipalId $graphSp.Id -All -ErrorAction SilentlyContinue |
Where-Object { $_.AppRoleId -eq $appRole.Id }
if (-not $appGrants) {
Write-Host " None." -ForegroundColor Green
}
else {
foreach ($g in $appGrants) {
[pscustomobject]@{
Type = 'Application'
PrincipalId = $g.PrincipalId
DisplayName = $g.PrincipalDisplayName
GrantedOn = $g.CreatedDateTime
AssignmentId = $g.Id
}
}
}
Write-Host "`n=== Delegated grants of $targetScope ===" -ForegroundColor Cyan
# OAuth2 delegated grants against Graph, matched on the scope string (paged)
$delegated = Get-MgOauth2PermissionGrant -All -Filter "resourceId eq '$($graphSp.Id)'" -ErrorAction SilentlyContinue |
Where-Object { $_.Scope -match [regex]::Escape($targetScope) }
if (-not $delegated) {
Write-Host " None." -ForegroundColor Green
}
else {
foreach ($d in $delegated) {
$clientSp = Get-MgServicePrincipal -ServicePrincipalId $d.ClientId -ErrorAction SilentlyContinue
[pscustomobject]@{
Type = 'Delegated'
ClientApp = $clientSp.DisplayName
ClientId = $d.ClientId
ConsentType = $d.ConsentType # AllPrincipals = admin-consented tenant-wide
PrincipalId = $d.PrincipalId # null when AllPrincipals
GrantId = $d.Id
}
}
}
Disconnect-MgGraph | Out-Null
Feed that into | Format-Table -AutoSize or | Export-Csv. The ConsentType of AllPrincipals is the one to stare at — it means an admin granted the app access on behalf of the whole tenant, so every user’s Business Central context is reachable through that client.
Check your consent default, then close the door
Reporting is half the job. Here’s the audit for your tenant-wide user consent policy — read-only — so you know whether a clerk can grant Financials without you:
#Requires -Modules Microsoft.Graph.Identity.SignIns, Microsoft.Graph.Authentication
Connect-MgGraph -Scopes 'Policy.Read.All' -NoWelcome
$auth = Get-MgPolicyAuthorizationPolicy -ErrorAction Stop
[pscustomobject]@{
AllowUserConsentForApps = $auth.DefaultUserRolePermissions.PermissionGrantPoliciesAssigned
Note = 'Empty array = users cannot consent. "ManagePermissionGrantsForSelf.microsoft-user-default-low" = low-impact only (recommended).'
} | Format-List
Disconnect-MgGraph | Out-Null
If you decide to revoke a specific risky delegated grant, do it deliberately — never pipe a live query straight into a delete. This wrapper defaults to a dry run and forces you to opt in:
function Revoke-FinancialsGrant {
[CmdletBinding(SupportsShouldProcess, ConfirmImpact='High')]
param(
[Parameter(Mandatory)][string]$GrantId,
[switch]$Execute # nothing happens without this
)
# Connect-MgGraph -Scopes 'DelegatedPermissionGrant.ReadWrite.All' first
if (-not $Execute) {
Write-Host "DRY RUN: would remove OAuth2 grant $GrantId. Re-run with -Execute to apply." -ForegroundColor Yellow
return
}
if ($PSCmdlet.ShouldProcess($GrantId, 'Remove delegated permission grant')) {
try { Remove-MgOauth2PermissionGrant -OAuth2PermissionGrantId $GrantId -ErrorAction Stop
Write-Host "Removed $GrantId." -ForegroundColor Green }
catch { Write-Error "Failed to remove $GrantId : $($_.Exception.Message)" }
}
}
Where to verify this in the admin center
- Enterprise applications → Consent and permissions → User consent settings — confirm it’s set to “Allow user consent for apps from verified publishers, for selected permissions.” Turn on the admin consent workflow under the same node so risky requests route to you instead of dying silently.
- Enterprise applications → [app] → Permissions — read the granted Graph permissions for any Business Central connector;
Financials.ReadWrite.Allshould be there if the app touches the ledger. - Identity → Monitoring & health → Sign-in logs → Service principal sign-ins — watch for the connector’s app-only sign-ins to confirm what’s actually calling the API versus what merely holds the permission.
- Protection → Conditional Access → Workload identities — scope a policy to the connector’s service principal to block it from unexpected IP ranges.
Bottom line
- Run the audit script today if you have Business Central. You want the list of principals holding
Financials.ReadWrite.Allbefore an auditor asks for it. - Fix your user consent default if it still allows all apps. This single setting decides whether ledger access is a self-service giveaway.
- Turn on the admin consent workflow so Financials requests reach a human.
- Retire legacy Business Central web-service keys and move integrations onto Graph app-only auth with certificates or federated credentials.
- Migrate your audit tooling off
AzureAD/MSOnline— they’re retired — ontoMicrosoft.Graphor GAMicrosoft.Entra.
The API is a genuine upgrade over pasted-in keys. But Microsoft shipped a read/write-only scope into the same consent pool as calendar-read permissions, and left the door-lock setting up to you. Go check the lock.