---
name: microsoft-oauth-app
description: The Microsoft Graph **sign-in** layer on top of the shared `microsoft-entra-app-registration` skill — the tenant-agnostic `common` authority, the `openid email profile User.Read` scope set, `prompt=select_account`, and why an authentication connector receives no refresh token. Read the base skill first. Use this when the job is Microsoft sign-in / identity, or a general Graph connector with no specific product named; for a product use its own skill — `microsoft-outlook-oauth-app` (mail and calendar), `microsoft-onedrive-oauth-app` (files), `microsoft-sharepoint-oauth-app` (sites and libraries), `microsoft-teams-oauth-app`, `microsoft-entra-directory-oauth-app` (users and groups) or `microsoft-dynamics-oauth-app`. For any other vendor's developer portal, use that vendor's skill instead.
---

# Microsoft Graph OAuth2 App Registration

Get a working Microsoft OAuth2 client — an app registration, a Web redirect URI, Microsoft Graph permissions, a
client ID and a client secret — for a platform that connects *other organizations'* Microsoft 365 tenants on behalf
of many customers.

Every Microsoft Graph–backed connector registers identically. Outlook mail and calendar, OneDrive, Teams, Entra
directory, Excel, To Do: same portal, same form, same consent model. Only the permission strings differ, and the
product decides those, not the portal. That is why almost all of this job lives in the base skill.

## Built on: `microsoft-entra-app-registration`

**Read the base skill first.** It is the source of truth for the Entra mechanics this one assumes, and it carries the
facts that cost a launch when they are wrong:

- **Supported account types** — single tenant vs multitenant vs personal Microsoft accounts, the 30-permission
  ceiling once personal accounts are in scope, and `AADSTS50194` when a single-tenant app is called through `/common`.
- **Redirect URIs** — the Web vs SPA vs public-client platform choice (only **Web** may present a client secret),
  exact case-sensitive matching, the count and length limits, and the Unified.to callback hosts.
- **Delegated vs application permissions, and admin consent** — which permissions need an admin, and the fact that
  Microsoft Graph *application* permissions need a **Privileged Role Administrator**, not an Application
  Administrator.
- **Client secrets** — the 24-month cap with no never-expires option, the **Value** shown exactly once,
  `AADSTS7000222` on expiry, add-then-delete rotation, and certificates as Microsoft's preferred alternative.
- **`offline_access`, publisher verification and the generic AADSTS symptom table** — including risk-based step-up
  consent, which blocks ordinary users from consenting to a non-verified multitenant app.

The base also holds the inputs to collect, the run order, the tenant-ownership rules, the credential hand-off, the
stop-and-ask conditions and the doc references. Everything below is what Graph sign-in adds on top.

## This is a sign-in connector, not a data connector

The distinction matters more than it sounds. A Graph **data** connector authorizes a user in order to read their mail,
files or calendar for as long as the connection lives, so it must request `offline_access` and hold a refresh token.
A Graph **authentication** connector authorizes a user only to establish who they are — it reads the signed-in user
once, at connect time, and never needs to call Graph again on that user's behalf.

Unified.to's Microsoft connector is the second kind. That is why the scope set below carries no `offline_access` and
why the absence of a refresh token on this path is the designed behavior rather than the bug it looks like. If the
task is actually a data connector — mail, files, calendar, directory sync — it needs `offline_access` and a
different permission set, and that is a product decision to confirm with the connector's owner, not an assumption to
make here.

## Product fact — as of 2026-09-20

Unified.to's Microsoft connector authorizes against the tenant-agnostic `common` authority
(`https://login.microsoftonline.com/common/oauth2/v2.0/authorize` and the matching v2.0 `/token`), with Microsoft
Graph at `https://graph.microsoft.com`, and requests the space-delimited sign-in scope set
**`openid email profile User.Read`**. It always sends `prompt=select_account` on the authorize request, so users get
the account picker instead of silent SSO. Token exchange is a form-encoded POST carrying `client_id` and
`client_secret` in the **body** (not HTTP Basic), and it sends `redirect_uri` on refresh as well as on exchange. It
identifies the user by reading the signed-in user from Graph and **requires a user principal name to be present**.

Two consequences to raise before you register anything:

- The `common` authority means the registration **must** be multitenant. A single-tenant registration called through
  `/common` fails for every customer with `AADSTS50194` (base skill, §3).
- **`offline_access` is not in that sign-in scope set**, so this connector's Microsoft sign-in path receives no
  refresh token. Other Microsoft Graph connectors on the same platform do request it.

Confirm all of this with the connector's owner before choosing permissions; **do not infer either answer**. Scope
lists change and this one is a snapshot.

## Symptom → cause, specific to this connector

The base skill's table covers the generic AADSTS failures. These are the ones that only make sense here:

| Symptom | Cause |
| --- | --- |
| Sign-in succeeds but no `refresh_token` is stored, on *this* connector | Expected — `offline_access` is not in the `openid email profile User.Read` set. Do not "fix" it by adding the scope without the owner's sign-off |
| Every customer tenant fails at authorize with `AADSTS50194` | The registration is single-tenant while the connector calls `/common`. Fix the registration, not the authority |
| Token exchange returns `invalid_client` although the secret is right | Credentials sent as HTTP Basic; this connector puts `client_id`/`client_secret` in the form body |
| Refresh fails although exchange worked | `redirect_uri` omitted on the refresh request — this connector sends it on both |
| Connect completes, then the connection is rejected for a missing user | The signed-in account has no user principal name; a Graph identity read cannot identify it |
| Users are silently signed in as the wrong account | `prompt=select_account` was dropped; without it the browser's existing Microsoft session is reused |

## References

The base skill carries the full reference list. These back the sign-in specifics above:

- OAuth 2.0 authorization code flow (v2.0 endpoints, `prompt`, form-encoded exchange) — https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow
- Scopes and permissions (`openid`, `email`, `profile`, `offline_access`) — https://learn.microsoft.com/en-us/entra/identity-platform/scopes-oidc
- Get a user (`/me`, `userPrincipalName`) — https://learn.microsoft.com/en-us/graph/api/user-get
- Microsoft Graph permissions reference (`User.Read`) — https://learn.microsoft.com/en-us/graph/permissions-reference
