44Implements authorization code flow with PKCE and automatic token refresh.
55"""
66
7- import base64
8- import hashlib
97import logging
108import secrets
11- import string
129import time
1310from collections .abc import AsyncGenerator , Awaitable , Callable
1411from dataclasses import dataclass , field
3229 handle_auth_metadata_response ,
3330 handle_protected_resource_response ,
3431 handle_registration_response ,
32+ handle_token_response_scopes ,
3533)
3634from mcp .client .streamable_http import MCP_PROTOCOL_VERSION
3735from mcp .shared .auth import (
4139 OAuthToken ,
4240 ProtectedResourceMetadata ,
4341)
44- from mcp .shared .auth_utils import check_resource_allowed , resource_url_from_server_url
42+ from mcp .shared .auth_utils import (
43+ calculate_token_expiry ,
44+ check_resource_allowed ,
45+ generate_pkce_parameters ,
46+ resource_url_from_server_url ,
47+ )
4548
4649logger = logging .getLogger (__name__ )
4750
@@ -54,10 +57,8 @@ class PKCEParameters(BaseModel):
5457
5558 @classmethod
5659 def generate (cls ) -> "PKCEParameters" :
57- """Generate new PKCE parameters."""
58- code_verifier = "" .join (secrets .choice (string .ascii_letters + string .digits + "-._~" ) for _ in range (128 ))
59- digest = hashlib .sha256 (code_verifier .encode ()).digest ()
60- code_challenge = base64 .urlsafe_b64encode (digest ).decode ().rstrip ("=" )
60+ """Generate new PKCE parameters using shared util function."""
61+ code_verifier , code_challenge = generate_pkce_parameters (verifier_length = 128 )
6162 return cls (code_verifier = code_verifier , code_challenge = code_challenge )
6263
6364
@@ -114,11 +115,8 @@ def get_authorization_base_url(self, server_url: str) -> str:
114115 return f"{ parsed .scheme } ://{ parsed .netloc } "
115116
116117 def update_token_expiry (self , token : OAuthToken ) -> None :
117- """Update token expiry time."""
118- if token .expires_in :
119- self .token_expiry_time = time .time () + token .expires_in
120- else :
121- self .token_expiry_time = None
118+ """Update token expiry time using shared util function."""
119+ self .token_expiry_time = calculate_token_expiry (token .expires_in )
122120
123121 def is_token_valid (self ) -> bool :
124122 """Check if current token is valid."""
@@ -364,26 +362,20 @@ async def _handle_token_response(self, response: httpx.Response) -> None:
364362 """Handle token exchange response."""
365363 if response .status_code != 200 :
366364 body = await response .aread ()
367- body = body .decode ("utf-8" )
368- raise OAuthTokenError (f"Token exchange failed ({ response .status_code } ): { body } " )
369-
370- try :
371- content = await response .aread ()
372- token_response = OAuthToken .model_validate_json (content )
373-
374- # Validate scopes
375- if token_response .scope and self .context .client_metadata .scope :
376- requested_scopes = set (self .context .client_metadata .scope .split ())
377- returned_scopes = set (token_response .scope .split ())
378- unauthorized_scopes = returned_scopes - requested_scopes
379- if unauthorized_scopes :
380- raise OAuthTokenError (f"Server granted unauthorized scopes: { unauthorized_scopes } " )
365+ body_text = body .decode ("utf-8" )
366+ raise OAuthTokenError (f"Token exchange failed ({ response .status_code } ): { body_text } " )
367+
368+ # Parse and validate response with scope validation
369+ token_response = await handle_token_response_scopes (
370+ response ,
371+ self .context .client_metadata ,
372+ validate_scope = True ,
373+ )
381374
382- self .context .current_tokens = token_response
383- self .context .update_token_expiry (token_response )
384- await self .context .storage .set_tokens (token_response )
385- except ValidationError as e :
386- raise OAuthTokenError (f"Invalid token response: { e } " )
375+ # Store tokens in context
376+ self .context .current_tokens = token_response
377+ self .context .update_token_expiry (token_response )
378+ await self .context .storage .set_tokens (token_response )
387379
388380 async def _refresh_token (self ) -> httpx .Request :
389381 """Build token refresh request."""
0 commit comments