Skip to content

feat(auth): Add 403 Forbidden insufficient_scope support per MCP Auth Spec 2025-11-25 and RFC 6750 (Section 3.1)#537

Merged
gocamille merged 7 commits intomainfrom
AMS-171
Jan 9, 2026
Merged

feat(auth): Add 403 Forbidden insufficient_scope support per MCP Auth Spec 2025-11-25 and RFC 6750 (Section 3.1)#537
gocamille merged 7 commits intomainfrom
AMS-171

Conversation

@gocamille
Copy link
Copy Markdown
Contributor

@gocamille gocamille commented Jan 6, 2026

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: Added BearerError::InsufficientScope enum and error field to WWW-Authenticate header
  • valid_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 get 403
  • headers.rs: Updated tests for new ValidToken struct

Behavior

  • 401 Unauthorized: Missing or invalid token
  • 403 Forbidden: Valid token but insufficient scopes (includes error="insufficient_scope" in response)

@apollo-librarian
Copy link
Copy Markdown
Contributor

apollo-librarian Bot commented Jan 6, 2026

✅ Docs preview has no changes

The preview was not built because there were no changes.

Build ID: 5beabf5d664ad5d66c2ba16e
Build Logs: View logs

@gocamille gocamille marked this pull request as ready for review January 7, 2026 17:19
@gocamille gocamille requested a review from DaleSeo January 7, 2026 17:19
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jan 7, 2026

Changeset file missing for PR

All changes should include an associated changeset file.
Please refer to README for more information on generating changesets.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 scope and Azure AD scp claims)
  • 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.

Comment on lines +249 to +268
// 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));
}
}
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This looks like one of those rare times when AI gives a good review. 😃

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Implemented test coverage in 3188f91

})?;

// Check if token has required scopes (fail-closed: missing scope claim = insufficient)
if !auth_config.scopes.is_empty() {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we assume that if users set the transport.auth.scopes option to an empty list, they want to skip scope validation completely?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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_supported field 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_supported from scopes_required — like you mentioned the MCP spec explicitly says scopes in WWW-Authenticate "may" match scopes_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)

Copy link
Copy Markdown
Member

@DaleSeo DaleSeo Jan 9, 2026

Choose a reason for hiding this comment

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

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.

Comment thread crates/apollo-mcp-server/src/auth.rs
Comment thread crates/apollo-mcp-server/src/auth/valid_token.rs Outdated
Comment thread .changeset/feat_add_insufficient_scope.md Outdated
Comment thread .changeset/feat_add_insufficient_scope.md Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.

Comment thread .changeset/feat_add_insufficient_scope.md Outdated

**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)
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

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:

  1. Removing the reference to "scp" and Azure AD from the documentation if Azure AD support via the "scp" claim is not needed, or
  2. Adding support for the "scp" claim in the Claims struct and extract_scopes logic to actually support Azure AD tokens
Suggested change
- `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)

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

@DaleSeo DaleSeo left a comment

Choose a reason for hiding this comment

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

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>
@gocamille gocamille merged commit 2354228 into main Jan 9, 2026
10 checks passed
@gocamille gocamille deleted the AMS-171 branch January 9, 2026 18:55
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.

3 participants