Initial commit: MCP Hub Community Edition v3.0.0
Community edition generated from private repo via sync pipeline. Includes 9 plugins (WordPress, WooCommerce, WP Advanced, Gitea, n8n, Supabase, OpenPanel, Appwrite, Directus) with ~587 tools. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
339
tests/test_oauth_integration.py
Normal file
339
tests/test_oauth_integration.py
Normal file
@@ -0,0 +1,339 @@
|
||||
"""
|
||||
OAuth 2.1 Integration Tests
|
||||
|
||||
Tests the complete OAuth flow end-to-end:
|
||||
1. Client registration
|
||||
2. Authorization request
|
||||
3. Authorization code generation
|
||||
4. Token exchange
|
||||
5. Token refresh
|
||||
6. JWT validation
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
from core.oauth import (
|
||||
OAuthError,
|
||||
generate_code_challenge,
|
||||
generate_code_verifier,
|
||||
get_client_registry,
|
||||
get_oauth_server,
|
||||
get_storage,
|
||||
get_token_manager,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_storage():
|
||||
"""Create temporary storage for tests"""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
# Set environment for storage
|
||||
os.environ["OAUTH_STORAGE_PATH"] = tmpdir
|
||||
os.environ["OAUTH_JWT_SECRET_KEY"] = "test_secret_key_for_integration_tests"
|
||||
|
||||
# Reset singletons by clearing them
|
||||
from core.oauth import client_registry, server, storage, token_manager
|
||||
|
||||
client_registry._client_registry = None
|
||||
token_manager._token_manager = None
|
||||
storage._storage = None
|
||||
server._oauth_server = None
|
||||
|
||||
yield tmpdir
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def oauth_components(temp_storage):
|
||||
"""Get fresh OAuth components"""
|
||||
return {
|
||||
"server": get_oauth_server(),
|
||||
"client_registry": get_client_registry(),
|
||||
"token_manager": get_token_manager(),
|
||||
"storage": get_storage(),
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_client(oauth_components):
|
||||
"""Create a test OAuth client"""
|
||||
client_registry = oauth_components["client_registry"]
|
||||
|
||||
client_id, client_secret = client_registry.create_client(
|
||||
client_name="Test Client",
|
||||
redirect_uris=["http://localhost:3000/callback"],
|
||||
grant_types=["authorization_code", "refresh_token", "client_credentials"],
|
||||
allowed_scopes=["read", "write", "admin"],
|
||||
)
|
||||
|
||||
return {
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
"redirect_uri": "http://localhost:3000/callback",
|
||||
}
|
||||
|
||||
|
||||
def test_full_authorization_code_flow(oauth_components, test_client):
|
||||
"""
|
||||
Test complete Authorization Code flow with PKCE
|
||||
|
||||
Steps:
|
||||
1. Generate PKCE verifier/challenge
|
||||
2. Validate authorization request
|
||||
3. Create authorization code
|
||||
4. Exchange code for tokens
|
||||
5. Validate access token
|
||||
6. Refresh access token
|
||||
"""
|
||||
server = oauth_components["server"]
|
||||
token_manager = oauth_components["token_manager"]
|
||||
|
||||
# Step 1: Generate PKCE
|
||||
code_verifier = generate_code_verifier()
|
||||
code_challenge = generate_code_challenge(code_verifier)
|
||||
|
||||
# Step 2: Validate authorization request
|
||||
validated = server.validate_authorization_request(
|
||||
client_id=test_client["client_id"],
|
||||
redirect_uri=test_client["redirect_uri"],
|
||||
response_type="code",
|
||||
code_challenge=code_challenge,
|
||||
code_challenge_method="S256",
|
||||
scope="read write",
|
||||
state="random_state_token",
|
||||
)
|
||||
|
||||
assert validated["client_id"] == test_client["client_id"]
|
||||
assert validated["scope"] == "read write"
|
||||
assert validated["state"] == "random_state_token"
|
||||
|
||||
# Step 3: Create authorization code
|
||||
auth_code = server.create_authorization_code(
|
||||
client_id=validated["client_id"],
|
||||
redirect_uri=validated["redirect_uri"],
|
||||
scope=validated["scope"],
|
||||
code_challenge=validated["code_challenge"],
|
||||
code_challenge_method=validated["code_challenge_method"],
|
||||
)
|
||||
|
||||
assert auth_code.startswith("auth_")
|
||||
|
||||
# Step 4: Exchange code for tokens
|
||||
token_response = server.exchange_code_for_tokens(
|
||||
client_id=test_client["client_id"],
|
||||
client_secret=test_client["client_secret"],
|
||||
code=auth_code,
|
||||
redirect_uri=test_client["redirect_uri"],
|
||||
code_verifier=code_verifier,
|
||||
)
|
||||
|
||||
assert token_response.access_token
|
||||
assert token_response.refresh_token
|
||||
assert token_response.token_type == "Bearer"
|
||||
assert token_response.expires_in > 0
|
||||
assert token_response.scope == "read write"
|
||||
|
||||
# Step 5: Validate access token
|
||||
payload = token_manager.validate_access_token(token_response.access_token)
|
||||
|
||||
assert payload["client_id"] == test_client["client_id"]
|
||||
assert payload["scope"] == "read write"
|
||||
assert "exp" in payload
|
||||
assert "iat" in payload
|
||||
assert "jti" in payload
|
||||
|
||||
# Step 6: Refresh access token
|
||||
new_tokens = server.handle_refresh_token_grant(
|
||||
client_id=test_client["client_id"],
|
||||
client_secret=test_client["client_secret"],
|
||||
refresh_token=token_response.refresh_token,
|
||||
)
|
||||
|
||||
assert new_tokens.access_token != token_response.access_token
|
||||
assert new_tokens.refresh_token != token_response.refresh_token
|
||||
assert new_tokens.scope == "read write"
|
||||
|
||||
|
||||
def test_client_credentials_flow(oauth_components, test_client):
|
||||
"""
|
||||
Test Client Credentials flow (machine-to-machine)
|
||||
"""
|
||||
server = oauth_components["server"]
|
||||
token_manager = oauth_components["token_manager"]
|
||||
|
||||
# Request token with client credentials
|
||||
token_response = server.handle_client_credentials_grant(
|
||||
client_id=test_client["client_id"], client_secret=test_client["client_secret"], scope="read"
|
||||
)
|
||||
|
||||
assert token_response.access_token
|
||||
assert token_response.refresh_token is None # No refresh token for client credentials
|
||||
assert token_response.scope == "read"
|
||||
|
||||
# Validate token
|
||||
payload = token_manager.validate_access_token(token_response.access_token)
|
||||
|
||||
assert payload["client_id"] == test_client["client_id"]
|
||||
assert payload["scope"] == "read"
|
||||
|
||||
|
||||
def test_pkce_validation_failure(oauth_components, test_client):
|
||||
"""Test that PKCE validation fails with wrong code_verifier"""
|
||||
server = oauth_components["server"]
|
||||
|
||||
# Generate PKCE
|
||||
code_verifier = generate_code_verifier()
|
||||
code_challenge = generate_code_challenge(code_verifier)
|
||||
|
||||
# Create authorization code
|
||||
validated = server.validate_authorization_request(
|
||||
client_id=test_client["client_id"],
|
||||
redirect_uri=test_client["redirect_uri"],
|
||||
response_type="code",
|
||||
code_challenge=code_challenge,
|
||||
code_challenge_method="S256",
|
||||
)
|
||||
|
||||
auth_code = server.create_authorization_code(
|
||||
client_id=validated["client_id"],
|
||||
redirect_uri=validated["redirect_uri"],
|
||||
scope=validated["scope"],
|
||||
code_challenge=validated["code_challenge"],
|
||||
code_challenge_method=validated["code_challenge_method"],
|
||||
)
|
||||
|
||||
# Try to exchange with WRONG code_verifier
|
||||
wrong_verifier = generate_code_verifier()
|
||||
|
||||
with pytest.raises(OAuthError, match="PKCE validation failed"):
|
||||
server.exchange_code_for_tokens(
|
||||
client_id=test_client["client_id"],
|
||||
client_secret=test_client["client_secret"],
|
||||
code=auth_code,
|
||||
redirect_uri=test_client["redirect_uri"],
|
||||
code_verifier=wrong_verifier, # WRONG!
|
||||
)
|
||||
|
||||
|
||||
def test_authorization_code_reuse_detection(oauth_components, test_client):
|
||||
"""Test that reusing an authorization code is detected"""
|
||||
server = oauth_components["server"]
|
||||
|
||||
# Generate PKCE and authorization code
|
||||
code_verifier = generate_code_verifier()
|
||||
code_challenge = generate_code_challenge(code_verifier)
|
||||
|
||||
validated = server.validate_authorization_request(
|
||||
client_id=test_client["client_id"],
|
||||
redirect_uri=test_client["redirect_uri"],
|
||||
response_type="code",
|
||||
code_challenge=code_challenge,
|
||||
code_challenge_method="S256",
|
||||
)
|
||||
|
||||
auth_code = server.create_authorization_code(
|
||||
client_id=validated["client_id"],
|
||||
redirect_uri=validated["redirect_uri"],
|
||||
scope=validated["scope"],
|
||||
code_challenge=validated["code_challenge"],
|
||||
code_challenge_method=validated["code_challenge_method"],
|
||||
)
|
||||
|
||||
# First exchange - should succeed
|
||||
token_response = server.exchange_code_for_tokens(
|
||||
client_id=test_client["client_id"],
|
||||
client_secret=test_client["client_secret"],
|
||||
code=auth_code,
|
||||
redirect_uri=test_client["redirect_uri"],
|
||||
code_verifier=code_verifier,
|
||||
)
|
||||
|
||||
assert token_response.access_token
|
||||
|
||||
# Second exchange with same code - should fail
|
||||
with pytest.raises(OAuthError, match="already used"):
|
||||
server.exchange_code_for_tokens(
|
||||
client_id=test_client["client_id"],
|
||||
client_secret=test_client["client_secret"],
|
||||
code=auth_code,
|
||||
redirect_uri=test_client["redirect_uri"],
|
||||
code_verifier=code_verifier,
|
||||
)
|
||||
|
||||
|
||||
def test_invalid_client_credentials(oauth_components, test_client):
|
||||
"""Test that invalid client credentials are rejected"""
|
||||
server = oauth_components["server"]
|
||||
|
||||
# Try with wrong client secret
|
||||
with pytest.raises(OAuthError, match="Invalid client credentials"):
|
||||
server.handle_client_credentials_grant(
|
||||
client_id=test_client["client_id"], client_secret="wrong_secret", scope="read"
|
||||
)
|
||||
|
||||
|
||||
def test_scope_validation(oauth_components, test_client):
|
||||
"""Test that invalid scopes are rejected"""
|
||||
server = oauth_components["server"]
|
||||
|
||||
# Try to request invalid scope
|
||||
code_verifier = generate_code_verifier()
|
||||
code_challenge = generate_code_challenge(code_verifier)
|
||||
|
||||
with pytest.raises(OAuthError, match="not allowed"):
|
||||
server.validate_authorization_request(
|
||||
client_id=test_client["client_id"],
|
||||
redirect_uri=test_client["redirect_uri"],
|
||||
response_type="code",
|
||||
code_challenge=code_challenge,
|
||||
code_challenge_method="S256",
|
||||
scope="invalid_scope", # Not in allowed_scopes
|
||||
)
|
||||
|
||||
|
||||
def test_redirect_uri_validation(oauth_components, test_client):
|
||||
"""Test that invalid redirect URIs are rejected"""
|
||||
server = oauth_components["server"]
|
||||
|
||||
code_verifier = generate_code_verifier()
|
||||
code_challenge = generate_code_challenge(code_verifier)
|
||||
|
||||
# Try with unregistered redirect_uri
|
||||
with pytest.raises(OAuthError, match="Invalid redirect_uri"):
|
||||
server.validate_authorization_request(
|
||||
client_id=test_client["client_id"],
|
||||
redirect_uri="http://evil.com/callback", # Not registered!
|
||||
response_type="code",
|
||||
code_challenge=code_challenge,
|
||||
code_challenge_method="S256",
|
||||
)
|
||||
|
||||
|
||||
def test_expired_token(oauth_components, test_client):
|
||||
"""Test that expired JWT tokens are rejected"""
|
||||
import time
|
||||
|
||||
import jwt as pyjwt
|
||||
|
||||
token_manager = oauth_components["token_manager"]
|
||||
|
||||
# Set very short TTL
|
||||
original_ttl = token_manager.access_token_ttl
|
||||
token_manager.access_token_ttl = 1 # 1 second
|
||||
|
||||
# Generate token
|
||||
access_token = token_manager.generate_access_token(
|
||||
client_id=test_client["client_id"], scope="read"
|
||||
)
|
||||
|
||||
# Wait for expiration
|
||||
time.sleep(2)
|
||||
|
||||
# Try to validate expired token
|
||||
with pytest.raises(pyjwt.ExpiredSignatureError):
|
||||
token_manager.validate_access_token(access_token)
|
||||
|
||||
# Restore original TTL
|
||||
token_manager.access_token_ttl = original_ttl
|
||||
Reference in New Issue
Block a user