feat(auth): Add 403 Forbidden insufficient_scope support per MCP Auth Spec 2025-11-25 and RFC 6750 (Section 3.1)#537
Conversation
✅ Docs preview has no changesThe preview was not built because there were no changes. Build ID: 5beabf5d664ad5d66c2ba16e |
|
❌ Changeset file missing for PR All changes should include an associated changeset file. |
There was a problem hiding this comment.
Pull request overview
This PR adds HTTP 403 Forbidden responses with error="insufficient_scope" to properly handle valid tokens that lack required OAuth scopes, implementing the MCP Auth Spec 2025-11-25 and RFC 6750 Section 3.1.
Key Changes:
- Added scope extraction from JWT tokens (supporting both standard
scopeand Azure ADscpclaims) - Implemented fail-closed scope validation that returns 403 when valid tokens lack required scopes
- Enhanced WWW-Authenticate header to include
error="insufficient_scope"parameter
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
crates/apollo-mcp-server/src/headers.rs |
Updated test cases to use new ValidToken struct with token and scopes fields |
crates/apollo-mcp-server/src/auth/www_authenticate.rs |
Added BearerError enum with InsufficientScope variant and error field to Bearer authentication header |
crates/apollo-mcp-server/src/auth/valid_token.rs |
Restructured ValidToken to include scopes; added extract_scopes function with comprehensive tests for OAuth/Azure AD scope claim handling |
crates/apollo-mcp-server/src/auth.rs |
Implemented scope validation middleware that returns 403 Forbidden when tokens have insufficient scopes |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Check if token has required scopes (fail-closed: missing scope claim = insufficient) | ||
| if !auth_config.scopes.is_empty() { | ||
| let missing_scopes: Vec<_> = auth_config | ||
| .scopes | ||
| .iter() | ||
| .filter(|required| !valid_token.scopes.iter().any(|s| s == *required)) | ||
| .collect(); | ||
|
|
||
| if !missing_scopes.is_empty() { | ||
| tracing::warn!( | ||
| required = ?auth_config.scopes, | ||
| present = ?valid_token.scopes, | ||
| missing = ?missing_scopes, | ||
| "Token has insufficient scopes" | ||
| ); | ||
| tracing::Span::current().record("reason", "insufficient_scope"); | ||
| tracing::Span::current().record("status_code", StatusCode::FORBIDDEN.as_u16()); | ||
| return Err(forbidden_error(&auth_config.scopes)); | ||
| } | ||
| } |
There was a problem hiding this comment.
The scope validation logic lacks test coverage. Consider adding test cases for:
- A valid token with insufficient scopes should return 403 Forbidden with
error="insufficient_scope"in the WWW-Authenticate header - A valid token with all required scopes should succeed
- A valid token with no scopes when scopes are required should return 403 Forbidden
- A valid token with extra scopes (superset) should succeed
This is critical functionality for the security model and should be thoroughly tested.
There was a problem hiding this comment.
This looks like one of those rare times when AI gives a good review. 😃
| })?; | ||
|
|
||
| // Check if token has required scopes (fail-closed: missing scope claim = insufficient) | ||
| if !auth_config.scopes.is_empty() { |
There was a problem hiding this comment.
Should we assume that if users set the transport.auth.scopes option to an empty list, they want to skip scope validation completely?
There was a problem hiding this comment.
This (and the one below) is a really great question (and super nuanced @DaleSeo !) -- looking at the specs (both MCP and OAuth), this assumption is correct. So when transport.auth.scopes is set to an empty list the following would happen:
- Scope validation would be completely skipped
- The token would only need to pass signature and audience validation
- The
scopes_supportedfield in the protected resource metadata would be empty (which a client could see as "no specific scopes needed")
This seems a reasonable default for those who want simple "authenticated or not" access control, but (and this is related to the comment below too) we may want to consider adding flexibility / complexity in a few ways:
- Separating
scopes_supportedfromscopes_required— like you mentioned the MCP spec explicitly says scopes inWWW-Authenticate"may" matchscopes_supported, be a subset or superset of it, or an alternative collection, while right now we use one list for enforcement. - Supporting per-tool scope requirements (a big one) — The spec mentions "minimum scopes needed for the operation," so different MCP tools might require different scopes.
- Adding "any-of" logic may also be good here, as the current implementation requires all listed scopes while some use cases might only need at least one from a set.
What do you think of the current "all-or-nothing" approach? Should we enhance the flexibility of this to cover any of the above items?
(If we do stay with the current approach I would update the docs to document this clearly, and possibly add a warning when the scopes list is empty)
There was a problem hiding this comment.
Thanks for sharing your thoughts, @gocamille! This reminds me that in PR #535, we introduced the allow_any_audience flag instead of treating audiences: [] as skipping validation. I think we should do the same for scopes, and it might be another case where a CORS-like configuration makes sense. Skipping scope validation reduces security, so I would suggest making it an explicit opt-out.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| **Changes:** | ||
| - `www_authenticate.rs`: Added `BearerError::InsufficientScope` enum and [error](cci:1://file:///Users/camillelawrence/Desktop/repos/apollo-mcp-server/crates/apollo-mcp-server/src/auth/www_authenticate.rs:135:4-149:5) field to `WWW-Authenticate` header | ||
| - `valid_token.rs`: Extract [scope](cci:1://file:///Users/camillelawrence/Desktop/repos/apollo-mcp-server/crates/apollo-mcp-server/src/auth/valid_token.rs:27:0-38:1)/[scp](cci:1://file:///Users/camillelawrence/Desktop/repos/apollo-mcp-server/crates/apollo-mcp-server/src/auth/valid_token.rs:503:8-509:9) claims from JWTs (handles both standard OAuth and Azure AD) |
There was a problem hiding this comment.
The changeset documentation mentions handling both "scope" and "scp" claims for Azure AD, but the implementation only handles the standard "scope" claim. Azure AD uses "scp" for delegated permissions in access tokens, which is different from the standard OAuth "scope" claim. Consider either:
- Removing the reference to "scp" and Azure AD from the documentation if Azure AD support via the "scp" claim is not needed, or
- Adding support for the "scp" claim in the Claims struct and extract_scopes logic to actually support Azure AD tokens
| - `valid_token.rs`: Extract [scope](cci:1://file:///Users/camillelawrence/Desktop/repos/apollo-mcp-server/crates/apollo-mcp-server/src/auth/valid_token.rs:27:0-38:1)/[scp](cci:1://file:///Users/camillelawrence/Desktop/repos/apollo-mcp-server/crates/apollo-mcp-server/src/auth/valid_token.rs:503:8-509:9) claims from JWTs (handles both standard OAuth and Azure AD) | |
| - `valid_token.rs`: Extract [scope](cci:1://file:///Users/camillelawrence/Desktop/repos/apollo-mcp-server/crates/apollo-mcp-server/src/auth/valid_token.rs:27:0-38:1) claims from JWTs (standard OAuth scopes) |
DaleSeo
left a comment
There was a problem hiding this comment.
Thanks for elaborating to address my feedback! I think the current implementation is good for now. We should be one of the early adopters and see how other MCP servers handle the ambiguous spec. Since this is related to security, I have one more suggestion to let users opt out of scope validation more explicitly, but I'll leave that up to you. I agree that we need to clearly document this scope validation behavior, regardless.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Summary
This adds HTTP 403 Forbidden responses with
error="insufficient_scope"per MCP Auth Spec 2025-11-25 Section 10: Error Handling and RFC 6750 Section 3.1.Changes
www_authenticate.rs: AddedBearerError::InsufficientScopeenum and error field toWWW-Authenticateheadervalid_token.rs: Extract scope/scp claims from JWTs (handles both standard OAuth and Azure AD)auth.rs: Scope validation with fail-closed behaviour—valid tokens lacking required scopes get403headers.rs: Updated tests for newValidTokenstructBehavior
error="insufficient_scope"in response)