Skip to content

Fix Azure provider OIDC scope handling#2506

Merged
jlowin merged 3 commits intomainfrom
fix-azure-oidc-scope-handling
Dec 1, 2025
Merged

Fix Azure provider OIDC scope handling#2506
jlowin merged 3 commits intomainfrom
fix-azure-oidc-scope-handling

Conversation

@jlowin
Copy link
Copy Markdown
Member

@jlowin jlowin commented Dec 1, 2025

The Azure provider was incorrectly prefixing OIDC scopes (openid, profile, email, offline_access) with identifier_uri, causing Azure to reject authorization requests with errors like AADSTS65005: The application asked for scope 'openid' that doesn't exist.

Per Microsoft documentation, OIDC scopes are always sent as simple string identifiers without resource prefixes.

This fix allows users to include OIDC scopes in required_scopes just like other providers (Google, Auth0):

auth = AzureProvider(
    client_id="...",
    client_secret="...",
    tenant_id="...",
    required_scopes=["read", "openid", "profile"],  # Now works!
)

FastMCP automatically:

  • Sends OIDC scopes unprefixed to Azure
  • Filters OIDC scopes from token validation (Azure doesn't include them in scp claims)
  • Advertises all scopes to MCP clients via Protected Resource Metadata
Scope Type Example Behavior
Custom API read, write, my.scope Prefixed with identifier_uri
OIDC openid, profile, email, offline_access Sent as-is, excluded from validation
Fully-qualified api://xxx/read, https://graph.microsoft.com/User.Read Sent as-is

Note: Microsoft Graph scopes like User.Read should use additional_authorize_scopes or the fully-qualified format https://graph.microsoft.com/User.Read.

Fixes #2451, Fixes #2420

OIDC scopes (openid, profile, email, offline_access) were being
incorrectly prefixed with identifier_uri, causing Azure to reject
authorization requests. This fix:

- Detects OIDC scopes and sends them unprefixed to Azure
- Filters OIDC scopes from token validation (Azure doesn't include
  them in access token scp claims)
- Still advertises OIDC scopes to clients via valid_scopes
- Also handles dot-notation scopes (e.g., User.Read) correctly

Fixes #2451, #2420
@marvin-context-protocol marvin-context-protocol Bot added bug Something isn't working. Reports of errors, unexpected behavior, or broken functionality. auth Related to authentication (Bearer, JWT, OAuth, WorkOS) for client or server. labels Dec 1, 2025
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 1, 2025

Walkthrough

A module-level constant OIDC_SCOPES was added to identify standard OIDC scopes (openid, profile, email, offline_access). Scope handling was revised: OIDC scopes are preserved (not prefixed), fully-qualified and dot-notation scopes are preserved, and unprefixed custom API scopes are prefixed with the provider's identifier URI. Token validation now excludes OIDC scopes (validation uses only non-OIDC scopes) while the provider advertises the full required-scope set. Documentation was updated to describe these behaviors and examples.

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Title accurately captures the main fix: correcting OIDC scope handling in the Azure provider.
Description check ✅ Passed Description follows template with clear problem statement, solution, code examples, scope behavior table, and linked issues, though Contributors Checklist is not explicitly marked.
Linked Issues check ✅ Passed Code changes directly address both #2451 and #2420 by detecting OIDC scopes, sending them unprefixed, and excluding them from token validation.
Out of Scope Changes check ✅ Passed All changes remain scoped to Azure OIDC scope handling: documentation updates in docs/integrations/azure.mdx and implementation in src/fastmcp/server/auth/providers/azure.py.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-azure-oidc-scope-handling

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f18d598 and f4a52dd.

📒 Files selected for processing (1)
  • docs/integrations/azure.mdx (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/integrations/azure.mdx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run tests: Python 3.10 on windows-latest

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/fastmcp/server/auth/providers/azure.py (1)

368-382: Dot-notation detection may affect custom scopes with dots.

The check for "." in scope assumes dot-notation indicates Microsoft Graph scopes. However, a custom API scope containing a dot (e.g., my.custom.scope) would be treated as a Graph scope and not prefixed.

While uncommon, consider documenting this behavior or using a more specific check if Graph scope detection needs to be more precise (e.g., checking against a known Graph scope pattern).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 246a0ad and fecf0a1.

⛔ Files ignored due to path filters (1)
  • tests/server/auth/providers/test_azure.py is excluded by none and included by none
📒 Files selected for processing (2)
  • docs/integrations/azure.mdx (2 hunks)
  • src/fastmcp/server/auth/providers/azure.py (5 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
docs/**/*.mdx

📄 CodeRabbit inference engine (docs/.cursor/rules/mintlify.mdc)

docs/**/*.mdx: Use clear, direct language appropriate for technical audiences
Write in second person ('you') for instructions and procedures in MDX documentation
Use active voice over passive voice in MDX technical documentation
Employ present tense for current states and future tense for outcomes in MDX documentation
Maintain consistent terminology throughout all MDX documentation
Keep sentences concise while providing necessary context in MDX documentation
Use parallel structure in lists, headings, and procedures in MDX documentation
Lead with the most important information using inverted pyramid structure in MDX documentation
Use progressive disclosure in MDX documentation: present basic concepts before advanced ones
Break complex procedures into numbered steps in MDX documentation
Include prerequisites and context before instructions in MDX documentation
Provide expected outcomes for each major step in MDX documentation
End sections with next steps or related information in MDX documentation
Use descriptive, keyword-rich headings for navigation and SEO in MDX documentation
Focus on user goals and outcomes rather than system features in MDX documentation
Anticipate common questions and address them proactively in MDX documentation
Include troubleshooting for likely failure points in MDX documentation
Provide multiple pathways (beginner vs advanced) but offer an opinionated path to avoid overwhelming users in MDX documentation
Always include complete, runnable code examples that users can copy and execute in MDX documentation
Show proper error handling and edge case management in MDX code examples
Use realistic data instead of placeholder values in MDX code examples
Include expected outputs and results for verification in MDX code examples
Test all code examples thoroughly before publishing in MDX documentation
Specify language and include filename when relevant in MDX code examples
Add explanatory comments for complex logic in MDX code examples
Document all API...

Files:

  • docs/integrations/azure.mdx
src/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.py: Python source code must use Python ≥3.10 with full type annotations
Never use bare except - be specific with exception types
Follow Ruff linting and Prettier formatting standards - run uv run prek run --all-files before committing
Prioritize readable, understandable code - clarity over cleverness, avoid obfuscated or confusing patterns

Files:

  • src/fastmcp/server/auth/providers/azure.py
🧠 Learnings (2)
📓 Common learnings
Learnt from: jlowin
Repo: jlowin/fastmcp PR: 0
File: :0-0
Timestamp: 2025-12-01T15:48:05.083Z
Learning: PR #2505 in fastmcp adds NEW functionality to get_access_token(): it now first checks request.scope["user"] for the token (which never existed before), then falls back to _sdk_get_access_token() (the only thing the original code did). This is not a reversal of order but entirely new functionality to fix stale token issues.
📚 Learning: 2025-12-01T15:48:05.083Z
Learnt from: jlowin
Repo: jlowin/fastmcp PR: 0
File: :0-0
Timestamp: 2025-12-01T15:48:05.083Z
Learning: PR #2505 in fastmcp adds NEW functionality to get_access_token(): it now first checks request.scope["user"] for the token (which never existed before), then falls back to _sdk_get_access_token() (the only thing the original code did). This is not a reversal of order but entirely new functionality to fix stale token issues.

Applied to files:

  • docs/integrations/azure.mdx
  • src/fastmcp/server/auth/providers/azure.py
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run tests: Python 3.10 on windows-latest
🔇 Additional comments (6)
src/fastmcp/server/auth/providers/azure.py (4)

28-31: LGTM!

Good choice using frozenset for the OIDC scope constant—immutable and provides O(1) membership testing. The referenced Microsoft documentation accurately describes these as the standard OIDC scopes that should never be prefixed.


261-267: LGTM!

Correctly passes only non-OIDC scopes to the JWT verifier. This aligns with Azure v2.0 token format where the scp claim contains unprefixed custom API scopes only.


297-299: LGTM!

Clean separation between advertised scopes (full set for MCP client discovery) and validated scopes (non-OIDC only). The comment accurately documents this intentional distinction.


248-260: No action needed—scope validation behavior is intentional and correct.

When required_scopes contains only OIDC scopes, validation_scopes correctly becomes None. This is the intended design: Azure v2.0 access tokens don't include OIDC scopes in the scp claim, so validating them would fail. The code and inline comments (lines 248–251, 257, 266) adequately document this behavior.

docs/integrations/azure.mdx (2)

167-183: LGTM!

Clear and accurate documentation of the scope handling behavior. The table provides an excellent quick reference, and the Info block with the concrete example helps users understand the actual flow. The documentation correctly reflects the implementation.


325-326: LGTM!

Accurate explanation of OIDC scope handling within the required_scopes parameter documentation. This proactively addresses a common question users might have about including standard OIDC scopes.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
docs/integrations/azure.mdx (1)

167-187: Scope Handling section is clear and accurate.

The new section effectively explains the three scope categories and their behaviors. The table format is well-structured, the <Info> block clearly articulates the common use case (OIDC + custom scopes in required_scopes), and the <Tip> about Graph scopes provides helpful guidance on a likely follow-up question.

Minor observation: The table cell for fully-qualified scopes uses "Already has URI"—consider slightly more explicit phrasing like "Sent as-is (already fully qualified)" for added clarity, though the current wording is adequate.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fecf0a1 and f18d598.

⛔ Files ignored due to path filters (1)
  • tests/server/auth/providers/test_azure.py is excluded by none and included by none
📒 Files selected for processing (2)
  • docs/integrations/azure.mdx (2 hunks)
  • src/fastmcp/server/auth/providers/azure.py (5 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
src/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.py: Python source code must use Python ≥3.10 with full type annotations
Never use bare except - be specific with exception types
Follow Ruff linting and Prettier formatting standards - run uv run prek run --all-files before committing
Prioritize readable, understandable code - clarity over cleverness, avoid obfuscated or confusing patterns

Files:

  • src/fastmcp/server/auth/providers/azure.py
docs/**/*.mdx

📄 CodeRabbit inference engine (docs/.cursor/rules/mintlify.mdc)

docs/**/*.mdx: Use clear, direct language appropriate for technical audiences
Write in second person ('you') for instructions and procedures in MDX documentation
Use active voice over passive voice in MDX technical documentation
Employ present tense for current states and future tense for outcomes in MDX documentation
Maintain consistent terminology throughout all MDX documentation
Keep sentences concise while providing necessary context in MDX documentation
Use parallel structure in lists, headings, and procedures in MDX documentation
Lead with the most important information using inverted pyramid structure in MDX documentation
Use progressive disclosure in MDX documentation: present basic concepts before advanced ones
Break complex procedures into numbered steps in MDX documentation
Include prerequisites and context before instructions in MDX documentation
Provide expected outcomes for each major step in MDX documentation
End sections with next steps or related information in MDX documentation
Use descriptive, keyword-rich headings for navigation and SEO in MDX documentation
Focus on user goals and outcomes rather than system features in MDX documentation
Anticipate common questions and address them proactively in MDX documentation
Include troubleshooting for likely failure points in MDX documentation
Provide multiple pathways (beginner vs advanced) but offer an opinionated path to avoid overwhelming users in MDX documentation
Always include complete, runnable code examples that users can copy and execute in MDX documentation
Show proper error handling and edge case management in MDX code examples
Use realistic data instead of placeholder values in MDX code examples
Include expected outputs and results for verification in MDX code examples
Test all code examples thoroughly before publishing in MDX documentation
Specify language and include filename when relevant in MDX code examples
Add explanatory comments for complex logic in MDX code examples
Document all API...

Files:

  • docs/integrations/azure.mdx
🧠 Learnings (2)
📓 Common learnings
Learnt from: jlowin
Repo: jlowin/fastmcp PR: 0
File: :0-0
Timestamp: 2025-12-01T15:48:05.083Z
Learning: PR #2505 in fastmcp adds NEW functionality to get_access_token(): it now first checks request.scope["user"] for the token (which never existed before), then falls back to _sdk_get_access_token() (the only thing the original code did). This is not a reversal of order but entirely new functionality to fix stale token issues.
📚 Learning: 2025-12-01T15:48:05.083Z
Learnt from: jlowin
Repo: jlowin/fastmcp PR: 0
File: :0-0
Timestamp: 2025-12-01T15:48:05.083Z
Learning: PR #2505 in fastmcp adds NEW functionality to get_access_token(): it now first checks request.scope["user"] for the token (which never existed before), then falls back to _sdk_get_access_token() (the only thing the original code did). This is not a reversal of order but entirely new functionality to fix stale token issues.

Applied to files:

  • src/fastmcp/server/auth/providers/azure.py
  • docs/integrations/azure.mdx
🧬 Code graph analysis (1)
src/fastmcp/server/auth/providers/azure.py (1)
src/fastmcp/server/auth/providers/jwt.py (1)
  • JWTVerifier (165-498)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Run tests with lowest-direct dependencies
  • GitHub Check: Run tests: Python 3.10 on windows-latest
🔇 Additional comments (8)
src/fastmcp/server/auth/providers/azure.py (6)

28-32: OIDC scopes constant is well-defined.

Good use of frozenset for immutability and O(1) membership testing. The included scopes (openid, profile, email, offline_access) match Microsoft's documented standard OIDC scopes.


248-260: Validation scope filtering logic is correct.

The implementation properly:

  1. Excludes OIDC scopes from token validation (since Azure doesn't include them in scp claims).
  2. Falls back to None if only OIDC scopes are configured (disabling scope validation).
  3. Is protected by the earlier validation (lines 227-234) ensuring at least one scope exists.

This aligns with the PR objective to fix Azure's rejection of OIDC scope prefixing.


261-267: JWTVerifier receives correctly filtered scopes.

Passing validation_scopes (which excludes OIDC scopes) to the verifier ensures token validation won't fail due to missing OIDC scopes in Azure's scp claim. This directly addresses issue #2451.


297-299: Scope advertisement correctly includes all scopes.

Advertising the full required_scopes (including OIDC scopes) to MCP clients via valid_scopes while only validating non-OIDC scopes is the intended behavior per the PR objectives. This ensures clients can request OIDC scopes without causing validation failures.


372-381: Dot-notation Graph scopes in required_scopes would be incorrectly prefixed.

The PR description states: "Dot-notation Graph scopes (User.Read, Mail.Send): sent as-is (dot-notation recognized)." However, the implementation only checks for "://" or "/" to identify already-qualified scopes, not for dot-notation patterns.

If a user includes "User.Read" in required_scopes instead of additional_authorize_scopes, it would be prefixed to api://xxx/User.Read, which is incorrect.

Consider adding a check for dot-notation scopes:

 for scope in scopes:
     if scope in OIDC_SCOPES:
         # Standard OIDC scopes - never prefix
         prefixed.append(scope)
-    elif "://" in scope or "/" in scope:
+    elif "://" in scope or "/" in scope or "." in scope:
         # Already fully-qualified (e.g., "api://xxx/read" or
-        # "https://graph.microsoft.com/User.Read")
+        # "https://graph.microsoft.com/User.Read") or
+        # dot-notation Graph scopes (e.g., "User.Read")
         prefixed.append(scope)
     else:
         # Unprefixed custom API scope - prefix with identifier_uri
         prefixed.append(f"{self.identifier_uri}/{scope}")

Alternatively, if the current behavior is intentional (requiring Graph scopes via additional_authorize_scopes only), update the PR description to clarify.


409-446: Token refresh scope handling reuses the centralized prefixing logic.

The _prepare_scopes_for_upstream_refresh method correctly delegates to _prefix_scopes_for_azure, ensuring consistent scope handling across authorization and refresh flows. The deduplication using dict.fromkeys() is an idiomatic O(n) approach.

docs/integrations/azure.mdx (2)

328-329: ParamField update accurately documents OIDC scope behavior.

The addition clearly states that OIDC scopes can be included in required_scopes, are sent unprefixed to Azure, and are excluded from validation—directly addressing the PR objective. The note about Azure requiring at least one scope is consistent with earlier sections.


1-383: No revisions needed—the Azure API claims are accurate.

The documentation correctly states that:

  • OIDC scopes (openid, profile, email, offline_access) do not appear in the access token's scp claim (they control ID token/UserInfo content instead)
  • The scope parameter is required in Azure Entra ID v2.0 authorization requests
  • OIDC scopes can be mixed with API scopes in a single authorization request
  • FastMCP's automatic scope prefixing and handling aligns with Microsoft Entra ID's actual behavior

All technical claims match current Microsoft Entra ID documentation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auth Related to authentication (Bearer, JWT, OAuth, WorkOS) for client or server. bug Something isn't working. Reports of errors, unexpected behavior, or broken functionality.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Standard OIDC scopes get prefixed in azure auth provider AzureProvider fails on idp token exchange

1 participant