---
name: microsoft-entra-app-registration
description: The shared Microsoft Entra ID (formerly Azure AD) app-registration mechanics behind every Microsoft OAuth2 client — the Entra admin center, registering the app, supported account types and what they cap, redirect-URI platform types, delegated vs application permissions, admin consent and who is allowed to grant it, the 24-month client secret and its one-time Value, certificates, `offline_access`, publisher verification, and the AADSTS errors these produce. Read this first when registering any Entra app for Microsoft Graph; the product skills — Microsoft Graph sign-in, SharePoint — build on it and cover only what their product adds. Use directly when the task is a plain Entra ID / Microsoft Graph OAuth client and no particular Microsoft product is named.
---

# Microsoft Entra ID — shared app-registration mechanics

Everything common to registering an OAuth2 client for **any** Microsoft Graph–backed product: the owning tenant, the
app registration, supported account types, redirect URIs, Graph permissions and consent, the client secret, and
publisher verification.

The product skills — Microsoft Graph sign-in, SharePoint — assume this file and cover only what their product adds
(a `Sites.Selected` grant, a dated scope set, a retirement deadline). **Read this first, then the product skill.**
If you are registering a plain Entra app with no particular Microsoft product in play, this file is the whole job.
Every Microsoft Graph connector — Outlook, OneDrive, SharePoint, Teams, Entra directory, Excel, To Do — registers
exactly the same way; only the permission strings differ.

Three decisions here are expensive to reverse, and none of them fail at save time:

1. **Supported account types.** One dropdown decides whether customer tenants can use the app at all, caps how many
   Graph permissions you may ever request, and changes the redirect-URI rules. Changing it later touches every
   customer. §3.
2. **Permissions and admin consent.** Which Graph permissions you pick decides whether an ordinary user can click
   through your connect flow or whether every customer needs a tenant administrator first — and for Graph
   *application* permissions, an Application Administrator is not enough. You find this out at the customer's consent
   screen, not in the portal. §5, §6.
3. **The client secret expires.** Microsoft caps secret lifetime at **24 months** with no never-expires option, and
   says secrets should not be used in production at all. Every Entra app you register is a recurring calendar event,
   not a one-off. §7.

Everything else — the portal path, the redirect URIs, the endpoints — is mechanical.

## Inputs to collect before you start

Ask in one batch. Never invent.

| Input | Notes |
| --- | --- |
| **App name** (customer-visible), publisher/contact, logo | Shown on the consent screen in every customer tenant; changeable later |
| **Which tenant owns the registration** | An existing work/school tenant, or a new one (§2). It cannot be moved afterward |
| **Who will use the app** | Your org only / any Microsoft 365 org / also personal Microsoft accounts (§3) — the decisive input |
| **Redirect URIs** | Every callback host your platform serves (§4) |
| **Graph permission set** | The exact permission strings the connector calls — the product skill supplies these (§5) |
| **Delegated (user authorizes) or application (app-only)?** | Different permission lists, different consent story (§5) |
| **Is a tenant admin available for consent, and with which role?** | Blocking for admin-restricted permissions (§6) |
| **Secret or certificate?** | Secrets expire in ≤24 months; certificates are Microsoft's recommendation (§7) |
| **Which cloud** | Commercial, US Gov L4/L5 (DoD), or China (21Vianet) — different hosts (§7) |
| **Publisher verification status** | CPP/Partner One ID + verified publisher domain, if you have them (§8) |
| **New app or edit to an existing one?** | Editing is almost always the right answer (§1) |

## Quick Start

1. Confirm an **edit** is not enough — a new registration means a new client ID and mass re-authorization (§1).
2. Sign in to the Microsoft Entra admin center with the tenant that will own the app (§2).
3. **Entra ID → App registrations → New registration**, and choose **Supported account types** deliberately (§3).
4. **Authentication → Add Redirect URI → Web**, and add every callback host exactly (§4).
5. **API permissions → Add a permission → Microsoft Graph**; pick delegated or application, and note which need an
   admin (§5).
6. Get admin consent from an account that actually can grant it (§6).
7. **Certificates & secrets → New client secret**; copy the **Value** before you leave the page, and diary the
   expiry (§7).
8. Decide whether publisher verification is required for your customers to consent at all (§8).
9. Verify by authorizing from a **different tenant** than the one that owns the app (§9).
10. Hand the credentials over — never commit them (§10).

## Platform state (verified 2026-09-20 — re-verify before trusting)

Microsoft renames this surface constantly. Check against the linked docs before following any older runbook.

- **The portal is the Microsoft Entra admin center at `entra.microsoft.com`.** The host blocks non-browser requests,
  so it is deliberately not linked in References — reach it from the registration quickstart, which links it. The
  current registration doc gives the path as **Entra ID → App registrations → New registration**. Older docs and
  runbooks say *Identity → Applications → App registrations*, and some enterprise-app pages still use the *Identity →*
  form; both land in the same place. The Azure portal (`portal.azure.com`) still hosts the same blades. **Microsoft's
  own docs disagree about the label** — if your left-hand nav reads differently from both, stop and report what you
  see rather than guessing.
- **"Azure AD" is gone as a name**, not as a product. Entra ID, Azure AD and "the Microsoft identity platform" are the
  same thing for this task. App registration objects, client IDs and secrets are unchanged.
- **The supported-account-types labels were reworded.** The registration form now reads **Single tenant only -
  \<your tenant\>**, **Multiple Entra ID tenants**, **Any Entra ID Tenant + Personal Microsoft accounts** and
  **Personal accounts only**. Docs elsewhere still use the older long forms ("Accounts in any organizational
  directory…"), and the underlying manifest `signInAudience` values (`AzureADMyOrg`, `AzureADMultipleOrgs`,
  `AzureADandPersonalMicrosoftAccount`, `PersonalMicrosoftAccount`) have not changed. Match on meaning, not wording.
- **Client secret lifetime is capped at 24 months, with no non-expiring option**, and Microsoft recommends under 12.
  The secret **Value** is shown once and never displayed again. Microsoft's current guidance goes further: client
  secrets "should **not be used** in production environments" — use a certificate or a federated credential.
- **New app registrations are hidden from users by default** (My Apps). That is a visibility setting under Enterprise
  apps, not a functional block on OAuth; don't chase it while debugging consent.
- **Risk-based step-up consent (in effect since November 2020) blocks user consent to most multitenant apps that
  aren't publisher verified** — apps registered after 2020-11-08, requesting more than basic sign-in and profile read,
  in tenants other than the app's home tenant. For a multitenant connector this is a launch blocker, not polish. §8.
- **Endpoints are the v2.0 ones**: `https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize` and
  `.../oauth2/v2.0/token`, where `{tenant}` is `common`, `organizations`, `consumers`, or a tenant ID/domain.

If the Entra admin center or Graph does not behave like this, stop and report what you actually see.

**This skill does not click through the portal.** If the session has no browser automation — the usual case for a CLI
or cloud run — do not pretend to. Hand the user an exact ordered click path with the literal values to paste (the
redirect URIs from §4, the permission strings from the product skill), and continue once they report back with the
client ID and tenant ID.

## 1. Decide: reuse the existing registration, or create a new one

A new registration means a **new Application (client) ID**, and every existing customer consent grant is bound to the
old one. Every customer would have to re-authorize, and admin-consented tenants would need their admin again.

**Edit the existing app** for: adding a redirect URI, adding or narrowing a Graph permission, rotating an expiring or
compromised secret, adding a certificate, changing branding, or diagnosing a sign-in failure. All of these are safe,
same-day changes.

**Register a new app** only when the user explicitly wants one: a separate product, a replacement for a compromised
app, or a move to a different owning tenant — **app registrations cannot be moved between tenants**, so that is always
a new app. Say which path you are taking before you touch anything.

One thing that looks like an edit but is not: **changing supported account types from single-tenant to multitenant**
is a real change with real conditions (§3), and widening the audience to include personal Microsoft accounts can
*invalidate* an existing permission set (§3). Treat it as a project.

## 2. The tenant that owns the registration

The app object lives in exactly one Microsoft Entra tenant, forever, and is *used* by every customer tenant.

- **An existing work or school tenant** is the normal home for a company's connector. You need at least the
  **Application Developer** role to create a registration; **Cloud Application Administrator** or **Application
  Administrator** to consent on behalf of the tenant (§6 for the exception that catches people).
- **If you hold access to several tenants, switch tenants with the Settings icon before registering.** Registering in
  the wrong tenant is unrecoverable except by deleting and starting over.
- **A new tenant** — create one via the Entra fundamentals quickstart if the user has no suitable tenant. The
  registration quickstart also lists an Azure account with an active subscription as a prerequisite; a free Entra
  tenant is sufficient for registering an app, so if the user is blocked on billing, say so rather than improvising.
- **Not a personal Microsoft account.** An app registered with a personal Microsoft account **cannot be publisher
  verified** (§8). For a multitenant connector, register from a work or school tenant.
- **A Microsoft 365 developer tenant** is the right place to *test*, but not necessarily the right place to own a
  production app.
- **National clouds** (US Gov, China) are separate instances with different endpoint hosts (§7), and publisher
  verification is not supported there at all. If a customer is in one, that is a distinct app and a distinct task.

Hand control back for anything only a human can do: tenant creation, email/phone verification, MFA enrollment,
accepting terms, or granting themselves a directory role. Do not retry a blocked step in a loop.

## 3. Register the app — and choose Supported account types deliberately

**Entra ID → App registrations → New registration.** Give it the customer-visible **Name** (changeable later), pick
the account type, optionally set the first redirect URI under the **Web** platform, and press **Register**. The
**Overview** page then shows the **Application (client) ID** and **Directory (tenant) ID** — record both.

The account type is the one field on this form that is hard to walk back:

| Option (current label) | Manifest `signInAudience` | What it means for a connector |
| --- | --- | --- |
| **Single tenant only - \<your tenant\>** | `AzureADMyOrg` | Only users in *your* tenant can sign in. A customer's tenant cannot use the app at all. Correct for internal tools; wrong for a multi-customer connector. |
| **Multiple Entra ID tenants** | `AzureADMultipleOrgs` | Any Microsoft 365 / Entra organization can consent and sign in. **This is the normal choice for a SaaS connector.** |
| **Any Entra ID Tenant + Personal Microsoft accounts** | `AzureADandPersonalMicrosoftAccount` | Adds Outlook.com / Hotmail / Xbox / Live accounts. Needed only if consumer accounts are genuinely in scope — and it costs real capability (below). |
| **Personal accounts only** | `PersonalMicrosoftAccount` | Consumer accounts only. Almost never right for a business connector. |

Three consequences that are not obvious from the dropdown:

- **The permission ceiling collapses when personal accounts are included.** An app whose audience is `AzureADMyOrg` or
  `AzureADMultipleOrgs` may request **400** permissions, 400 of which may be Microsoft Graph, with roughly 155
  delegated or 300 application permissions consentable in a single request. An app that includes personal Microsoft
  accounts — `AzureADandPersonalMicrosoftAccount` or `PersonalMicrosoftAccount` — is capped at **30**, full stop. A
  broad Graph connector will hit 30. Additionally, **not every delegated Graph permission is valid for personal
  Microsoft accounts**; the Graph permissions reference marks which are.
- **Redirect URI rules tighten.** Query parameters in redirect URIs are allowed only for work/school audiences and are
  *not* allowed once personal accounts are included; the redirect-URI ceiling drops from 256 to 100; and wildcard
  redirect URIs are unsupported for that audience. §4.
- **Application permissions don't exist for consumer accounts.** App-only (application) permissions are supported for
  `AzureADMyOrg`, `AzureADMultipleOrgs` and `AzureADandPersonalMicrosoftAccount`, but not for
  `PersonalMicrosoftAccount`.

**A multitenant app must authorize against `/common` or `/organizations`**, not your own tenant ID, and your token
validation must handle `iss`/`tid` coming back as the *customer's* tenant. A single-tenant registration called through
`/common` fails with **`AADSTS50194`**.

**If you are converting an existing single-tenant app to multitenant** (Authentication → Supported account types →
accounts in any organizational directory): the app's **Application ID URI must be globally unique** across all tenants
— e.g. `https://<yourtenant>.onmicrosoft.com/<app>` — or the change fails. While you are in the manifest, consider
setting an **app instance property lock**, which stops administrators in customer tenants from adding credentials to
your app's service principal there.

## 4. Redirect URIs

**Authentication → Add a platform → Web.** The platform type is not cosmetic:

| Platform | Client type | Consequence |
| --- | --- | --- |
| **Web** | Confidential | The only kind that may present a `client_secret` at the token endpoint. **This one**, for a hosted connector. |
| Single-page application (`spa`) | Public | Cannot carry a client secret; forces PKCE; client-credentials calls are refused. Surfaces later as a cross-origin / `invalid_request` failure, not at save time. |
| Mobile and desktop | Public | Loopback and custom-scheme redirects for native apps; no secret. |

Register **every** callback host your platform serves. For Unified.to these are one per data center; confirm the
current list with the platform owner rather than assuming:

```
https://api.unified.to/oauth/code          # us (default)
https://api-eu.unified.to/oauth/code       # eu
https://api-au.unified.to/oauth/code       # au
https://api-dev.unified.to/oauth/code      # dev
```

The rules Entra actually enforces — all of them failing at connect time, not at save time:

- **Exact match, and case-sensitive on the path.** `/oauth/Code` is not `/oauth/code`. A mismatch returns
  **`AADSTS50011: InvalidReplyTo — The reply address is missing, misconfigured, or doesn't match reply addresses
  configured for the app`**.
- A URI with **no path segment** comes back with a trailing slash (`https://contoso.com` → `https://contoso.com/`);
  one with a path does not.
- **`https` only**, except `http://localhost` (and `127.0.0.1`, which the portal text box refuses for `http` — it has
  to go in via the manifest). Prefer `127.0.0.1` over `localhost` for local testing.
- **Maximum 256 redirect URIs** for work/school audiences, **100** once personal Microsoft accounts are included;
  **256 characters** per URI. The limits cannot be raised.
- **No wildcards** for audiences that include personal accounts, and Microsoft's own guidance is not to use them
  anywhere — a wildcard match strips query strings and fragments. Use the `state` parameter to carry per-tenant or
  per-subdomain routing instead of multiplying URIs.
- **No `! $ ' ( ) , ;`**, no internationalized domain names.
- **Add redirect URIs to the application object only**, never to a service principal — service-principal values get
  wiped on sync.
- Trim URIs you no longer own. A lapsed DNS record on a registered redirect URI is an app compromise.

## 5. Permissions: delegated vs application

**API permissions → Add a permission → Microsoft Graph**, then choose the permission kind. The difference is not
cosmetic:

| | Delegated permissions | Application permissions |
| --- | --- | --- |
| Acts as | The signed-in user | The app itself, no user present |
| Ceiling | Never more than the user can already do | Everything the permission covers, tenant-wide |
| Who can consent | The user, *or* an admin for everyone | **Admin only, always** — and see §6 for which admin |
| Consent style | Static (registered list) or dynamic (per request) | Static only; requested as `.default` |
| Use for | A customer-authorizes-us connector | Daemons and background sync with no user |

Prefer **delegated**. Microsoft's own guidance is to use delegated access whenever it meets the requirement, and
customer security reviewers treat application permissions as a much bigger ask.

**Scope strings.** Graph permissions are named `{resource}.{operation}.{constraint}` — `User.Read`, `Mail.ReadBasic`,
`Calendars.ReadWrite`, `Files.Read.All`, `Directory.Read.All`, `Sites.Read.All`. In the authorize request they are
space-delimited, and may be bare or resource-qualified: `scope=User.Read` is equivalent to
`scope=https://graph.microsoft.com/User.Read`. App-only calls use the literal
**`https://graph.microsoft.com/.default`** (adjusted to the cloud's Graph host) rather than individual names, and
**mixing `/.default` with individual scopes in one request is an error**.

**A trap worth stating plainly:** any scope present in the token is honored. Requesting a narrow permission
*alongside* a broad one does not produce a least-privilege app — it produces a broad app with a least-privilege
label, and a customer security review will find it. Narrow means the broad scopes come out of the request.

**The four OIDC scopes**, which are not Graph data permissions:

| Scope | Effect |
| --- | --- |
| `openid` | Required for sign-in; yields the `sub` claim and the ID token ("Sign you in" on the consent screen) |
| `profile` | Name, preferred username, object ID |
| `email` | The `email` claim — **may be absent** even when requested, if the account has no email address |
| `offline_access` | **Must be requested explicitly to receive a refresh token** ("Maintain access to data you have given it access to") |

`address` and `phone` are not supported at all.

### `offline_access` and refresh tokens

`offline_access` is the single most common cause of "the connection dies after an hour". The v2.0 token response
returns `refresh_token` *only if* `offline_access` was in the authorize request. Access tokens last about an hour;
refresh tokens are typically valid for 90 days.

Microsoft's own scopes page comes close to contradicting itself here, so read it carefully rather than quoting it:
it says that "if any delegated permission is granted, `offline_access` is implicitly granted" and that you may assume
the app has it — and then, a few lines later, that on the Microsoft identity platform "your app must **explicitly
request** the `offline_access` scope to receive refresh tokens." It also notes that the permission appears on *all*
consent pages, including flows such as the **implicit** grant that never return a refresh token at all, to support a
client that starts in the implicit flow and moves to the code flow. The operative rule is the strict one: **request
`offline_access` explicitly and confirm a `refresh_token` actually came back** (§9). Do not infer it from a granted
delegated permission or from what the consent screen displayed.

## 6. Admin consent

Two independent questions decide whether this step succeeds: whether an admin is needed, and whether the admin in the
room is the right kind.

**Whether an admin is needed.** Two reasons, and neither is fixed by re-registering:

1. **The permission is admin-restricted.** Every application permission, and many higher-privilege delegated ones
   (`User.Read.All`, `Group.Read.All`, `Directory.ReadWrite.All`, …), require an administrator. The Graph permissions
   reference marks these. An ordinary user who tries gets an error, not a prompt — **`AADSTS90094:
   AdminConsentRequired`**. That is the expected behavior, not a bug in your authorize URL.
2. **The customer's tenant disabled user consent**, or restricted it to verified publishers and low-impact
   permissions (**Entra ID → Enterprise apps → Consent and permissions → User consent settings**, policies
   `microsoft-user-default-legacy` vs `microsoft-user-default-low`). Nothing about your permission set changes this;
   it is their policy. Tenants can enable an **admin consent request workflow** so users can ask.

**Which admin can grant it.** This is the trap that stalls runs:

| Role | Can grant |
| --- | --- |
| **Privileged Role Administrator** | Any permission for any API — **including Microsoft Graph application permissions** |
| Cloud Application Administrator / Application Administrator | Any permission for any API **except Microsoft Graph app roles (application permissions)** |
| Application Developer | Register apps — **not** grant consent |

So an app-only Graph connector cannot be consented by an Application Administrator. That restriction is **specific to Microsoft Graph application permissions**. Application permissions on other Microsoft APIs — Dataverse, Business Central — are consentable by a Cloud Application Administrator, so telling a customer they need a Privileged Role Administrator for those costs a scheduling round trip they did not need. Say so up front; it saves a
scheduling round trip with the customer.

**How to grant it.** In your own tenant: App registrations → your app → **API permissions** → **Grant admin consent
for `<tenant>`**, then confirm every permission shows **Granted for `<tenant>`**. In a customer tenant, give the
admin a URL rather than instructions — either form works:

```
https://login.microsoftonline.com/{organization}/adminconsent?client_id={client-id}
```

```
https://login.microsoftonline.com/{tenant}/v2.0/adminconsent
    ?client_id={client-id}
    &scope=https://graph.microsoft.com/Calendars.Read https://graph.microsoft.com/Mail.Send
    &redirect_uri={a redirect URI registered on the app}
    &state={your state}
```

`{tenant}` / `{organization}` must be a tenant ID, a verified domain, or the literal `organizations` — **never
`common`**, because a personal account cannot grant admin consent. The `redirect_uri` must already be registered (§4).
Success comes back as `admin_consent=True&tenant={guid}&scope=…`; never authenticate or authorize off that returned
`tenant` value.

Four more things worth knowing before you pick permissions:

- Consenting creates a **service principal** for your app in the customer's tenant and pre-consents every user; after
  that, no user in that tenant sees a consent prompt. Until that service principal exists, nothing customer-side can
  reference your app at all.
- An admin who signs in **without** `prompt=consent` consents only for their own account — other users still cannot
  sign in. If your flow depends on an admin unlocking the tenant, send `prompt=consent` or the admin-consent URL.
- Granting tenant-wide admin consent **may revoke permissions already granted tenant-wide** for that app. Re-consent
  after a permission change is not purely additive, so do not re-consent a live production app casually.
- A tenant admin can disable user consent entirely. In such a tenant, admin consent is required even for permissions
  that would normally be user-consentable.

## 7. Client secrets, certificates, and capturing the credentials

**Certificates & secrets → Client secrets → New client secret.** Add a description, choose an expiry, **Add**, then
copy the **Value** column immediately.

**The expiry is the part people get wrong.** Entra caps a client secret at **24 months** and offers no never-expires
option; Microsoft recommends under 12 months. When it lapses, every token exchange and refresh fails with
**`AADSTS7000222: InvalidClientSecretExpiredKeysProvided`** — for every customer at once, with no warning in the
product. So:

- Put the exact expiry date in a shared calendar and in the credential-store entry, the day you create the secret.
- The **Value** is shown once and **never displayed again**. Copy the *Value*, not the *Secret ID* — the Secret ID is
  not the secret and is useless for authentication, and mistaking one for the other produces
  **`AADSTS7000215: Invalid client secret is provided`**.
- **Rotate by adding a second secret before the first expires** — an app can hold more than one — deploying the new
  value, confirming every environment exchanges and refreshes, then deleting the old. Never rotate by deleting first:
  issued access tokens live out their lifetime, but every exchange and refresh fails until the new secret is live.
- **Microsoft's position is that client secrets should not be used in production.** Certificates are the supported
  alternative and the recommendation: upload a `.cer`/`.pem`/`.crt`, record the thumbprint, and exchange a signed
  `client_assertion` in place of `client_secret`, with
  `client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer`. Certificates expire too — on *your*
  schedule, and rotatable without a portal visit. Federated credentials remove the secret entirely for workloads on
  GitHub Actions, Kubernetes or Azure. If the platform only supports a secret, note that as a known gap rather than
  arguing.

Capture:

- **Application (client) ID** — the OAuth `client_id`, on the Overview page, readable any time
- **Directory (tenant) ID** — the owning tenant; needed for tenant-scoped authority URLs and for admin consent
- **Client secret Value**, delivered to the user for their secret store, with its **exact expiry date**
- The exact permission strings, and whether they are delegated or application
- **The authority and Graph host for the target cloud:**

  | Cloud | Authority | Graph host |
  | --- | --- | --- |
  | Commercial (global) | `https://login.microsoftonline.com` | `https://graph.microsoft.com` |
  | US Government L4 | `https://login.microsoftonline.us` | `https://graph.microsoft.us` |
  | US Government L5 (DoD) | `https://login.microsoftonline.us` | `https://dod-graph.microsoft.us` |
  | China (21Vianet) | `https://login.chinacloudapi.cn` | `https://microsoftgraph.chinacloudapi.cn` |

  Endpoints are `/{tenant}/oauth2/v2.0/authorize` and `/{tenant}/oauth2/v2.0/token`, with `{tenant}` = `common`
  (org + personal), `organizations` (org only), `consumers`, or a tenant ID/domain.

Report the secret once so the user can paste it into their secret store, say plainly that it is now in the transcript
and can be rotated, then move on.

## 8. Publisher verification, and whether customers can consent at all

For a **multitenant** app this is not branding — it can be the difference between a connect button that works and one
that dead-ends.

Since November 2020, **risk-based step-up consent blocks ordinary users from consenting to most multitenant apps that
are not publisher verified**: apps registered after 2020-11-08, requesting anything beyond basic sign-in and profile
read, in a tenant other than the app's home tenant. The user sees a warning that the app was created by an unverified
publisher. Separately, tenants that apply the `microsoft-user-default-low` policy allow user consent *only* for
verified-publisher apps. Both push every customer into the admin-consent path (§6).

Verification is free and fast once the prerequisites exist — the prerequisites are the slow part:

- A verified **Microsoft AI Cloud Partner Program** (CPP, formerly MPN) account with a **Partner One ID**, which must
  be the organization's **partner global account** (a location Partner One ID will not work).
- The app must be **registered in a work or school tenant** — apps registered with a personal Microsoft account can
  never be verified — and the tenant must be associated with the partner global account.
- The app must have a **publisher domain** set, and it **cannot be `*.onmicrosoft.com`**. The email domain used for
  CPP verification must match it, or be a DNS-verified custom domain on the tenant.
- The person doing it needs **Application Administrator or Cloud Application Administrator** in Entra *and* CPP
  Partner Admin or Account Admin in Partner Center, and must sign in with MFA.
- Not supported in national clouds.

Whether to join the partner program, and under which legal entity, is a business decision. Surface the requirement and
the blockers; do not sign anything up.

## 9. Verify end-to-end

Authorizing in the tenant that owns the app proves very little — that tenant already trusts it. Test the path a
customer takes:

1. Authorize from a **different tenant** through your platform's real connect flow, with a **non-admin** user if
   customers will use one. That is the run that reveals admin-consent requirements.
2. Confirm the token response carries a `refresh_token` — if it does not, `offline_access` was not requested (§5).
3. Force a refresh, then make one real Graph call with the refreshed token
   (`GET https://graph.microsoft.com/v1.0/me` is the cheapest).
4. Decode the access token's `scp` (delegated) or `roles` (application) claim and check it against what you intended
   to request. This is the fastest way to catch a permission that was declared in the portal but never sent, or
   consented but never granted.
5. Repeat with an admin user, after admin consent, and confirm ordinary users then connect without a prompt.
6. If personal Microsoft accounts are in scope, test one — permission validity and redirect rules differ (§3).

| Symptom | Cause |
| --- | --- |
| `AADSTS50011` reply address mismatch | Redirect URI not registered, or case/trailing-slash mismatch (§4) |
| `AADSTS50194` — app "isn't configured as a multitenant application", `/common` unsupported | Single-tenant registration being called through `/common` (§3) |
| `AADSTS700016` — application not found in the directory/tenant | Wrong client ID, or no service principal in that tenant yet — not consented/installed (§3, §6) |
| `AADSTS500011` — resource principal not found in tenant | The resource isn't provisioned in the customer tenant; needs admin consent, or the service isn't licensed there |
| `AADSTS65001` — user or administrator hasn't consented | No consent grant yet; send an interactive authorize, or the admin-consent URL (§6) |
| `AADSTS90094` — administrator consent is required | An admin-restricted permission, or the tenant disabled user consent (§6) |
| Admin clicks "Grant admin consent" and it fails | Application Administrator cannot consent Graph **application** permissions — needs Privileged Role Administrator (§6) |
| `AADSTS65004` — user declined consent | The consent screen scared them; check whether you're over-requesting or unverified (§5, §8) |
| `AADSTS7000215` — invalid client secret provided | Wrong secret, or the Secret **ID** was used instead of the **Value** (§7) |
| `AADSTS7000222` — provided client secret keys are expired | The 24-month cap arrived, and every customer broke on the same day (§7) |
| `AADSTS70011` / `invalid_scope` | A permission string that doesn't exist, isn't valid for that account type, or `/.default` mixed with individual scopes (§5) |
| `AADSTS50020` — user account from identity provider doesn't exist in tenant | Guest/B2B user signing into the wrong tenant |
| Auth succeeds, no `refresh_token` in the response | `offline_access` not requested (§5) |
| Consent screen warns "unverified publisher", users can't proceed | Risk-based step-up consent on a non-verified multitenant app (§8) |
| Token request rejected for cross-origin / client credentials in browser | Redirect URI registered as `spa` instead of `Web` (§4) |

Error bodies carry `error_codes`, `trace_id`, `correlation_id` and a timestamp. Give the customer's admin the
correlation ID — it is what Microsoft support and their own sign-in logs key on.

## 10. Hand off — never commit the secret

- **Do not** write the client secret into source control, a test, a fixture, a committed `.env`, a ticket, a PR body
  or a chat channel. Values go to the user, for their secret store or console.
- The client ID and tenant ID are not secrets, but treat them as configuration, not constants in code.
- If a code change is needed (a callback host, a scope, `offline_access`, a tenant-scoped authority), keep it
  secret-free and say what the human must set out of band.
- Close with: app name and Application (client) ID; owning tenant (Directory ID), its domain and which cloud;
  supported account types as chosen; every redirect URI registered and under which platform; the exact permission
  strings, whether they are delegated or application, and which require admin consent; whether admin consent was
  granted and by which role; where the secret was delivered and **its exact expiry date**; the authorize and token
  endpoints with the `{tenant}` value in use; publisher verification status; and anything left for the user or the
  customer's admin to do.

## Stop and ask

Hand back to a human rather than guessing when:

- The account type would have to change on a live app, or the app would have to move tenants (both re-authorize every
  customer).
- A required Graph permission is admin-restricted and no tenant admin is available for testing — report it; do not
  substitute a lower permission and hope.
- Consent is blocked because the available admin is not a Privileged Role Administrator.
- An application (app-only) permission is being proposed where delegated would do; that is a security decision the
  customer's reviewers will weigh.
- Publisher verification is blocking consent and the organization is not in the partner program — that is a business
  and legal commitment.
- The user asks you to rotate or delete a secret on a live app without a cutover plan.
- Anyone proposes a wildcard redirect URI, `http` outside localhost, or a permission requested "just in case".
- Privacy/terms URLs or a security questionnaire requires attesting to something you cannot verify.
- A national cloud, an Entra External ID / B2C tenant, or SAML is involved — different endpoints, different rules, a
  different task.
- The portal does not match the **Platform state** section above.

**Not every Microsoft API has a fixed resource host.** Graph is always `https://graph.microsoft.com`, but Dataverse
scopes embed the customer's own environment host and Business Central has its own resource — so for those products the
scope string is discovered per customer rather than hardcoded. The product skill says which applies.

## References

Verified 2026-09-20; every link returned HTTP 200.

- Register an app in Microsoft Entra ID — https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app
- Create a new Microsoft Entra tenant — https://learn.microsoft.com/en-us/entra/fundamentals/create-new-tenant
- Single-tenant and multitenant apps — https://learn.microsoft.com/en-us/entra/identity-platform/single-and-multi-tenant-apps
- Convert a single-tenant app to multitenant — https://learn.microsoft.com/en-us/entra/identity-platform/howto-convert-app-to-be-multi-tenant
- Application and service principal objects — https://learn.microsoft.com/en-us/entra/identity-platform/app-objects-and-service-principals
- App types and authentication flows — https://learn.microsoft.com/en-us/entra/identity-platform/v2-app-types
- Add a redirect URI — https://learn.microsoft.com/en-us/entra/identity-platform/how-to-add-redirect-uri
- Redirect URI restrictions and limitations — https://learn.microsoft.com/en-us/entra/identity-platform/reply-url
- Permissions and consent overview — https://learn.microsoft.com/en-us/entra/identity-platform/permissions-consent-overview
- Scopes and permissions (`openid`, `offline_access`, `.default`) — https://learn.microsoft.com/en-us/entra/identity-platform/scopes-oidc
- Microsoft Graph permissions overview (naming, per-audience limits) — https://learn.microsoft.com/en-us/graph/permissions-overview
- Microsoft Graph permissions reference — https://learn.microsoft.com/en-us/graph/permissions-reference
- Microsoft Graph auth concepts — https://learn.microsoft.com/en-us/graph/auth/auth-concepts
- Get access without a signed-in user (app-only / `.default`) — https://learn.microsoft.com/en-us/graph/auth-v2-service
- OAuth 2.0 authorization code flow (v2.0 endpoints) — https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow
- OAuth 2.0 client credentials flow — https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow
- Protocols and endpoints — https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols
- Admin consent protocol — https://learn.microsoft.com/en-us/entra/identity-platform/v2-admin-consent
- Grant tenant-wide admin consent — https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/grant-admin-consent
- Configure how users consent to applications — https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/configure-user-consent
- Admin consent request workflow — https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/configure-admin-consent-workflow
- Review/revoke permissions granted to enterprise apps — https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/manage-application-permissions
- Risk-based step-up consent — https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/configure-risk-based-step-up-consent
- Microsoft Entra role permissions reference — https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/permissions-reference
- Add and manage app credentials (secrets, certificates, federated) — https://learn.microsoft.com/en-us/entra/identity-platform/how-to-add-credentials
- Certificate credentials — https://learn.microsoft.com/en-us/entra/identity-platform/certificate-credentials
- Security best practices for app registration — https://learn.microsoft.com/en-us/entra/identity-platform/security-best-practices-for-app-registration
- Publisher verification overview — https://learn.microsoft.com/en-us/entra/identity-platform/publisher-verification-overview
- Mark an app as publisher verified — https://learn.microsoft.com/en-us/entra/identity-platform/mark-app-as-publisher-verified
- Configure an app's publisher domain — https://learn.microsoft.com/en-us/entra/identity-platform/howto-configure-publisher-domain
- National cloud endpoints — https://learn.microsoft.com/en-us/entra/identity-platform/authentication-national-cloud
- AADSTS error code reference — https://learn.microsoft.com/en-us/entra/identity-platform/reference-error-codes
