feat(oauth): Phase B — claude.ai protocol compatibility

Add 3 features for claude.ai Connectors compatibility:

- FEATURE-3: client_secret_basic auth on token endpoint (RFC 6749 §2.3.1)
- FEATURE-2: Token revocation endpoint /oauth/revoke (RFC 7009)
- FEATURE-4: resource parameter support with JWT aud claim (RFC 8707)

23 new tests (481 total), all passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
airano
2026-02-24 20:49:23 +03:30
parent 1736779d69
commit d3bcb31053
8 changed files with 852 additions and 3 deletions

View File

@@ -63,6 +63,7 @@ class AuthorizationCode(BaseModel):
api_key_id: str | None = None # API Key ID for scope/project inheritance
api_key_project_id: str | None = None # Project ID from API Key
api_key_scope: str | None = None # Scope from API Key
resource: str | None = None # RFC 8707: Resource indicator
def is_expired(self) -> bool:
"""Check if code is expired"""

View File

@@ -143,6 +143,7 @@ class OAuthServer:
api_key_id: str | None = None,
api_key_project_id: str | None = None,
api_key_scope: str | None = None,
resource: str | None = None,
) -> str:
"""
Create authorization code (Step 2 of Authorization Code flow)
@@ -157,6 +158,7 @@ class OAuthServer:
api_key_id: Optional API Key ID for scope/project inheritance
api_key_project_id: Optional project ID from API Key
api_key_scope: Optional scope from API Key
resource: Optional resource indicator (RFC 8707)
Returns:
Authorization code (valid for 5 minutes)
@@ -178,6 +180,7 @@ class OAuthServer:
api_key_id=api_key_id,
api_key_project_id=api_key_project_id,
api_key_scope=api_key_scope,
resource=resource,
)
# Save to storage
@@ -257,12 +260,14 @@ class OAuthServer:
# If authorization code has API Key metadata, use it for scoping
project_id = auth_code.api_key_project_id or "*"
token_scope = auth_code.api_key_scope or auth_code.scope
resource = auth_code.resource
access_token = self.token_manager.generate_access_token(
client_id=client_id,
scope=token_scope,
user_id=auth_code.user_id or auth_code.api_key_id,
project_id=project_id,
resource=resource,
)
refresh_token = self.token_manager.generate_refresh_token(

View File

@@ -54,7 +54,12 @@ class TokenManager:
self.refresh_token_ttl = int(os.getenv("OAUTH_REFRESH_TOKEN_TTL", "604800")) # 7 days
def generate_access_token(
self, client_id: str, scope: str, user_id: str | None = None, project_id: str = "*"
self,
client_id: str,
scope: str,
user_id: str | None = None,
project_id: str = "*",
resource: str | None = None,
) -> str:
"""
Generate JWT access token.
@@ -64,6 +69,7 @@ class TokenManager:
scope: Granted scopes (space-separated)
user_id: User ID (optional, for user-based auth)
project_id: Project ID for scoping (default: "*" for global)
resource: Resource indicator for aud claim (RFC 8707)
Returns:
JWT access token
@@ -87,6 +93,9 @@ class TokenManager:
if user_id:
payload["sub"] = user_id # Subject (user ID)
if resource:
payload["aud"] = resource
# Encode JWT
token = jwt.encode(payload, self.jwt_secret, algorithm=self.jwt_algorithm)