Skip to content

Files.ReadWrite.AppFolder: every drive endpoint returns 403 accessDenied for personal accounts; only workaround is full-drive Files.ReadWrite #1930

Description

Summary

With a delegated token that was consented for Files.ReadWrite.AppFolder, every drive endpoint returns 403 accessDenied for a personal Microsoft account — including GET /me/drive/special/approot, which the docs say auto-creates the app folder on first access. Changing exactly one thing — the scope, to Files.ReadWrite — makes the identical requests succeed.

This forces apps that only ever write into their own folder to request full access to the user's entire OneDrive, which is a hard sell on the consent screen and inappropriate least-privilege-wise.

What we built

A B2B SaaS (Rails, plain REST calls against graph.microsoft.com/v1.0, no SDK) that uploads PDF reports into its app folder. Reproduced on 2026-08-25.

Reproduction

  1. App registration: "Accounts in any organizational directory and personal Microsoft accounts", Web redirect URI.
  2. API permissions (verified in the portal before consenting): User.Read, Files.ReadWrite.AppFolder (both delegated, no admin consent required).
  3. Authorization code flow via https://login.microsoftonline.com/common/oauth2/v2.0/authorize with scope=offline_access Files.ReadWrite.AppFolder User.Read and prompt=consent. Signed in with a personal Microsoft account; the consent dialog was shown and listed the app-folder permission ("Have full access to the application's folder (preview)").
  4. Token exchange succeeds; GET /v1.0/me200.
  5. All of the following return 403 accessDenied / "Access denied" with that token:
    • GET /v1.0/me/drive
    • GET /v1.0/me/drive/root
    • GET /v1.0/me/drive/special/approot
    • POST /v1.0/me/drive/special/approot:/report.pdf:/createUploadSession
  6. Re-consented with scope=offline_access Files.ReadWrite User.Read — no other change (same account, same app registration, same code). Every request above succeeds; createUploadSession + single-range PUT uploads work and the file lands in Apps/<app name> as expected.

(Personal-account Graph tokens are opaque, so we could not inspect the scp claim directly; the behavior was reproduced across two independent consent round-trips.)

Related reports

This appears to be the same long-standing problem as #1667 (403 on createUploadSession under approot, open since 2023, "Needs: Investigation"), #701, and #682 (closed with the workaround "add the broad Files permission", which defeats the scope's purpose). For business accounts the scope is rejected at sign-in altogether (AzureAD/microsoft-authentication-library-for-dotnet#681), so there is currently no account type for which Files.ReadWrite.AppFolder actually works end-to-end.

Comparison with other providers

The equivalent least-privilege grants work as documented elsewhere, and are platform-enforced:

Provider Mechanism Result
Google Drive drive.file scope App sees only files it created; non-sensitive scope, no verification burden. Works.
Dropbox "App folder" access type App is jailed to Apps/<app>; enforced server-side. Works.
OneDrive Files.ReadWrite.AppFolder 403 on everything (personal), sign-in rejected (business). Only workaround: request full-drive Files.ReadWrite.

Ask

Either fix Files.ReadWrite.AppFolder for personal accounts (and support it for business), or document an alternative least-privilege path for "app writes only into its own folder". We do not want to ask users for full access to their OneDrive when we only ever create files in Apps/<app name> — users notice the difference on the consent screen, and rightly so.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions