Graph app setup

Manage365 talks to Microsoft Graph through an Entra app registration that lives in your own MSP tenant. This page covers the one-click wizard, what the manifest grants, the difference between admin consent and directory roles, and how to verify the whole chain works end-to-end.

For the separate question of how customer tenants connect via GDAP, see that page. The Graph app and GDAP are independent: the app is your authentication identity, GDAP is the customer-tenant relationship that authorises the app to do things on the customer's behalf.

The wizard at /settings/graph-setup

The wizard does five things in one device-code flow:

  1. Authenticates you against your MSP tenant via Microsoft's well-known device-code public client
  2. Creates (or updates, on re-run) a multi-tenant app registration named Manage365 — <your MSP>
  3. Generates a self-signed RSA cert and attaches it to the app's keyCredentials
  4. PATCHes the app's requiredResourceAccess with the current manifest (Graph + SharePoint Online + Office 365 Exchange Online)
  5. Returns an admin-consent URL using scope=.default so a single click consents the entire manifest at once

Re-running the wizard on an existing setup is safe: the app registration is updated in place (same client ID, same cert), the manifest is refreshed, and a fresh consent URL is issued. The token cache is flushed automatically when consent completes.

Manifest version & the “new permissions available” banner

Each release of Manage365 stamps the canonical app manifest with a version number. When you set up your Graph app, the version at that moment is recorded against your credential row. When the platform's current version moves ahead of yours, the wizard shows a yellow banner:

New GDAP permissions available — Manage365 has added permissions since you last ran one-click setup (X total now, manifest vN — you're on vN-1). Re-run setup to PATCH the Entra app and consent to the new scopes. Existing GDAP consent stays intact.

Click Refresh scopes to re-run the wizard. The existing app is PATCHed (not replaced), the new admin-consent URL grants any newly-added permissions, and you keep the same client ID + cert so customer-tenant GDAP consents stay valid.

What changed in v13

The most recent manifest update (v13) does two things:

  1. Moves Exchange.ManageAsApp to the correct resource. v11 declared this app role under Microsoft Graph — the wrong resource. Microsoft silently ignored the entry at consent time, so cert-based Connect-ExchangeOnline returned UnAuthorized even when the service principal had the Exchange Administrator directory role. v12 introduced a dedicated Office 365 Exchange Online resource block; v13 ships alongside v12 in the same release.
  2. Adds Application.ReadWrite.OwnedBy. Lets the app PATCH its own registration via cert-based app-only auth. Future manifest updates can be self-applied without another device-code flow — re-consent is still needed for the new permissions to land in tokens, but the manifest update step itself becomes invisible.

The Verify Exchange button

On the Graph setup page, the Verify Exchange connection card runs an end-to-end probe:

  1. Health-checks the PowerShell sidecar container
  2. Picks your partner tenant (or any customer tenant if no partner tenant is flagged) as the target
  3. Uses your stored cert to mint an Exchange Online token
  4. Calls Connect-ExchangeOnline -Certificate via the sidecar
  5. Runs Get-AcceptedDomain as the cheapest cmdlet that exercises the full auth path

The result is one of six structured stages — each maps to a specific failure cause with an actionable fix:

stageWhat it meansFix
verifiedSidecar returned the accepted-domain list. Auth chain works.Nothing — you're done.
unauthorizedApp role Exchange.ManageAsApp not granted. Most common cause: manifest updated but consent not re-run.Click Refresh scopes → device-code → Grant admin consent. Make sure “Office 365 Exchange Online: Manage Exchange As Application” appears in the consent list.
tenant_not_foundDefault domain on the tenant is a placeholder GUID, not a real onmicrosoft.com domain.PowerShellService self-heals via Graph lookup on next call. Re-run verify in 60 seconds. If it persists, check customer_tenants.default_domain.
guid_organizationSame as above, caught at a different point.Same — wait for self-heal or fix the DB row.
sidecar_downThe manage365-powershell-1 container isn't running or isn't reachable on port 7790.docker compose up -d powershell
sidecar_errorSome other sidecar failure. Raw PowerShell error is in the diagnostic field.Read the diagnostic. Common causes: missing cmdlet allowlist entry, Graph token scope mismatch.
no_tenantNo tenants linked to your MSP at all.Run GDAP setup first, or add the partner tenant via “My tenant” → “Manage as tenant”.

Admin consent vs directory roles

Two separate authorisation surfaces have to align before cert-based Exchange Online PowerShell works. People often confuse them, so here's the split:

Admin consent (per app role, granted to the service principal)

When you click Grant admin consent, Microsoft records — for each app role declared on the app — a row in the service principal'sappRoleAssignments collection. Without that row, the token Microsoft issues won't carry the role claim, regardless of what directory roles the SP holds.

This is why moving Exchange.ManageAsApp to the correct resource block in v12+ was essential: declaring it under Graph silently created no appRoleAssignment row at all (Graph doesn't expose that role) so the resulting token had zero roles on the Exchange audience.

Directory roles (per tenant, granted to the service principal)

Independent of admin consent. Granting the SP the Exchange Administrator role in your MSP tenant authorises it to perform Exchange admin operations once it's authenticated. But Exchange Online still requires the Exchange.ManageAsApp app role in the access token — directory role alone is not enough.

Both required

For a fully-working sidecar:

  • Admin consent granted (so Exchange.ManageAsApp is in the token)
  • Service principal assigned the Exchange Administrator directory role in the target tenant (so Exchange authorises the operation once the token validates)

For your own MSP tenant, the directory role is granted manually once. For customer tenants, the role flows in via the GDAP relationship's role template — make sure the template you use during invite includes Exchange Administrator if you plan to use sidecar-backed Exchange functions on that customer.

What the manifest grants

The current manifest declares ~29 application roles plus the delegated scopes used during the device-code flow. The full list is shown in the wizard's “Required permissions” section and is also returned by GET /settings/graph-credentials/required-scopes. Highlights:

  • Identity — User.ReadWrite.All, Directory.ReadWrite.All, Group.ReadWrite.All, Application.Read.All, AuditLog.Read.All, UserAuthenticationMethod.Read.All
  • Policy — Policy.Read.All, Policy.ReadWrite.ConditionalAccess
  • Security — SecurityEvents.Read.All, SecurityIncident.Read.All, SecurityAlert.Read.All, IdentityRiskyUser.Read.All, IdentityRiskEvent.Read.All
  • Intune — DeviceManagement* including PrivilegedOperations.All for wipe/retire/sync, BitlockerKey.Read.All
  • Reports + service health — Reports.Read.All, ServiceHealth.Read.All
  • Mail — Mail.Send (alert dispatcher), MailboxSettings.Read
  • SharePoint — Sites.Read.All on Graph plus Sites.FullControl.All on the SharePoint resource
  • Exchange — Exchange.ManageAsApp on the Office 365 Exchange Online resource
  • Partner / GDAP — DelegatedAdminRelationship.Read.All + ReadWrite.All
  • Self-management — Application.ReadWrite.OwnedBy (self-patch)

Common pitfalls

  • Consent skipped a permission. If a feature returns 403 after consent, the most likely cause is that the consent URL used a scope-list instead of .default. The wizard always uses .default now, but old credentials might have consented before that fix landed. Click Re-run admin consent (grants all permissions) on the Graph setup page.
  • Wrong tenant connected. If the wizard was run from a customer tenant rather than your partner tenant, the SP ends up in the wrong place. The Partner status card surfaces this with a Reset button.
  • Cert expired. Cert lifetime is governed by the tenant's app-management policy. The wizard reads the policy before attaching and picks the longest lifetime allowed (up to 24 months). The Cert status card shows days remaining; renew before it hits zero.
  • Sidecar Exchange returns UnAuthorized after consent. Check both: did the consent URL include “Office 365 Exchange Online: Manage Exchange As Application”? Was the Exchange Administrator directory role granted to the SP in the target tenant? Run Verify Exchange to get a structured diagnostic.