Skip to content

Commit 04b5b58

Browse files
committed
feat: validate server config
1 parent cf7b48b commit 04b5b58

File tree

2 files changed

+266
-0
lines changed

2 files changed

+266
-0
lines changed
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
from enum import Enum
2+
from typing import Any, Dict, List, Optional
3+
from pydantic import BaseModel, ValidationError
4+
from mcpauth.models.auth_server import AuthServerConfig
5+
6+
7+
class AuthServerConfigErrorCode(str, Enum):
8+
"""
9+
The codes for errors that can occur when validating the authorization server metadata.
10+
"""
11+
12+
INVALID_SERVER_METADATA = "invalid_server_metadata"
13+
CODE_RESPONSE_TYPE_NOT_SUPPORTED = "code_response_type_not_supported"
14+
AUTHORIZATION_CODE_GRANT_NOT_SUPPORTED = "authorization_code_grant_not_supported"
15+
PKCE_NOT_SUPPORTED = "pkce_not_supported"
16+
S256_CODE_CHALLENGE_METHOD_NOT_SUPPORTED = (
17+
"s256_code_challenge_method_not_supported"
18+
)
19+
20+
21+
auth_server_config_error_description: Dict[AuthServerConfigErrorCode, str] = {
22+
AuthServerConfigErrorCode.INVALID_SERVER_METADATA: "The server metadata is not a valid object or does not conform to the expected schema.",
23+
AuthServerConfigErrorCode.CODE_RESPONSE_TYPE_NOT_SUPPORTED: 'The server does not support the "code" response type or the "code" response type is not included in one of the supported response types.',
24+
AuthServerConfigErrorCode.AUTHORIZATION_CODE_GRANT_NOT_SUPPORTED: 'The server does not support the "authorization_code" grant type.',
25+
AuthServerConfigErrorCode.PKCE_NOT_SUPPORTED: "The server does not support Proof Key for Code Exchange (PKCE).",
26+
AuthServerConfigErrorCode.S256_CODE_CHALLENGE_METHOD_NOT_SUPPORTED: 'The server does not support the "S256" code challenge method for Proof Key for Code Exchange (PKCE).',
27+
}
28+
29+
30+
class AuthServerConfigError(BaseModel):
31+
"""
32+
Represents an error that occurs during the validation of the authorization server metadata.
33+
"""
34+
35+
code: AuthServerConfigErrorCode
36+
description: str
37+
cause: Any
38+
39+
40+
def _create_error(
41+
code: AuthServerConfigErrorCode, cause: Optional[Exception] = None
42+
) -> AuthServerConfigError:
43+
44+
return AuthServerConfigError(
45+
code=code,
46+
description=auth_server_config_error_description[code],
47+
cause=cause,
48+
)
49+
50+
51+
class AuthServerConfigWarningCode(str, Enum):
52+
"""
53+
The codes for warnings that can occur when validating the authorization server metadata.
54+
"""
55+
56+
DYNAMIC_REGISTRATION_NOT_SUPPORTED = "dynamic_registration_not_supported"
57+
58+
59+
auth_server_config_warning_description: Dict[AuthServerConfigWarningCode, str] = {
60+
AuthServerConfigWarningCode.DYNAMIC_REGISTRATION_NOT_SUPPORTED: "Dynamic Client Registration (RFC 7591) is not supported by the server."
61+
}
62+
63+
64+
class AuthServerConfigWarning(BaseModel):
65+
"""
66+
Represents a warning that occurs during the validation of the authorization server metadata.
67+
"""
68+
69+
code: AuthServerConfigWarningCode
70+
description: str
71+
72+
73+
def _create_warning(code: AuthServerConfigWarningCode) -> AuthServerConfigWarning:
74+
return AuthServerConfigWarning(
75+
code=code,
76+
description=auth_server_config_warning_description[code],
77+
)
78+
79+
80+
class AuthServerConfigValidationResult(BaseModel):
81+
is_valid: bool
82+
errors: List[AuthServerConfigError]
83+
warnings: List[AuthServerConfigWarning]
84+
85+
86+
def validate_server_config(
87+
config: AuthServerConfig,
88+
) -> AuthServerConfigValidationResult:
89+
"""
90+
Validates the authorization server configuration against the MCP specification.
91+
92+
Args:
93+
config: The configuration object containing the server metadata to validate.
94+
95+
Returns:
96+
An object indicating whether the configuration is valid (`{ is_valid: True }`) or
97+
invalid (`{ is_valid: False }`), along with any errors or warnings encountered during validation.
98+
"""
99+
100+
errors: List[AuthServerConfigError] = []
101+
warnings: List[AuthServerConfigWarning] = []
102+
103+
metadata = config.metadata
104+
105+
# Validate metadata
106+
try:
107+
# Validation is already done by Pydantic when the object is created
108+
# But we can add additional validation if needed
109+
pass
110+
except ValidationError as e:
111+
errors.append(
112+
_create_error(AuthServerConfigErrorCode.INVALID_SERVER_METADATA, e)
113+
)
114+
return AuthServerConfigValidationResult(
115+
is_valid=False, errors=errors, warnings=warnings
116+
)
117+
118+
# Check if 'code' is included in any of the supported response types
119+
has_code_response_type = any(
120+
"code" in response_type.split(" ")
121+
for response_type in metadata.response_types_supported
122+
)
123+
if not has_code_response_type:
124+
errors.append(
125+
_create_error(AuthServerConfigErrorCode.CODE_RESPONSE_TYPE_NOT_SUPPORTED)
126+
)
127+
128+
# Check if 'authorization_code' grant type is supported
129+
if (
130+
not metadata.grant_types_supported
131+
or "authorization_code" not in metadata.grant_types_supported
132+
):
133+
errors.append(
134+
_create_error(
135+
AuthServerConfigErrorCode.AUTHORIZATION_CODE_GRANT_NOT_SUPPORTED
136+
)
137+
)
138+
139+
# Check PKCE support
140+
if not metadata.code_challenge_methods_supported:
141+
errors.append(_create_error(AuthServerConfigErrorCode.PKCE_NOT_SUPPORTED))
142+
elif "S256" not in metadata.code_challenge_methods_supported:
143+
errors.append(
144+
_create_error(
145+
AuthServerConfigErrorCode.S256_CODE_CHALLENGE_METHOD_NOT_SUPPORTED
146+
)
147+
)
148+
149+
# Check dynamic client registration support
150+
if not metadata.registration_endpoint:
151+
warnings.append(
152+
_create_warning(
153+
AuthServerConfigWarningCode.DYNAMIC_REGISTRATION_NOT_SUPPORTED
154+
)
155+
)
156+
157+
if len(errors) == 0:
158+
return AuthServerConfigValidationResult(
159+
is_valid=True, errors=[], warnings=warnings
160+
)
161+
else:
162+
return AuthServerConfigValidationResult(
163+
is_valid=False, errors=errors, warnings=warnings
164+
)
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
from mcpauth.models.auth_server import AuthServerConfig, AuthServerType
2+
from mcpauth.models.oauth import AuthorizationServerMetadata
3+
from mcpauth.utils.validate_server_config import (
4+
validate_server_config,
5+
AuthServerConfigErrorCode,
6+
AuthServerConfigWarningCode,
7+
)
8+
9+
10+
class TestValidateServerConfig:
11+
def test_valid_server_config(self):
12+
config = AuthServerConfig(
13+
type=AuthServerType.OAUTH,
14+
metadata=AuthorizationServerMetadata(
15+
issuer="https://example.com",
16+
authorization_endpoint="https://example.com/oauth/authorize",
17+
token_endpoint="https://example.com/oauth/token",
18+
response_types_supported=["code"],
19+
grant_types_supported=["authorization_code"],
20+
code_challenge_methods_supported=["S256"],
21+
registration_endpoint="https://example.com/register",
22+
),
23+
)
24+
25+
result = validate_server_config(config)
26+
assert result.is_valid is True
27+
assert not hasattr(result, "errors") or len(result.errors) == 0
28+
assert result.warnings == []
29+
30+
def test_invalid_server_config(self):
31+
config = AuthServerConfig(
32+
type=AuthServerType.OAUTH,
33+
metadata=AuthorizationServerMetadata(
34+
issuer="https://example.com",
35+
authorization_endpoint="https://example.com/oauth/authorize",
36+
token_endpoint="https://example.com/oauth/token",
37+
response_types_supported=["token"], # Invalid response type
38+
),
39+
)
40+
41+
result = validate_server_config(config)
42+
assert result.is_valid is False
43+
44+
error_codes = [error.code for error in result.errors]
45+
assert AuthServerConfigErrorCode.CODE_RESPONSE_TYPE_NOT_SUPPORTED in error_codes
46+
assert (
47+
AuthServerConfigErrorCode.AUTHORIZATION_CODE_GRANT_NOT_SUPPORTED
48+
in error_codes
49+
)
50+
assert AuthServerConfigErrorCode.PKCE_NOT_SUPPORTED in error_codes
51+
52+
warning_codes = [warning.code for warning in result.warnings]
53+
assert (
54+
AuthServerConfigWarningCode.DYNAMIC_REGISTRATION_NOT_SUPPORTED
55+
in warning_codes
56+
)
57+
58+
def test_warning_for_missing_dynamic_registration(self):
59+
config = AuthServerConfig(
60+
type=AuthServerType.OAUTH,
61+
metadata=AuthorizationServerMetadata(
62+
issuer="https://example.com",
63+
authorization_endpoint="https://example.com/oauth/authorize",
64+
token_endpoint="https://example.com/oauth/token",
65+
response_types_supported=["code"],
66+
grant_types_supported=["authorization_code"],
67+
code_challenge_methods_supported=["S256"],
68+
),
69+
)
70+
71+
result = validate_server_config(config)
72+
assert result.is_valid is True
73+
assert not hasattr(result, "errors") or len(result.errors) == 0
74+
75+
warning_codes = [warning.code for warning in result.warnings]
76+
assert (
77+
AuthServerConfigWarningCode.DYNAMIC_REGISTRATION_NOT_SUPPORTED
78+
in warning_codes
79+
)
80+
assert len(result.warnings) == 1
81+
82+
def test_code_challenge_methods(self):
83+
config = AuthServerConfig(
84+
type=AuthServerType.OAUTH,
85+
metadata=AuthorizationServerMetadata(
86+
issuer="https://example.com",
87+
authorization_endpoint="https://example.com/oauth/authorize",
88+
token_endpoint="https://example.com/oauth/token",
89+
response_types_supported=["code"],
90+
grant_types_supported=["authorization_code"],
91+
code_challenge_methods_supported=["plain"],
92+
),
93+
)
94+
95+
result = validate_server_config(config)
96+
assert result.is_valid is False
97+
98+
error_codes = [error.code for error in result.errors]
99+
assert (
100+
AuthServerConfigErrorCode.S256_CODE_CHALLENGE_METHOD_NOT_SUPPORTED
101+
in error_codes
102+
)

0 commit comments

Comments
 (0)