You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Adds GraphMissingPermissionsGuidancePlugin, a guidance plugin that explains Microsoft Graph 403 Forbidden responses caused by missing permissions.
How it works
Runs in AfterResponseAsync for watched Microsoft Graph requests that return 403, and for 403 responses inside $batch responses.
Reads delegated scopes (scp) or application roles (roles) from the bearer token on the request.
Retrieves the permissions that allow calling the endpoint from the Microsoft Graph DevX permissions API (the same service used by the existing GraphMinimalPermissions* plugins). Results are cached in memory per scope type + method + tokenized URL. Lookups time out after 3s; batch lookups run in parallel, and concurrent lookups for the same key are coalesced (failed lookups are retried later).
Skips responses generated by plugins (mocks).
Logs per request:
Failed when the token has none of the accepted permissions: lists the token's permissions, the least-privileged permission to add and higher-privileged alternatives (or unranked alternatives when DevX doesn't mark a least-privileged one), and instructions for delegated (app registration + requested scopes + consent) vs. application (app registration + admin consent + new token) permissions.
Warning when the token already has an accepted permission (403 is likely caused by something else: resource access, license/role, policy), when the token can't be read, or when the required permissions can't be determined.
For delegated /users/{id} requests that don't target the signed-in user (compared with the token's oid/upn/preferred_username), self-only permissions (User.Read, User.ReadWrite) aren't treated as sufficient, and the message explains they only cover the signed-in user (use /me).
Delegated tokens for personal Microsoft accounts (tid = consumers tenant) use DelegatedPersonal permissions.
For delegated requests to another user's mailbox, calendar or contacts, the guidance notes that the data must be shared and the app needs the matching .Shared permission (or application permissions).
When the URL can't be parsed (e.g. parenthesized key syntax /users('id')), logs a tip that the analysis couldn't run. Fixing GetTokenizedUrl for that syntax is tracked separately.
The access token is decoded only when there's a 403 to analyze.
No configuration.
Example:
fail ╰ GraphMissingPermissionsGuidancePlugin: Request failed with 403 Forbidden because the access token doesn't have the permissions required to call GET /me/messages. Delegated permissions on the token: openid, User.Read. Add the least privileged permission: Mail.ReadBasic. Alternatively, use one of the following higher-privileged permissions: Mail.Read, Mail.ReadWrite. To fix it, add the permission to the delegated permissions of your app registration in Microsoft Entra ID, include it in the scopes your app requests when acquiring the access token, and make sure the user (or an administrator, for permissions that require admin consent) consents to it.
Other changes
Extracted JWT permission parsing into GraphUtils.GetPermissionsFromAuthorizationHeader and reused it in GraphMinimalPermissionsGuidancePlugin (no behavior change).
Exposed the existing RespondedByPlugin flag on IProxySession so the plugin skips mocked responses.
ProxyUtils.IsGraphUrl now recognizes dod-graph.microsoft.us (fixes DoD-cloud detection for all Graph plugins).
Enabled the plugin in the microsoft-graph.json and m365.json presets.
Added the plugin to the skill reference docs.
Added hermetic tests (stubbed DevX API) covering delegated/application tokens, sufficient permissions, missing token, unknown endpoints, caching, lookup coalescing and retry, self-only permissions, unranked permissions, personal accounts, other-user sharing note, unparsable URLs, mocked responses, DoD host, non-403 responses and $batch.
Adds a guidance plugin that inspects Microsoft Graph responses with
403 Forbidden (including 403s inside $batch responses), compares the
permissions on the access token with the permissions required for the
endpoint (retrieved from the Microsoft Graph DevX permissions API and
cached in memory), and logs which permission to add and how.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
The awaited DevX lookup can delay Graph response delivery and cause client timeouts, while DoD-cloud requests receive no guidance and mocked 403 responses can trigger misleading diagnoses.
Adds GraphMissingPermissionsGuidancePlugin to help explain Microsoft Graph 403 responses and recommend permission and consent changes, addressing issue #88.
Changes:
Adds cached permission lookups and guidance for individual requests and failed batch requests.
Shares JWT permission parsing with the existing minimal-permissions guidance plugin.
Enables the plugin in Graph and Microsoft 365 presets, documents it, and adds tests with a stubbed permissions API.
File
Description
skills/dev-proxy/references/plugin-catalog.md
Lists the new guidance plugin.
skills/dev-proxy/references/configuration.md
Documents the plugin in the configuration reference.
DevProxy/config/microsoft-graph.json
Enables the plugin in the Graph preset.
DevProxy/config/m365.json
Enables the plugin for Graph URLs in the Microsoft 365 preset.
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
Parallel analysis can trigger duplicate DevX requests for the same uncached key, requiring in-flight lookup coalescing that preserves retries and isolates caller cancellation.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Permission guidance can incorrectly rank unmarked permissions and treat self-only User.Read as sufficient for another user’s profile, producing misleading recommendations.
No open issues 🎉
2 previously missed issues
In code that hasn't changed since last review
Recommend User.ReadBasic.All for delegated access to other users
For a delegated GET /users/{other-user-id}, DevX includes User.Read, but that permission only grants access to the signed-in user's profile. A token containing only User.Read therefore triggers the “another reason” warning instead of guidance to add User.ReadBasic.All. Account for self-only permissions in both the sufficiency check and the least-privilege recommendation, preserving them when the request targets the signed-in user.
Do not rank permissions when least-privilege metadata is missing
When DevX returns permissions without a least-privilege marker, this promotes the first result to “least privileged” without establishing its ranking. For example, a response ordered as User.Read.All, User.ReadBasic.All with both flags false recommends the broader permission and labels the narrower one as higher-privileged. Report the accepted permissions as unranked alternatives when the metadata cannot identify the least-privileged choice.
Treat self-only delegated permissions (User.Read, User.ReadWrite) as
insufficient for /users/{id} requests and list permissions as unranked
alternatives when none is marked least privileged.
Refs dotnet#88
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Addressed the 2 previously missed issues from the latest Copilot review in c5317ce:
Self-only delegated permissions for other users: for delegated /users/... requests, User.Read/User.ReadWrite no longer count as sufficient and aren't recommended. A User.Read-only token calling /users/{id} now gets guidance to add User.ReadBasic.All, plus a note that the self-only permissions only allow access to the signed-in user and that /me works for that case. /me requests are unaffected.
Missing least-privilege metadata: if DevX doesn't mark any permission as least privileged, the plugin now lists the accepted permissions as unranked alternatives ("Add any of the following permissions: …") instead of promoting the first one.
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
Unconditionally excluding self-only permissions for /users/{id} can misdiagnose requests targeting the signed-in user and recommend unnecessary directory-wide permissions, requiring identity-aware guidance and coverage.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Human assessment is needed because valid parenthesized Graph URLs can trigger a parsing failure that silently suppresses permissions guidance for watched 403 responses.
Addressed the note in the latest Copilot review summary (parenthesized Graph URLs such as /users('id') make GraphUtils.GetTokenizedUrl fail, which silently skips guidance) in 9b45753:
When tokenizing the URL fails, for a single request or a $batch inner request, the plugin now logs a visible tip saying it couldn't analyze missing permissions for that URL and pointing to the Graph API reference, instead of only writing a debug message. Added tests for single and batch requests.
Out of scope: fixing GetTokenizedUrl itself to support parenthesized key syntax. It's shared with the GraphMinimalPermissions* plugins and will be tracked in a separate follow-up issue.
Re: the latest Copilot review summary (unpublished findings):
Unnecessary JWT processing: fixed in da0ed08. The access token is now decoded only when there's a 403 to analyze (a direct 403 or a batch with forbidden inner responses), and user identity claims are read only for delegated tokens. Before, every watched Graph response decoded the token twice.
Self-only permission guidance: the summary doesn't give specifics, so I requested another review to get the details.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Human assessment is needed because an unpublished finding indicates personal-account requests may receive unusable work/school permission recommendations.
Re: the latest Copilot review summary (an unpublished finding that personal-account requests may get unusable work/school recommendations). Fixed in aa6d0b0: delegated tokens issued to personal Microsoft accounts (tid = consumers tenant 9188040d-6c67-4c5b-b112-36a304b66dad) now look up scopeType=DelegatedPersonal permissions from the DevX API instead of DelegatedWork. Added a test.
Re: the latest Copilot review summary (an unpublished finding about misleading guidance for access to another user). Addressed in 64ff737: for delegated /users/{id}/... requests that don't target the signed-in user (or when the identity is unknown), the DevX API lists permissions like Mail.Read/Calendars.Read, but these only cover data the signed-in user can access. The guidance (both the missing-permission failure and the 'another reason' warning) now adds a note: to access another user's data, that user must share it with the signed-in user (using .Shared permissions where available), or the app must use application permissions. Added tests for the other-user and signed-in-user cases.
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
The guidance incorrectly presents sharing as a prerequisite for all nested /users/{id} resources and must limit that advice to mailbox/calendar access.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Human assessment is needed because the permission guard can incorrectly treat self-only delegated permissions as sufficient for another user’s request, producing misleading guidance.
Re: the latest Copilot review summary (an unpublished finding that self-only delegated permissions may be treated as sufficient for another user's request). Not changing this. For other-user mailbox, calendar and contacts requests, a token that has e.g. Mail.Read gets the 'likely failed for another reason' warning, and that warning now includes the note that the other user must share the data and the app needs the matching .Shared permission (or application permissions). That covers this case. For /users/{id} profile requests, User.Read/User.ReadWrite are already excluded unless the ID matches the signed-in user. This theme has come up repeatedly in recent rounds, so I'm treating it as addressed.
This branch has not been deployed
No deployments
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #88
Adds
GraphMissingPermissionsGuidancePlugin, a guidance plugin that explains Microsoft Graph403 Forbiddenresponses caused by missing permissions.How it works
AfterResponseAsyncfor watched Microsoft Graph requests that return403, and for403responses inside$batchresponses.scp) or application roles (roles) from the bearer token on the request.GraphMinimalPermissions*plugins). Results are cached in memory per scope type + method + tokenized URL. Lookups time out after 3s; batch lookups run in parallel, and concurrent lookups for the same key are coalesced (failed lookups are retried later)./users/{id}requests that don't target the signed-in user (compared with the token'soid/upn/preferred_username), self-only permissions (User.Read,User.ReadWrite) aren't treated as sufficient, and the message explains they only cover the signed-in user (use/me).tid= consumers tenant) useDelegatedPersonalpermissions..Sharedpermission (or application permissions)./users('id')), logs a tip that the analysis couldn't run. FixingGetTokenizedUrlfor that syntax is tracked separately.Example:
Other changes
GraphUtils.GetPermissionsFromAuthorizationHeaderand reused it inGraphMinimalPermissionsGuidancePlugin(no behavior change).RespondedByPluginflag onIProxySessionso the plugin skips mocked responses.ProxyUtils.IsGraphUrlnow recognizesdod-graph.microsoft.us(fixes DoD-cloud detection for all Graph plugins).microsoft-graph.jsonandm365.jsonpresets.$batch.