Skip to content

Conversation

@maxisbey
Copy link
Contributor

@maxisbey maxisbey commented Nov 6, 2025

Refactors OAuth 2.0 authentication implementation to expose reusable utilities.

Motivation and Context

The OAuth authentication logic was tightly coupled to the httpx auth provider, making it difficult for external tools (like MCP proxies or custom clients) to reuse core OAuth functionality. This refactor extracts the OAuth primitives into standalone utilities that can be used independently.

Key improvements:

  • Core OAuth utilities (discovery URL generation, PKCE, token handling) are now accessible without the full auth provider
  • Simplified the main OAuth2Auth class by removing internal state management
  • Better separation of concerns between protocol logic and HTTP transport

How Has This Been Tested?

Existing test suite updated to reflect new structure. All tests pass.

Breaking Changes

None - this is a pure refactor with no API changes.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

New utilities added:

  • client/auth/utils.py: OAuth protocol helpers (discovery URLs, WWW-Authenticate parsing, scope selection, response handlers)
  • shared/auth_utils.py: PKCE generation and token expiry calculation

The main OAuth2Auth provider (client/auth/oauth2.py) now delegates to these utilities, reducing from 221 lines to 93 lines of core logic.

@maxisbey maxisbey force-pushed the maxisbey/oauth-helpers branch 2 times, most recently from 040a546 to 81c470f Compare November 10, 2025 15:35
@maxisbey maxisbey changed the title refactor: pull out oauth helper functions refactor: extract OAuth helper functions and simplify provider state Nov 10, 2025
@maxisbey maxisbey marked this pull request as ready for review November 10, 2025 15:51
raise OAuthRegistrationError(f"Invalid registration response: {e}")


async def handle_token_response_scopes(

Choose a reason for hiding this comment

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

based on the discussion, we’ll want to remove that check from the SDK

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@maxisbey maxisbey force-pushed the maxisbey/oauth-helpers branch from 6780800 to fd612f2 Compare November 12, 2025 15:40
@maxisbey maxisbey requested a review from pcarleton November 12, 2025 15:40
Copy link
Member

@pcarleton pcarleton left a comment

Choose a reason for hiding this comment

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

looks good, a few minor comments

Validated OAuthToken model
Raises:
OAuthTokenError: If response JSON is invalid or contains unauthorized scopes
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
OAuthTokenError: If response JSON is invalid or contains unauthorized scopes
OAuthTokenError: If response JSON is invalid

return requested_path.startswith(configured_path)


def generate_pkce_parameters(verifier_length: int = 128) -> tuple[str, str]:
Copy link
Member

Choose a reason for hiding this comment

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

do we ever need to support different verifier lengths? seems like we can just always do 128, and cut out some complexity here

# Generate code_verifier using unreserved characters per RFC 7636 Section 4.1
# unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
code_verifier = "".join(
secrets.choice(string.ascii_letters + string.digits + "-._~") for _ in range(verifier_length)
Copy link
Member

Choose a reason for hiding this comment

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

optional: can use this:

code_verifier = secrets.token_urlsafe(96)

(borrowed from : https://github.com/RomeoDespres/pkce/blob/master/pkce/__init__.py#L40C5-L40C55)

oauth_path = f"/.well-known/oauth-authorization-server{parsed.path.rstrip('/')}"
urls.append(urljoin(base_url, oauth_path))

# OAuth root fallback
Copy link
Member

Choose a reason for hiding this comment

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

jfyi: we need to change this, but do it carefully (see modelcontextprotocol/typescript-sdk#1103) likely better in a follow-up, but wanted to flag as you're rolling this out.

tl;dr having this root-based above path-based OIDC means we'll get the root-level metadata when there's a path-based one we should use.

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.

4 participants