Use 120 characters instead of 88 (#856)
This commit is contained in:
parent
f7265f7b91
commit
543961968c
4 changed files with 687 additions and 2142 deletions
|
|
@ -7,9 +7,7 @@ from starlette.datastructures import FormData, QueryParams
|
|||
from starlette.requests import Request
|
||||
from starlette.responses import RedirectResponse, Response
|
||||
|
||||
from mcp.server.auth.errors import (
|
||||
stringify_pydantic_error,
|
||||
)
|
||||
from mcp.server.auth.errors import stringify_pydantic_error
|
||||
from mcp.server.auth.json_response import PydanticJSONResponse
|
||||
from mcp.server.auth.provider import (
|
||||
AuthorizationErrorCode,
|
||||
|
|
@ -18,10 +16,7 @@ from mcp.server.auth.provider import (
|
|||
OAuthAuthorizationServerProvider,
|
||||
construct_redirect_uri,
|
||||
)
|
||||
from mcp.shared.auth import (
|
||||
InvalidRedirectUriError,
|
||||
InvalidScopeError,
|
||||
)
|
||||
from mcp.shared.auth import InvalidRedirectUriError, InvalidScopeError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -29,23 +24,16 @@ logger = logging.getLogger(__name__)
|
|||
class AuthorizationRequest(BaseModel):
|
||||
# See https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
|
||||
client_id: str = Field(..., description="The client ID")
|
||||
redirect_uri: AnyUrl | None = Field(
|
||||
None, description="URL to redirect to after authorization"
|
||||
)
|
||||
redirect_uri: AnyUrl | None = Field(None, description="URL to redirect to after authorization")
|
||||
|
||||
# see OAuthClientMetadata; we only support `code`
|
||||
response_type: Literal["code"] = Field(
|
||||
..., description="Must be 'code' for authorization code flow"
|
||||
)
|
||||
response_type: Literal["code"] = Field(..., description="Must be 'code' for authorization code flow")
|
||||
code_challenge: str = Field(..., description="PKCE code challenge")
|
||||
code_challenge_method: Literal["S256"] = Field(
|
||||
"S256", description="PKCE code challenge method, must be S256"
|
||||
)
|
||||
code_challenge_method: Literal["S256"] = Field("S256", description="PKCE code challenge method, must be S256")
|
||||
state: str | None = Field(None, description="Optional state parameter")
|
||||
scope: str | None = Field(
|
||||
None,
|
||||
description="Optional scope; if specified, should be "
|
||||
"a space-separated list of scope strings",
|
||||
description="Optional scope; if specified, should be " "a space-separated list of scope strings",
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -57,9 +45,7 @@ class AuthorizationErrorResponse(BaseModel):
|
|||
state: str | None = None
|
||||
|
||||
|
||||
def best_effort_extract_string(
|
||||
key: str, params: None | FormData | QueryParams
|
||||
) -> str | None:
|
||||
def best_effort_extract_string(key: str, params: None | FormData | QueryParams) -> str | None:
|
||||
if params is None:
|
||||
return None
|
||||
value = params.get(key)
|
||||
|
|
@ -138,9 +124,7 @@ class AuthorizationHandler:
|
|||
|
||||
if redirect_uri and client:
|
||||
return RedirectResponse(
|
||||
url=construct_redirect_uri(
|
||||
str(redirect_uri), **error_resp.model_dump(exclude_none=True)
|
||||
),
|
||||
url=construct_redirect_uri(str(redirect_uri), **error_resp.model_dump(exclude_none=True)),
|
||||
status_code=302,
|
||||
headers={"Cache-Control": "no-store"},
|
||||
)
|
||||
|
|
@ -172,9 +156,7 @@ class AuthorizationHandler:
|
|||
if e["loc"] == ("response_type",) and e["type"] == "literal_error":
|
||||
error = "unsupported_response_type"
|
||||
break
|
||||
return await error_response(
|
||||
error, stringify_pydantic_error(validation_error)
|
||||
)
|
||||
return await error_response(error, stringify_pydantic_error(validation_error))
|
||||
|
||||
# Get client information
|
||||
client = await self.provider.get_client(
|
||||
|
|
@ -229,16 +211,9 @@ class AuthorizationHandler:
|
|||
)
|
||||
except AuthorizeError as e:
|
||||
# Handle authorization errors as defined in RFC 6749 Section 4.1.2.1
|
||||
return await error_response(
|
||||
error=e.error,
|
||||
error_description=e.error_description,
|
||||
)
|
||||
return await error_response(error=e.error, error_description=e.error_description)
|
||||
|
||||
except Exception as validation_error:
|
||||
# Catch-all for unexpected errors
|
||||
logger.exception(
|
||||
"Unexpected error in authorization_handler", exc_info=validation_error
|
||||
)
|
||||
return await error_response(
|
||||
error="server_error", error_description="An unexpected error occurred"
|
||||
)
|
||||
logger.exception("Unexpected error in authorization_handler", exc_info=validation_error)
|
||||
return await error_response(error="server_error", error_description="An unexpected error occurred")
|
||||
|
|
|
|||
|
|
@ -10,11 +10,7 @@ from starlette.responses import Response
|
|||
|
||||
from mcp.server.auth.errors import stringify_pydantic_error
|
||||
from mcp.server.auth.json_response import PydanticJSONResponse
|
||||
from mcp.server.auth.provider import (
|
||||
OAuthAuthorizationServerProvider,
|
||||
RegistrationError,
|
||||
RegistrationErrorCode,
|
||||
)
|
||||
from mcp.server.auth.provider import OAuthAuthorizationServerProvider, RegistrationError, RegistrationErrorCode
|
||||
from mcp.server.auth.settings import ClientRegistrationOptions
|
||||
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata
|
||||
|
||||
|
|
@ -60,9 +56,7 @@ class RegistrationHandler:
|
|||
|
||||
if client_metadata.scope is None and self.options.default_scopes is not None:
|
||||
client_metadata.scope = " ".join(self.options.default_scopes)
|
||||
elif (
|
||||
client_metadata.scope is not None and self.options.valid_scopes is not None
|
||||
):
|
||||
elif client_metadata.scope is not None and self.options.valid_scopes is not None:
|
||||
requested_scopes = set(client_metadata.scope.split())
|
||||
valid_scopes = set(self.options.valid_scopes)
|
||||
if not requested_scopes.issubset(valid_scopes):
|
||||
|
|
@ -78,8 +72,7 @@ class RegistrationHandler:
|
|||
return PydanticJSONResponse(
|
||||
content=RegistrationErrorResponse(
|
||||
error="invalid_client_metadata",
|
||||
error_description="grant_types must be authorization_code "
|
||||
"and refresh_token",
|
||||
error_description="grant_types must be authorization_code " "and refresh_token",
|
||||
),
|
||||
status_code=400,
|
||||
)
|
||||
|
|
@ -122,8 +115,6 @@ class RegistrationHandler:
|
|||
except RegistrationError as e:
|
||||
# Handle registration errors as defined in RFC 7591 Section 3.2.2
|
||||
return PydanticJSONResponse(
|
||||
content=RegistrationErrorResponse(
|
||||
error=e.error, error_description=e.error_description
|
||||
),
|
||||
content=RegistrationErrorResponse(error=e.error, error_description=e.error_description),
|
||||
status_code=400,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,15 +10,8 @@ from mcp.server.auth.errors import (
|
|||
stringify_pydantic_error,
|
||||
)
|
||||
from mcp.server.auth.json_response import PydanticJSONResponse
|
||||
from mcp.server.auth.middleware.client_auth import (
|
||||
AuthenticationError,
|
||||
ClientAuthenticator,
|
||||
)
|
||||
from mcp.server.auth.provider import (
|
||||
AccessToken,
|
||||
OAuthAuthorizationServerProvider,
|
||||
RefreshToken,
|
||||
)
|
||||
from mcp.server.auth.middleware.client_auth import AuthenticationError, ClientAuthenticator
|
||||
from mcp.server.auth.provider import AccessToken, OAuthAuthorizationServerProvider, RefreshToken
|
||||
|
||||
|
||||
class RevocationRequest(BaseModel):
|
||||
|
|
|
|||
|
|
@ -7,19 +7,10 @@ from typing import Annotated, Any, Literal
|
|||
from pydantic import AnyHttpUrl, AnyUrl, BaseModel, Field, RootModel, ValidationError
|
||||
from starlette.requests import Request
|
||||
|
||||
from mcp.server.auth.errors import (
|
||||
stringify_pydantic_error,
|
||||
)
|
||||
from mcp.server.auth.errors import stringify_pydantic_error
|
||||
from mcp.server.auth.json_response import PydanticJSONResponse
|
||||
from mcp.server.auth.middleware.client_auth import (
|
||||
AuthenticationError,
|
||||
ClientAuthenticator,
|
||||
)
|
||||
from mcp.server.auth.provider import (
|
||||
OAuthAuthorizationServerProvider,
|
||||
TokenError,
|
||||
TokenErrorCode,
|
||||
)
|
||||
from mcp.server.auth.middleware.client_auth import AuthenticationError, ClientAuthenticator
|
||||
from mcp.server.auth.provider import OAuthAuthorizationServerProvider, TokenError, TokenErrorCode
|
||||
from mcp.shared.auth import OAuthToken
|
||||
|
||||
|
||||
|
|
@ -27,9 +18,7 @@ class AuthorizationCodeRequest(BaseModel):
|
|||
# See https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3
|
||||
grant_type: Literal["authorization_code"]
|
||||
code: str = Field(..., description="The authorization code")
|
||||
redirect_uri: AnyUrl | None = Field(
|
||||
None, description="Must be the same as redirect URI provided in /authorize"
|
||||
)
|
||||
redirect_uri: AnyUrl | None = Field(None, description="Must be the same as redirect URI provided in /authorize")
|
||||
client_id: str
|
||||
# we use the client_secret param, per https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1
|
||||
client_secret: str | None = None
|
||||
|
|
@ -127,8 +116,7 @@ class TokenHandler:
|
|||
TokenErrorResponse(
|
||||
error="unsupported_grant_type",
|
||||
error_description=(
|
||||
f"Unsupported grant type (supported grant types are "
|
||||
f"{client_info.grant_types})"
|
||||
f"Unsupported grant type (supported grant types are " f"{client_info.grant_types})"
|
||||
),
|
||||
)
|
||||
)
|
||||
|
|
@ -137,9 +125,7 @@ class TokenHandler:
|
|||
|
||||
match token_request:
|
||||
case AuthorizationCodeRequest():
|
||||
auth_code = await self.provider.load_authorization_code(
|
||||
client_info, token_request.code
|
||||
)
|
||||
auth_code = await self.provider.load_authorization_code(client_info, token_request.code)
|
||||
if auth_code is None or auth_code.client_id != token_request.client_id:
|
||||
# if code belongs to different client, pretend it doesn't exist
|
||||
return self.response(
|
||||
|
|
@ -169,18 +155,13 @@ class TokenHandler:
|
|||
return self.response(
|
||||
TokenErrorResponse(
|
||||
error="invalid_request",
|
||||
error_description=(
|
||||
"redirect_uri did not match the one "
|
||||
"used when creating auth code"
|
||||
),
|
||||
error_description=("redirect_uri did not match the one " "used when creating auth code"),
|
||||
)
|
||||
)
|
||||
|
||||
# Verify PKCE code verifier
|
||||
sha256 = hashlib.sha256(token_request.code_verifier.encode()).digest()
|
||||
hashed_code_verifier = (
|
||||
base64.urlsafe_b64encode(sha256).decode().rstrip("=")
|
||||
)
|
||||
hashed_code_verifier = base64.urlsafe_b64encode(sha256).decode().rstrip("=")
|
||||
|
||||
if hashed_code_verifier != auth_code.code_challenge:
|
||||
# see https://datatracker.ietf.org/doc/html/rfc7636#section-4.6
|
||||
|
|
@ -193,9 +174,7 @@ class TokenHandler:
|
|||
|
||||
try:
|
||||
# Exchange authorization code for tokens
|
||||
tokens = await self.provider.exchange_authorization_code(
|
||||
client_info, auth_code
|
||||
)
|
||||
tokens = await self.provider.exchange_authorization_code(client_info, auth_code)
|
||||
except TokenError as e:
|
||||
return self.response(
|
||||
TokenErrorResponse(
|
||||
|
|
@ -205,13 +184,8 @@ class TokenHandler:
|
|||
)
|
||||
|
||||
case RefreshTokenRequest():
|
||||
refresh_token = await self.provider.load_refresh_token(
|
||||
client_info, token_request.refresh_token
|
||||
)
|
||||
if (
|
||||
refresh_token is None
|
||||
or refresh_token.client_id != token_request.client_id
|
||||
):
|
||||
refresh_token = await self.provider.load_refresh_token(client_info, token_request.refresh_token)
|
||||
if refresh_token is None or refresh_token.client_id != token_request.client_id:
|
||||
# if token belongs to different client, pretend it doesn't exist
|
||||
return self.response(
|
||||
TokenErrorResponse(
|
||||
|
|
@ -230,29 +204,20 @@ class TokenHandler:
|
|||
)
|
||||
|
||||
# Parse scopes if provided
|
||||
scopes = (
|
||||
token_request.scope.split(" ")
|
||||
if token_request.scope
|
||||
else refresh_token.scopes
|
||||
)
|
||||
scopes = token_request.scope.split(" ") if token_request.scope else refresh_token.scopes
|
||||
|
||||
for scope in scopes:
|
||||
if scope not in refresh_token.scopes:
|
||||
return self.response(
|
||||
TokenErrorResponse(
|
||||
error="invalid_scope",
|
||||
error_description=(
|
||||
f"cannot request scope `{scope}` "
|
||||
"not provided by refresh token"
|
||||
),
|
||||
error_description=(f"cannot request scope `{scope}` " "not provided by refresh token"),
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
# Exchange refresh token for new tokens
|
||||
tokens = await self.provider.exchange_refresh_token(
|
||||
client_info, refresh_token, scopes
|
||||
)
|
||||
tokens = await self.provider.exchange_refresh_token(client_info, refresh_token, scopes)
|
||||
except TokenError as e:
|
||||
return self.response(
|
||||
TokenErrorResponse(
|
||||
|
|
|
|||
Loading…
Reference in a new issue