Skip to content

feat(apps): resolve Agentic User file bytes through Graph /shares - #669

Open
Corina (corinagum) wants to merge 20 commits into
mainfrom
cg/files-graph-receive
Open

feat(apps): resolve Agentic User file bytes through Graph /shares#669
Corina (corinagum) wants to merge 20 commits into
mainfrom
cg/files-graph-receive

Conversation

@corinagum

Copy link
Copy Markdown
Collaborator

What this does

Agentic User file attachments arrive with a contentUrl and no downloadUrl, so the pre-authorized ("tempauth") URL traditional bots rely on is absent. This resolves those bytes through Microsoft Graph, GET /shares/{u!encoded}/driveItem/content, using the Agentic User's own credential.

The route is chosen from the shape of the attachment, not from the identity: a contentUrl with no downloadUrl, at personal scope, resolves via graphShare. Everything else keeps the existing preauthUrl path unchanged.

What it deliberately does not do

An expired pre-authorized URL is terminal. It raises FileUrlExpiredError and makes no Graph call.

A developer can still call Graph with their own client, and keeps the locator because contentUrl is public API. The constraint is on what the SDK does automatically.

Auditing trap: the agentic token is itself delegated-shaped, carrying scp rather than roles. Follow the identity, not the word "delegated".

Why .default and not a named scope

A by-name request hard-fails on exact-string consent mismatch: a blueprint consented to Files.ReadWrite.All returns AADSTS65001 and no token at all for a Files.Read.All request. Graph's own /shares docs list Files.ReadWrite.All as least-privileged and list no read-only permission, so admins following Microsoft's guidance land exactly where a by-name request breaks.

The cost is a late, ambiguous 403 when a file permission is missing. We pay that in the SDK instead: the pre-flight rejects a token carrying permissions but none file-capable, which restores an early, legible failure. A Files.Read.All-then-.default ladder was rejected, because AADSTS65001 is also what "no file permission consented at all" produces, so the ladder makes the most common deployment error harder to diagnose, not easier.

For security review: under .default the token carries the blueprint's full consented set, including unrelated scopes. The mitigation is the grant, not the request string: do not consent unrelated scopes on a file-handling blueprint.

Testing

Unit and integration coverage is in the diff. All three scenarios were also run live against real Teams, tenant asserted twice per run (browser session state, and the SharePoint host in the bot's own log).

Scenario Result
Traditional bot, fresh file preauthUrl, byte-exact
Agentic User via Graph /shares/{u!}/driveItem/content 200, byte-exact, driving the shipped helpers
Expired URL FileUrlExpiredError, zero requests to graph.microsoft.com in the entire process log

The third row was checked by grepping the whole bot process log for an absence, not by reading the rendered result, and the expiry is real rather than simulated: the same UniqueId returns 200 early and a hard 401 later. And the pre-authorized URL lifetime is not a constant: three runs bounded the flip at (62.7, 72.4], (62.5, 68.4] and (63.6, 66.4] minutes. Poll rather than picking a wait time.


This language

13 commits, 37 files, +3161 / -66.

A compatibility overload is kept deliberately. C# bakes optional arguments into the caller, so removing the five-parameter TeamsBotApplication signature is a runtime MissingMethodException for anything compiled against the old shape, even though the source change is compatible. Verified against the real v2.1.0 tag: that constructor exists there, and it is the only at-risk shipped signature in the change. It is hidden from IntelliSense and pinned by a reflection assertion against emitted metadata, because a positional call would simply rebind to the six-parameter overload and prove nothing.

ExpiredPreauthUrl_IsTerminal_AndDoesNotReachForGraph drives the shipped downloader with an expired downloadUrl and a valid contentUrl, the locator a fallback would use, and asserts zero token acquisitions. Supplying the contentUrl is the point: the URL is terminal because there is no recovery, not because the test withheld the means to recover.

Microsoft.Teams.Graph does not exist and is not this PR's job. .NET uses the official Graph package.

Running the integration tests. test/IntegrationTests is deliberately not referenced by Microsoft.Teams.slnx, so solution-wide runs skip it. Invoke it by project path.

Copilot AI lite review requested due to automatic review settings September 10, 2026 21:24
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved findings include FileDownloader binary compatibility and HTTPS validation for Graph endpoints.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds Microsoft Graph /shares resolution for Agentic User attachments lacking downloadUrl, while preserving pre-authorized URL handling and terminal expiry behavior.

Changes:

  • Adds Graph credentials, routing, host configuration, and attachment mapping.
  • Updates file downloading, error handling, and application wiring.
  • Adds unit/integration coverage and sample documentation.
File summaries
File Summary
test/Microsoft.Teams.Core.UnitTests/Hosting/BotTokenProviderAgenticIdentityTests.cs Tests Agentic identity token behavior.
test/Microsoft.Teams.Core.UnitTests/Hosting/BotConfigTests.cs Tests Graph host configuration.
test/Microsoft.Teams.Apps.UnitTests/TeamsBotApplicationTests.cs Tests constructor compatibility.
test/Microsoft.Teams.Apps.UnitTests/TeamsActivityTests.cs Tests tenant resolution.
test/Microsoft.Teams.Apps.UnitTests/FilesCredentialTests.cs Tests credential selection.
test/Microsoft.Teams.Apps.UnitTests/Files/IncomingFileTests.cs Tests file response handling and cancellation.
test/Microsoft.Teams.Apps.UnitTests/Files/GraphShareTests.cs Tests Graph share URL encoding.
test/Microsoft.Teams.Apps.UnitTests/Files/FilesAccessorAgenticTests.cs Tests content-only attachment mapping.
test/Microsoft.Teams.Apps.UnitTests/Files/FileErrorsTests.cs Tests file error contracts.
test/Microsoft.Teams.Apps.UnitTests/Files/FileDownloaderTransportTests.cs Tests transport authentication behavior.
test/Microsoft.Teams.Apps.UnitTests/Files/FileDownloaderGraphTests.cs Tests Graph downloads and failures.
test/Microsoft.Teams.Apps.UnitTests/CustomBotApplicationGraphWiringTests.cs Tests custom application Graph wiring.
test/Microsoft.Teams.Apps.UnitTests/ContextFilesCredentialTests.cs Tests context credential wiring.
test/IntegrationTests/GraphFilesFixture.cs Provides Graph integration test data.
test/IntegrationTests/FilesIntegrationTests.cs Updates file integration coverage.
test/IntegrationTests/FilesGraphIntegrationTests.cs Tests live Graph retrieval and expiry.
src/Microsoft.Teams.Core/Microsoft.Teams.Core.csproj Grants integration-test internals access.
src/Microsoft.Teams.Core/Hosting/BotTokenProvider.cs Adds app and Agentic Graph token acquisition.
src/Microsoft.Teams.Core/Hosting/BotConfig.cs Adds Graph host configuration.
src/Microsoft.Teams.Core/Hosting/BotAuthenticationHandler.cs Shares token option construction.
src/Microsoft.Teams.Core/Hosting/AddBotApplicationExtensions.cs Registers token provider services.
src/Microsoft.Teams.Core/GlobalSuppressions.cs Suppresses Graph URI configuration warnings.
src/Microsoft.Teams.Apps/TeamsBotApplicationOptions.cs Adds Graph host options.
src/Microsoft.Teams.Apps/TeamsBotApplication.HostingExtensions.cs Wires Graph services and configuration.
src/Microsoft.Teams.Apps/TeamsBotApplication.cs Adds Graph token and downloader wiring.
src/Microsoft.Teams.Apps/Schema/TeamsActivity.cs Adds tenant fallback resolution.
src/Microsoft.Teams.Apps/FilesCredential.cs Selects file credentials.
src/Microsoft.Teams.Apps/Files/IncomingFile.cs Carries Graph locators and credentials.
src/Microsoft.Teams.Apps/Files/GraphShare.cs Builds Graph share URLs.
src/Microsoft.Teams.Apps/Files/GraphCredential.cs Defines Graph credential data.
src/Microsoft.Teams.Apps/Files/FilesAccessor.cs Maps content-only attachments.
src/Microsoft.Teams.Apps/Files/FileErrors.cs Adds Graph retrieval errors.
src/Microsoft.Teams.Apps/Files/FileDownloadInfo.cs Updates file locator metadata.
src/Microsoft.Teams.Apps/Files/FileDownloader.cs Implements pre-authorized and Graph download routes.
src/Microsoft.Teams.Apps/Context.cs Connects file access to credentials.
samples/AIFileAnalysisBot/README.md Documents file retrieval routes.
samples/AIFileAnalysisBot/FileCard.cs Clarifies the displayed file source.
Review details

Suppressed comments (4)

samples/AIFileAnalysisBot/README.md:83

  • This rule is unconditional, but FilesAccessor only admits a content-URL-only attachment for personal scope and skips groupChat/channel attachments until those receive paths exist. State that scope restriction here so users do not expect a file handle, or a Graph fetch, from those activities.
The route is determined by whether or not the pre-authorized URL exists. If it does, it is used. Otherwise, the Graph path is followed.

samples/AIFileAnalysisBot/README.md:80

  • The new sentence is missing the article "a"; it should read "Teams provides a URL on the attachment."
- **A bot reads through the pre-authorized download URL.** Teams provides URL on the attachment and the SDK fetches it directly, with no tokens attached, shown in this example.

src/Microsoft.Teams.Apps/Files/FileErrors.cs:233

  • FileActor.App is a real Graph route here: content-URL-only files select this actor and GetAppGraphTokenAsync supplies an app token, and the Graph tests cover application roles. This message therefore tells ordinary bots that an app credential/permission can never fix the failure, which contradicts the implemented path and sends users to the wrong remedy. Update the message and its assertion to describe missing app Graph credentials/permissions and file access.
            FileActor.App => "the app has no usable Graph credential for this file. Graph file retrieval is supported for Agentic Users, which read as their own identity; an app identity and/or user-delegated permissions may be used but are not supported via the SDK at this time",

test/Microsoft.Teams.Apps.UnitTests/Files/IncomingFileTests.cs:129

  • The test handler ignores cancellationToken and always returns a response. The assertion can still pass because DownloadAsync later receives the same canceled token during content copy, so this test would stay green if a refactor passed CancellationToken.None to the transport. Record the token or make the handler fail based on the token it receives to actually pin propagation through SendAsync.
  • Files reviewed: 37/37 changed files
  • Comments generated: 6
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/Microsoft.Teams.Apps/Files/FileDownloader.cs
Comment thread src/Microsoft.Teams.Apps/Files/GraphShare.cs Outdated
Comment thread src/Microsoft.Teams.Core/Hosting/BotConfig.cs
Comment thread test/IntegrationTests/FilesGraphIntegrationTests.cs
Comment thread src/Microsoft.Teams.Apps/Files/FileDownloader.cs Outdated
Comment thread src/Microsoft.Teams.Apps/Files/FileErrors.cs Outdated
Port of the TypeScript change. FileRetrievalException and FileRetrievalFailureReason are replaced by FileCredentialException and FileAccessException, and the actor is no longer defaulted.

Two deliberate divergences. There is no tenant threading, because this provider applies no tenant override by design. The redirect guard is post-hoc on the final hop rather than per-hop, matching the existing 'must use https' InvalidOperationException precedent in this repo.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7d326e8a-aeb4-4481-bc0a-58d2a012584e
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants