awslabs.openapi-mcp-server 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- awslabs/__init__.py +16 -0
- awslabs/openapi_mcp_server/__init__.py +69 -0
- awslabs/openapi_mcp_server/api/__init__.py +18 -0
- awslabs/openapi_mcp_server/api/config.py +200 -0
- awslabs/openapi_mcp_server/auth/__init__.py +27 -0
- awslabs/openapi_mcp_server/auth/api_key_auth.py +185 -0
- awslabs/openapi_mcp_server/auth/auth_cache.py +190 -0
- awslabs/openapi_mcp_server/auth/auth_errors.py +206 -0
- awslabs/openapi_mcp_server/auth/auth_factory.py +146 -0
- awslabs/openapi_mcp_server/auth/auth_protocol.py +63 -0
- awslabs/openapi_mcp_server/auth/auth_provider.py +160 -0
- awslabs/openapi_mcp_server/auth/base_auth.py +218 -0
- awslabs/openapi_mcp_server/auth/basic_auth.py +171 -0
- awslabs/openapi_mcp_server/auth/bearer_auth.py +108 -0
- awslabs/openapi_mcp_server/auth/cognito_auth.py +538 -0
- awslabs/openapi_mcp_server/auth/register.py +100 -0
- awslabs/openapi_mcp_server/patch/__init__.py +17 -0
- awslabs/openapi_mcp_server/prompts/__init__.py +18 -0
- awslabs/openapi_mcp_server/prompts/generators/__init__.py +22 -0
- awslabs/openapi_mcp_server/prompts/generators/operation_prompts.py +642 -0
- awslabs/openapi_mcp_server/prompts/generators/workflow_prompts.py +257 -0
- awslabs/openapi_mcp_server/prompts/models.py +70 -0
- awslabs/openapi_mcp_server/prompts/prompt_manager.py +150 -0
- awslabs/openapi_mcp_server/server.py +511 -0
- awslabs/openapi_mcp_server/utils/__init__.py +18 -0
- awslabs/openapi_mcp_server/utils/cache_provider.py +249 -0
- awslabs/openapi_mcp_server/utils/config.py +35 -0
- awslabs/openapi_mcp_server/utils/error_handler.py +349 -0
- awslabs/openapi_mcp_server/utils/http_client.py +263 -0
- awslabs/openapi_mcp_server/utils/metrics_provider.py +503 -0
- awslabs/openapi_mcp_server/utils/openapi.py +217 -0
- awslabs/openapi_mcp_server/utils/openapi_validator.py +253 -0
- awslabs_openapi_mcp_server-0.1.1.dist-info/METADATA +418 -0
- awslabs_openapi_mcp_server-0.1.1.dist-info/RECORD +38 -0
- awslabs_openapi_mcp_server-0.1.1.dist-info/WHEEL +4 -0
- awslabs_openapi_mcp_server-0.1.1.dist-info/entry_points.txt +2 -0
- awslabs_openapi_mcp_server-0.1.1.dist-info/licenses/LICENSE +175 -0
- awslabs_openapi_mcp_server-0.1.1.dist-info/licenses/NOTICE +2 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""Base authentication provider interface."""
|
|
15
|
+
|
|
16
|
+
import abc
|
|
17
|
+
import httpx
|
|
18
|
+
from typing import Any, Dict, Optional
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AuthProvider(abc.ABC):
|
|
22
|
+
"""Abstract base class for authentication providers.
|
|
23
|
+
|
|
24
|
+
Authentication providers handle different authentication methods for APIs.
|
|
25
|
+
Implementing classes must provide methods for setting up authentication
|
|
26
|
+
for HTTP requests.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
@abc.abstractmethod
|
|
30
|
+
def get_auth_headers(self) -> Dict[str, str]:
|
|
31
|
+
"""Get authentication headers for HTTP requests.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Dict[str, str]: Headers to include in HTTP requests
|
|
35
|
+
|
|
36
|
+
"""
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
@abc.abstractmethod
|
|
40
|
+
def get_auth_params(self) -> Dict[str, str]:
|
|
41
|
+
"""Get authentication query parameters for HTTP requests.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Dict[str, str]: Query parameters to include in HTTP requests
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
@abc.abstractmethod
|
|
50
|
+
def get_auth_cookies(self) -> Dict[str, str]:
|
|
51
|
+
"""Get authentication cookies for HTTP requests.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Dict[str, str]: Cookies to include in HTTP requests
|
|
55
|
+
|
|
56
|
+
"""
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
@abc.abstractmethod
|
|
60
|
+
def get_httpx_auth(self) -> Optional[httpx.Auth]:
|
|
61
|
+
"""Get authentication object for HTTPX.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Optional[httpx.Auth]: Authentication object for HTTPX client or None
|
|
65
|
+
|
|
66
|
+
"""
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
@abc.abstractmethod
|
|
70
|
+
def is_configured(self) -> bool:
|
|
71
|
+
"""Check if the authentication provider is properly configured.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
bool: True if configured, False otherwise
|
|
75
|
+
|
|
76
|
+
"""
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
@abc.abstractmethod
|
|
81
|
+
def provider_name(self) -> str:
|
|
82
|
+
"""Get the name of the authentication provider.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
str: Name of the authentication provider
|
|
86
|
+
|
|
87
|
+
"""
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class NullAuthProvider(AuthProvider):
|
|
92
|
+
"""No-op authentication provider.
|
|
93
|
+
|
|
94
|
+
This provider is used when authentication is disabled or not configured.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def __init__(self, config: Any = None):
|
|
98
|
+
"""Initialize with optional configuration.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
config: Optional configuration object (ignored by this provider)
|
|
102
|
+
|
|
103
|
+
"""
|
|
104
|
+
# Config is ignored by this provider
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
def get_auth_headers(self) -> Dict[str, str]:
|
|
108
|
+
"""Get authentication headers for HTTP requests.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Dict[str, str]: Empty dict as no authentication is provided
|
|
112
|
+
|
|
113
|
+
"""
|
|
114
|
+
return {}
|
|
115
|
+
|
|
116
|
+
def get_auth_params(self) -> Dict[str, str]:
|
|
117
|
+
"""Get authentication query parameters for HTTP requests.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Dict[str, str]: Empty dict as no authentication is provided
|
|
121
|
+
|
|
122
|
+
"""
|
|
123
|
+
return {}
|
|
124
|
+
|
|
125
|
+
def get_auth_cookies(self) -> Dict[str, str]:
|
|
126
|
+
"""Get authentication cookies for HTTP requests.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Dict[str, str]: Empty dict as no authentication is provided
|
|
130
|
+
|
|
131
|
+
"""
|
|
132
|
+
return {}
|
|
133
|
+
|
|
134
|
+
def get_httpx_auth(self) -> Optional[httpx.Auth]:
|
|
135
|
+
"""Get authentication object for HTTPX.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Optional[httpx.Auth]: None as no authentication is provided
|
|
139
|
+
|
|
140
|
+
"""
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
def is_configured(self) -> bool:
|
|
144
|
+
"""Check if the authentication provider is properly configured.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
bool: Always True as null provider requires no configuration
|
|
148
|
+
|
|
149
|
+
"""
|
|
150
|
+
return True
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def provider_name(self) -> str:
|
|
154
|
+
"""Get the name of the authentication provider.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
str: Name of the authentication provider
|
|
158
|
+
|
|
159
|
+
"""
|
|
160
|
+
return 'none'
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""Base authentication provider."""
|
|
15
|
+
|
|
16
|
+
import functools
|
|
17
|
+
import httpx
|
|
18
|
+
from abc import ABC, abstractmethod
|
|
19
|
+
from awslabs.openapi_mcp_server import logger
|
|
20
|
+
from awslabs.openapi_mcp_server.api.config import Config
|
|
21
|
+
from awslabs.openapi_mcp_server.auth.auth_errors import (
|
|
22
|
+
AuthError,
|
|
23
|
+
ConfigurationError,
|
|
24
|
+
format_error_message,
|
|
25
|
+
)
|
|
26
|
+
from awslabs.openapi_mcp_server.auth.auth_provider import AuthProvider
|
|
27
|
+
from typing import Any, Callable, Dict, Optional, TypeVar, cast
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Type variable for method return types
|
|
31
|
+
T = TypeVar('T')
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class BaseAuthProvider(AuthProvider, ABC):
|
|
35
|
+
"""Base authentication provider.
|
|
36
|
+
|
|
37
|
+
This abstract base class provides common functionality for all authentication providers.
|
|
38
|
+
It implements the Template Method pattern for configuration validation and error handling.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, config: Config):
|
|
42
|
+
"""Initialize with configuration.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
config: Application configuration
|
|
46
|
+
|
|
47
|
+
"""
|
|
48
|
+
self._config = config
|
|
49
|
+
self._is_valid = False
|
|
50
|
+
self._auth_headers: Dict[str, str] = {}
|
|
51
|
+
self._auth_params: Dict[str, str] = {}
|
|
52
|
+
self._auth_cookies: Dict[str, str] = {}
|
|
53
|
+
self._validation_error: Optional[AuthError] = None
|
|
54
|
+
|
|
55
|
+
# Template method pattern: validate and initialize
|
|
56
|
+
try:
|
|
57
|
+
self._is_valid = self._validate_config()
|
|
58
|
+
if self._is_valid:
|
|
59
|
+
self._initialize_auth()
|
|
60
|
+
else:
|
|
61
|
+
self._handle_validation_error()
|
|
62
|
+
except AuthError as e:
|
|
63
|
+
self._validation_error = e
|
|
64
|
+
self._is_valid = False
|
|
65
|
+
self._log_auth_error(e)
|
|
66
|
+
# Re-raise the exception for test cases to catch
|
|
67
|
+
raise e
|
|
68
|
+
except Exception as e:
|
|
69
|
+
self._validation_error = ConfigurationError(
|
|
70
|
+
f'Unexpected error during authentication provider initialization: {str(e)}'
|
|
71
|
+
)
|
|
72
|
+
self._is_valid = False
|
|
73
|
+
self._log_auth_error(self._validation_error)
|
|
74
|
+
# Re-raise the exception for test cases to catch
|
|
75
|
+
raise self._validation_error
|
|
76
|
+
|
|
77
|
+
def _initialize_auth(self) -> None:
|
|
78
|
+
"""Initialize authentication data after validation.
|
|
79
|
+
|
|
80
|
+
This method is called after successful validation to set up
|
|
81
|
+
headers, params, and cookies. Override in subclasses if needed.
|
|
82
|
+
"""
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
@abstractmethod
|
|
86
|
+
def _validate_config(self) -> bool:
|
|
87
|
+
"""Validate the configuration.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
bool: True if configuration is valid, False otherwise
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
AuthError: If validation fails with a specific error
|
|
94
|
+
|
|
95
|
+
"""
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
def _handle_validation_error(self) -> None:
|
|
99
|
+
"""Handle validation error.
|
|
100
|
+
|
|
101
|
+
This method is called when validation fails but no exception is raised.
|
|
102
|
+
It should create and log an appropriate error. Override in subclasses.
|
|
103
|
+
"""
|
|
104
|
+
self._validation_error = ConfigurationError(
|
|
105
|
+
f'Invalid configuration for {self.provider_name} authentication provider'
|
|
106
|
+
)
|
|
107
|
+
self._log_auth_error(self._validation_error)
|
|
108
|
+
|
|
109
|
+
def _log_auth_error(self, error: AuthError) -> None:
|
|
110
|
+
"""Log an authentication error.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
error: The authentication error
|
|
114
|
+
|
|
115
|
+
"""
|
|
116
|
+
message = format_error_message(self.provider_name, error.error_type, error.message)
|
|
117
|
+
logger.error(message)
|
|
118
|
+
|
|
119
|
+
# Log additional details at debug level
|
|
120
|
+
if error.details:
|
|
121
|
+
logger.debug(f'Error details: {error.details}')
|
|
122
|
+
|
|
123
|
+
def _log_validation_error(self) -> None:
|
|
124
|
+
"""Log validation error messages.
|
|
125
|
+
|
|
126
|
+
This method is kept for backward compatibility.
|
|
127
|
+
New implementations should use _handle_validation_error instead.
|
|
128
|
+
"""
|
|
129
|
+
self._handle_validation_error()
|
|
130
|
+
|
|
131
|
+
def _requires_valid_config(method: Callable[..., T]) -> Callable[..., T]: # type: ignore
|
|
132
|
+
"""Ensure a method is only called with valid configuration.
|
|
133
|
+
|
|
134
|
+
If the configuration is not valid, returns an empty result.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
@functools.wraps(method)
|
|
138
|
+
def wrapper(self: 'BaseAuthProvider', *args: Any, **kwargs: Any) -> T:
|
|
139
|
+
if not self._is_valid:
|
|
140
|
+
# Return empty result based on return type annotation
|
|
141
|
+
return_type = method.__annotations__.get('return')
|
|
142
|
+
if return_type == Dict[str, str]:
|
|
143
|
+
return cast(T, {})
|
|
144
|
+
elif return_type == Optional[httpx.Auth]:
|
|
145
|
+
return cast(T, None)
|
|
146
|
+
return cast(T, None)
|
|
147
|
+
return method(self, *args, **kwargs)
|
|
148
|
+
|
|
149
|
+
return wrapper
|
|
150
|
+
|
|
151
|
+
@_requires_valid_config
|
|
152
|
+
def get_auth_headers(self) -> Dict[str, str]:
|
|
153
|
+
"""Get authentication headers for HTTP requests.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Dict[str, str]: Authentication headers
|
|
157
|
+
|
|
158
|
+
"""
|
|
159
|
+
return self._auth_headers
|
|
160
|
+
|
|
161
|
+
@_requires_valid_config
|
|
162
|
+
def get_auth_params(self) -> Dict[str, str]:
|
|
163
|
+
"""Get authentication query parameters for HTTP requests.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Dict[str, str]: Authentication query parameters
|
|
167
|
+
|
|
168
|
+
"""
|
|
169
|
+
return self._auth_params
|
|
170
|
+
|
|
171
|
+
@_requires_valid_config
|
|
172
|
+
def get_auth_cookies(self) -> Dict[str, str]:
|
|
173
|
+
"""Get authentication cookies for HTTP requests.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Dict[str, str]: Authentication cookies
|
|
177
|
+
|
|
178
|
+
"""
|
|
179
|
+
return self._auth_cookies
|
|
180
|
+
|
|
181
|
+
@_requires_valid_config
|
|
182
|
+
def get_httpx_auth(self) -> Optional[httpx.Auth]:
|
|
183
|
+
"""Get authentication object for HTTPX.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Optional[httpx.Auth]: Authentication object for HTTPX client
|
|
187
|
+
|
|
188
|
+
"""
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
def is_configured(self) -> bool:
|
|
192
|
+
"""Check if the authentication provider is properly configured.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
bool: True if properly configured, False otherwise
|
|
196
|
+
|
|
197
|
+
"""
|
|
198
|
+
return self._is_valid
|
|
199
|
+
|
|
200
|
+
def get_validation_error(self) -> Optional[AuthError]:
|
|
201
|
+
"""Get the validation error if configuration is invalid.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Optional[AuthError]: The validation error or None if configuration is valid
|
|
205
|
+
|
|
206
|
+
"""
|
|
207
|
+
return self._validation_error
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
@abstractmethod
|
|
211
|
+
def provider_name(self) -> str:
|
|
212
|
+
"""Get the name of the authentication provider.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
str: Name of the authentication provider
|
|
216
|
+
|
|
217
|
+
"""
|
|
218
|
+
pass
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""Basic authentication provider."""
|
|
15
|
+
|
|
16
|
+
import base64
|
|
17
|
+
import httpx
|
|
18
|
+
from awslabs.openapi_mcp_server import logger
|
|
19
|
+
from awslabs.openapi_mcp_server.api.config import Config
|
|
20
|
+
from awslabs.openapi_mcp_server.auth.auth_cache import cached_auth_data
|
|
21
|
+
from awslabs.openapi_mcp_server.auth.auth_errors import MissingCredentialsError
|
|
22
|
+
from awslabs.openapi_mcp_server.auth.base_auth import BaseAuthProvider
|
|
23
|
+
from typing import Dict, Optional
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BasicAuthProvider(BaseAuthProvider):
|
|
27
|
+
"""Basic authentication provider.
|
|
28
|
+
|
|
29
|
+
This provider adds an Authorization header with Basic authentication
|
|
30
|
+
to all HTTP requests.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, config: Config):
|
|
34
|
+
"""Initialize with configuration.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
config: Application configuration
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
# Store credentials before calling super().__init__
|
|
41
|
+
self._username = config.auth_username
|
|
42
|
+
self._password = config.auth_password
|
|
43
|
+
self._httpx_auth: Optional[httpx.Auth] = None
|
|
44
|
+
self._credentials_hash = None
|
|
45
|
+
|
|
46
|
+
# Call parent initializer which will validate and initialize auth
|
|
47
|
+
super().__init__(config)
|
|
48
|
+
|
|
49
|
+
def _validate_config(self) -> bool:
|
|
50
|
+
"""Validate the configuration.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
bool: True if username and password are provided, False otherwise
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
MissingCredentialsError: If username or password is missing
|
|
57
|
+
|
|
58
|
+
"""
|
|
59
|
+
if not self._username:
|
|
60
|
+
raise MissingCredentialsError(
|
|
61
|
+
'Basic authentication requires a username',
|
|
62
|
+
{
|
|
63
|
+
'help': 'Provide a username using --auth-username command line argument or AUTH_USERNAME environment variable'
|
|
64
|
+
},
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if not self._password:
|
|
68
|
+
raise MissingCredentialsError(
|
|
69
|
+
'Basic authentication requires a password',
|
|
70
|
+
{
|
|
71
|
+
'help': 'Provide a password using --auth-password command line argument or AUTH_PASSWORD environment variable'
|
|
72
|
+
},
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Create a hash of the credentials for caching
|
|
76
|
+
self._credentials_hash = self._hash_credentials(self._username, self._password)
|
|
77
|
+
return True
|
|
78
|
+
|
|
79
|
+
def _log_validation_error(self) -> None:
|
|
80
|
+
"""Log validation error messages."""
|
|
81
|
+
logger.error(
|
|
82
|
+
'Basic authentication requires both username and password. Please provide them using --auth-username and --auth-password command line arguments or AUTH_USERNAME and AUTH_PASSWORD environment variables.'
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def _initialize_auth(self) -> None:
|
|
86
|
+
"""Initialize authentication data after validation."""
|
|
87
|
+
# Use cached methods to generate auth data
|
|
88
|
+
self._auth_headers = self._generate_auth_headers(self._credentials_hash)
|
|
89
|
+
self._httpx_auth = self._generate_httpx_auth(self._username, self._password)
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def _hash_credentials(username: str, password: str) -> str:
|
|
93
|
+
"""Create a hash of the credentials for caching.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
username: Username
|
|
97
|
+
password: Password
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
str: Hash of the credentials
|
|
101
|
+
|
|
102
|
+
"""
|
|
103
|
+
# Create a hash of the credentials to use as a cache key
|
|
104
|
+
# This avoids storing the actual credentials in the cache key
|
|
105
|
+
# Using bcrypt for stronger security
|
|
106
|
+
import bcrypt
|
|
107
|
+
|
|
108
|
+
credentials = f'{username}:{password}'
|
|
109
|
+
# Generate a salt and hash the credentials
|
|
110
|
+
# We only need a string representation for caching, so we'll use the hexdigest of the hash
|
|
111
|
+
hashed = bcrypt.hashpw(credentials.encode('utf-8'), bcrypt.gensalt(rounds=10))
|
|
112
|
+
# Convert to hex string for consistent cache key format
|
|
113
|
+
return hashed.hex()
|
|
114
|
+
|
|
115
|
+
@cached_auth_data(ttl=3600) # Cache for 1 hour by default
|
|
116
|
+
def _generate_auth_headers(self, credentials_hash: str) -> Dict[str, str]:
|
|
117
|
+
"""Generate authentication headers.
|
|
118
|
+
|
|
119
|
+
This method is cached to avoid regenerating headers for the same credentials.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
credentials_hash: Hash of the credentials
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Dict[str, str]: Authentication headers
|
|
126
|
+
|
|
127
|
+
"""
|
|
128
|
+
logger.debug(f'Generating new basic auth headers for user: {self._username}')
|
|
129
|
+
|
|
130
|
+
# Create the basic auth header
|
|
131
|
+
auth_string = f'{self._username}:{self._password}'
|
|
132
|
+
auth_bytes = auth_string.encode('utf-8')
|
|
133
|
+
encoded_auth = base64.b64encode(auth_bytes).decode('utf-8')
|
|
134
|
+
|
|
135
|
+
return {'Authorization': f'Basic {encoded_auth}'}
|
|
136
|
+
|
|
137
|
+
@cached_auth_data(ttl=3600) # Cache for 1 hour by default
|
|
138
|
+
def _generate_httpx_auth(self, username: str, password: str) -> httpx.BasicAuth:
|
|
139
|
+
"""Generate HTTPX auth object.
|
|
140
|
+
|
|
141
|
+
This method is cached to avoid regenerating auth objects for the same credentials.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
username: Username
|
|
145
|
+
password: Password
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
httpx.BasicAuth: HTTPX auth object
|
|
149
|
+
|
|
150
|
+
"""
|
|
151
|
+
logger.debug(f'Generating new HTTPX basic auth object for user: {username}')
|
|
152
|
+
return httpx.BasicAuth(username=username, password=password)
|
|
153
|
+
|
|
154
|
+
def get_httpx_auth(self) -> Optional[httpx.Auth]:
|
|
155
|
+
"""Get authentication object for HTTPX.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Optional[httpx.Auth]: Basic auth object for HTTPX client
|
|
159
|
+
|
|
160
|
+
"""
|
|
161
|
+
return self._httpx_auth
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def provider_name(self) -> str:
|
|
165
|
+
"""Get the name of the authentication provider.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
str: Name of the authentication provider
|
|
169
|
+
|
|
170
|
+
"""
|
|
171
|
+
return 'basic'
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""Bearer token authentication provider."""
|
|
15
|
+
|
|
16
|
+
from awslabs.openapi_mcp_server import logger
|
|
17
|
+
from awslabs.openapi_mcp_server.api.config import Config
|
|
18
|
+
from awslabs.openapi_mcp_server.auth.auth_cache import cached_auth_data
|
|
19
|
+
from awslabs.openapi_mcp_server.auth.auth_errors import MissingCredentialsError
|
|
20
|
+
from awslabs.openapi_mcp_server.auth.base_auth import BaseAuthProvider
|
|
21
|
+
from typing import Dict
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BearerAuthProvider(BaseAuthProvider):
|
|
25
|
+
"""Bearer token authentication provider.
|
|
26
|
+
|
|
27
|
+
This provider adds an Authorization header with a Bearer token
|
|
28
|
+
to all HTTP requests.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, config: Config):
|
|
32
|
+
"""Initialize with configuration.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
config: Application configuration
|
|
36
|
+
|
|
37
|
+
"""
|
|
38
|
+
# Store token before calling super().__init__
|
|
39
|
+
self._token = config.auth_token
|
|
40
|
+
self._token_ttl = getattr(config, 'auth_token_ttl', 3600) # Default 1 hour
|
|
41
|
+
|
|
42
|
+
# Call parent initializer which will validate and initialize auth
|
|
43
|
+
super().__init__(config)
|
|
44
|
+
|
|
45
|
+
def _validate_config(self) -> bool:
|
|
46
|
+
"""Validate the configuration.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
bool: True if token is provided, False otherwise
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
MissingCredentialsError: If token is missing
|
|
53
|
+
|
|
54
|
+
"""
|
|
55
|
+
if not self._token:
|
|
56
|
+
raise MissingCredentialsError(
|
|
57
|
+
'Bearer authentication requires a valid token',
|
|
58
|
+
{
|
|
59
|
+
'help': 'Provide a token using --auth-token command line argument or AUTH_TOKEN environment variable'
|
|
60
|
+
},
|
|
61
|
+
)
|
|
62
|
+
return True
|
|
63
|
+
|
|
64
|
+
def _log_validation_error(self) -> None:
|
|
65
|
+
"""Log validation error messages."""
|
|
66
|
+
logger.error(
|
|
67
|
+
'Bearer authentication requires a valid token. When using bearer authentication, a token must be provided.'
|
|
68
|
+
)
|
|
69
|
+
logger.error(
|
|
70
|
+
'Please provide a token using --auth-token command line argument or AUTH_TOKEN environment variable.'
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def _initialize_auth(self) -> None:
|
|
74
|
+
"""Initialize authentication data after validation."""
|
|
75
|
+
# We'll use the cached method to generate headers
|
|
76
|
+
self._auth_headers = self._generate_auth_headers(self._token)
|
|
77
|
+
|
|
78
|
+
@cached_auth_data(ttl=3600) # Cache for 1 hour by default
|
|
79
|
+
def _generate_auth_headers(self, token: str) -> Dict[str, str]:
|
|
80
|
+
"""Generate authentication headers.
|
|
81
|
+
|
|
82
|
+
This method is cached to avoid regenerating headers for the same token.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
token: Bearer token
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Dict[str, str]: Authentication headers
|
|
89
|
+
|
|
90
|
+
"""
|
|
91
|
+
# Log without including the token
|
|
92
|
+
logger.debug('Generating new bearer token headers')
|
|
93
|
+
|
|
94
|
+
# Calculate token length for debugging purposes
|
|
95
|
+
token_length = len(token) if token else 0
|
|
96
|
+
logger.debug(f'Token length: {token_length} characters')
|
|
97
|
+
|
|
98
|
+
return {'Authorization': f'Bearer {token}'}
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def provider_name(self) -> str:
|
|
102
|
+
"""Get the name of the authentication provider.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
str: Name of the authentication provider
|
|
106
|
+
|
|
107
|
+
"""
|
|
108
|
+
return 'bearer'
|