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,538 @@
|
|
|
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
|
+
"""Cognito User Pool authentication provider."""
|
|
15
|
+
|
|
16
|
+
import boto3
|
|
17
|
+
import threading
|
|
18
|
+
import time
|
|
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
|
+
ConfigurationError,
|
|
23
|
+
ExpiredTokenError,
|
|
24
|
+
InvalidCredentialsError,
|
|
25
|
+
MissingCredentialsError,
|
|
26
|
+
NetworkError,
|
|
27
|
+
)
|
|
28
|
+
from awslabs.openapi_mcp_server.auth.bearer_auth import BearerAuthProvider
|
|
29
|
+
from typing import Dict, Optional
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CognitoAuthProvider(BearerAuthProvider):
|
|
33
|
+
"""Cognito User Pool authentication provider.
|
|
34
|
+
|
|
35
|
+
This provider obtains ID tokens from AWS Cognito User Pools
|
|
36
|
+
and delegates to BearerAuthProvider for adding Authorization headers
|
|
37
|
+
to all HTTP requests.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, config: Config):
|
|
41
|
+
"""Initialize with configuration.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
config: Application configuration
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
# Store Cognito-specific configuration
|
|
48
|
+
self._client_id = config.auth_cognito_client_id
|
|
49
|
+
self._username = config.auth_cognito_username
|
|
50
|
+
self._password = config.auth_cognito_password
|
|
51
|
+
self._user_pool_id = config.auth_cognito_user_pool_id
|
|
52
|
+
self._region = config.auth_cognito_region
|
|
53
|
+
|
|
54
|
+
# Add debug log early in initialization
|
|
55
|
+
logger.debug(
|
|
56
|
+
f'Cognito auth configuration: Username={self._username}, ClientID={self._client_id}, '
|
|
57
|
+
f'Password={"SET" if self._password else "NOT SET"}, UserPoolID={self._user_pool_id or "NOT SET"}'
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Token management
|
|
61
|
+
self._token_expires_at = 0
|
|
62
|
+
self._refresh_token_value = None
|
|
63
|
+
self._token_lock = threading.RLock() # For thread safety
|
|
64
|
+
|
|
65
|
+
# Get initial token before parent initialization
|
|
66
|
+
try:
|
|
67
|
+
# Only try to get token if we have the minimum required credentials
|
|
68
|
+
if self._client_id and self._username and self._password:
|
|
69
|
+
token = self._get_cognito_token()
|
|
70
|
+
if token:
|
|
71
|
+
# Set token in config for parent class to use
|
|
72
|
+
config.auth_token = token
|
|
73
|
+
else:
|
|
74
|
+
logger.warning(
|
|
75
|
+
'Missing required Cognito credentials, skipping initial token acquisition'
|
|
76
|
+
)
|
|
77
|
+
except Exception as e:
|
|
78
|
+
logger.warning(f'Failed to get initial Cognito token: {e}')
|
|
79
|
+
# We'll let the parent validation handle this error
|
|
80
|
+
|
|
81
|
+
# Call parent initializer which will validate and initialize auth
|
|
82
|
+
# This will set self._token from config.auth_token
|
|
83
|
+
super().__init__(config)
|
|
84
|
+
|
|
85
|
+
def _validate_config(self) -> bool:
|
|
86
|
+
"""Validate the configuration.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
bool: True if all required parameters are provided, False otherwise
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
MissingCredentialsError: If required parameters are missing
|
|
93
|
+
ConfigurationError: If configuration is invalid
|
|
94
|
+
|
|
95
|
+
"""
|
|
96
|
+
# Validate required parameters
|
|
97
|
+
if not self._client_id:
|
|
98
|
+
raise MissingCredentialsError(
|
|
99
|
+
'Cognito authentication requires a client ID',
|
|
100
|
+
{
|
|
101
|
+
'help': 'Provide client ID using --auth-cognito-client-id command line argument or AUTH_COGNITO_CLIENT_ID environment variable'
|
|
102
|
+
},
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if not self._username:
|
|
106
|
+
raise MissingCredentialsError(
|
|
107
|
+
'Cognito authentication requires a username',
|
|
108
|
+
{
|
|
109
|
+
'help': 'Provide username using --auth-cognito-username command line argument or AUTH_COGNITO_USERNAME environment variable'
|
|
110
|
+
},
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if not self._password:
|
|
114
|
+
raise MissingCredentialsError(
|
|
115
|
+
'Cognito authentication requires a password',
|
|
116
|
+
{
|
|
117
|
+
'help': 'Provide password using --auth-cognito-password command line argument or AUTH_COGNITO_PASSWORD environment variable'
|
|
118
|
+
},
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Let parent class validate the token
|
|
122
|
+
return super()._validate_config()
|
|
123
|
+
|
|
124
|
+
def _log_validation_error(self) -> None:
|
|
125
|
+
"""Log validation error messages."""
|
|
126
|
+
logger.error('Cognito authentication requires client ID, username, and password.')
|
|
127
|
+
logger.error(
|
|
128
|
+
'Please provide client ID using --auth-cognito-client-id, username using --auth-cognito-username, '
|
|
129
|
+
'and password using --auth-cognito-password command line arguments or corresponding environment variables.'
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def get_auth_headers(self) -> Dict[str, str]:
|
|
133
|
+
"""Get authentication headers with auto-refresh.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Dict[str, str]: Authentication headers
|
|
137
|
+
|
|
138
|
+
"""
|
|
139
|
+
# Check if token needs refreshing and refresh if necessary
|
|
140
|
+
self._check_and_refresh_token_if_needed()
|
|
141
|
+
|
|
142
|
+
# Delegate to parent class for header generation
|
|
143
|
+
return super().get_auth_headers()
|
|
144
|
+
|
|
145
|
+
def _check_and_refresh_token_if_needed(self) -> None:
|
|
146
|
+
"""Check if token needs refreshing and refresh if necessary."""
|
|
147
|
+
with self._token_lock:
|
|
148
|
+
if self._is_token_expired_or_expiring_soon():
|
|
149
|
+
self._refresh_token()
|
|
150
|
+
|
|
151
|
+
def _is_token_expired_or_expiring_soon(self) -> bool:
|
|
152
|
+
"""Check if token is expired or will expire soon.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
bool: True if token is expired or will expire soon, False otherwise
|
|
156
|
+
|
|
157
|
+
"""
|
|
158
|
+
# Add buffer time (5 minutes) to refresh before actual expiration
|
|
159
|
+
buffer_seconds = 300
|
|
160
|
+
return time.time() + buffer_seconds >= self._token_expires_at
|
|
161
|
+
|
|
162
|
+
def _refresh_token(self) -> None:
|
|
163
|
+
"""Refresh the token if possible, or re-authenticate.
|
|
164
|
+
|
|
165
|
+
Logs at INFO level when token is refreshed.
|
|
166
|
+
"""
|
|
167
|
+
try:
|
|
168
|
+
old_token = self._token
|
|
169
|
+
new_token = None
|
|
170
|
+
|
|
171
|
+
# Try using refresh token if available
|
|
172
|
+
if self._refresh_token_value:
|
|
173
|
+
logger.debug(f'Attempting to refresh Cognito token for user: {self._username}')
|
|
174
|
+
new_token = self._refresh_cognito_token()
|
|
175
|
+
|
|
176
|
+
# If refresh failed or no refresh token available, re-authenticate
|
|
177
|
+
if not new_token:
|
|
178
|
+
logger.debug(f'Re-authenticating Cognito user: {self._username}')
|
|
179
|
+
new_token = self._get_cognito_token()
|
|
180
|
+
|
|
181
|
+
# Update token if we got a new one
|
|
182
|
+
if new_token and new_token != old_token:
|
|
183
|
+
self._token = new_token
|
|
184
|
+
logger.info(f'Cognito token refreshed for user: {self._username}')
|
|
185
|
+
|
|
186
|
+
# Force parent class to regenerate auth headers with new token
|
|
187
|
+
self._initialize_auth()
|
|
188
|
+
else:
|
|
189
|
+
logger.debug('Token refresh did not result in a new token')
|
|
190
|
+
|
|
191
|
+
except Exception as e:
|
|
192
|
+
logger.error(f'Failed to refresh token: {e}')
|
|
193
|
+
raise ExpiredTokenError('Token refresh failed', {'error': str(e)})
|
|
194
|
+
|
|
195
|
+
def _get_cognito_token(self) -> Optional[str]:
|
|
196
|
+
"""Get a new token from Cognito using username/password.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
str: Cognito ID token or None if authentication fails
|
|
200
|
+
|
|
201
|
+
Raises:
|
|
202
|
+
AuthenticationError: If authentication fails
|
|
203
|
+
|
|
204
|
+
"""
|
|
205
|
+
client = boto3.client('cognito-idp', region_name=self._region)
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
logger.debug(f'Authenticating with Cognito for user: {self._username}')
|
|
209
|
+
|
|
210
|
+
# Log parameters for debugging (without sensitive info)
|
|
211
|
+
logger.debug(f'Initiating auth with ClientId: {self._client_id}')
|
|
212
|
+
logger.debug('AuthFlow: USER_PASSWORD_AUTH')
|
|
213
|
+
logger.debug(f'USERNAME parameter provided: {self._username}')
|
|
214
|
+
logger.debug(
|
|
215
|
+
f'PASSWORD parameter provided: {"*" * (len(self._password) if self._password else 0)}'
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# Add clear confirmation of required variables
|
|
219
|
+
logger.debug(
|
|
220
|
+
f'Cognito auth configuration: Username={self._username}, ClientID={self._client_id}, Password={"SET" if self._password else "NOT SET"}'
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Try with different parameter formats
|
|
224
|
+
# Format 1: Standard format
|
|
225
|
+
auth_params = {'USERNAME': self._username, 'PASSWORD': self._password}
|
|
226
|
+
|
|
227
|
+
# Add user pool ID if provided (some configurations might require this)
|
|
228
|
+
if self._user_pool_id:
|
|
229
|
+
logger.debug(f'User pool ID provided: {self._user_pool_id}')
|
|
230
|
+
# Some Cognito configurations might use this format
|
|
231
|
+
auth_params['UserPoolId'] = self._user_pool_id
|
|
232
|
+
|
|
233
|
+
# Try with USER_PASSWORD_AUTH flow first
|
|
234
|
+
try:
|
|
235
|
+
logger.debug('Trying USER_PASSWORD_AUTH flow')
|
|
236
|
+
response = client.initiate_auth(
|
|
237
|
+
ClientId=self._client_id,
|
|
238
|
+
AuthFlow='USER_PASSWORD_AUTH',
|
|
239
|
+
AuthParameters=auth_params,
|
|
240
|
+
)
|
|
241
|
+
except client.exceptions.InvalidParameterException:
|
|
242
|
+
# If USER_PASSWORD_AUTH fails, try ADMIN_USER_PASSWORD_AUTH flow
|
|
243
|
+
# This requires user pool ID
|
|
244
|
+
if self._user_pool_id:
|
|
245
|
+
logger.debug('USER_PASSWORD_AUTH failed, trying ADMIN_USER_PASSWORD_AUTH flow')
|
|
246
|
+
logger.debug(f'Using user pool ID: {self._user_pool_id}')
|
|
247
|
+
|
|
248
|
+
# ADMIN_USER_PASSWORD_AUTH requires admin credentials
|
|
249
|
+
# This will use the AWS credentials from the environment
|
|
250
|
+
response = client.admin_initiate_auth(
|
|
251
|
+
UserPoolId=self._user_pool_id,
|
|
252
|
+
ClientId=self._client_id,
|
|
253
|
+
AuthFlow='ADMIN_USER_PASSWORD_AUTH',
|
|
254
|
+
AuthParameters={'USERNAME': self._username, 'PASSWORD': self._password},
|
|
255
|
+
)
|
|
256
|
+
else:
|
|
257
|
+
# Re-raise the original exception if we can't try ADMIN_USER_PASSWORD_AUTH
|
|
258
|
+
logger.error(
|
|
259
|
+
'USER_PASSWORD_AUTH failed and no user pool ID provided for ADMIN_USER_PASSWORD_AUTH'
|
|
260
|
+
)
|
|
261
|
+
raise
|
|
262
|
+
|
|
263
|
+
auth_result = response.get('AuthenticationResult', {})
|
|
264
|
+
|
|
265
|
+
# Store the refresh token
|
|
266
|
+
self._refresh_token_value = auth_result.get('RefreshToken')
|
|
267
|
+
|
|
268
|
+
# Extract token expiry from ID token
|
|
269
|
+
id_token = auth_result.get('IdToken')
|
|
270
|
+
if id_token:
|
|
271
|
+
self._token_expires_at = self._extract_token_expiry(id_token)
|
|
272
|
+
|
|
273
|
+
# Get the ID token
|
|
274
|
+
id_token = auth_result.get('IdToken')
|
|
275
|
+
if id_token:
|
|
276
|
+
# Extract token expiry
|
|
277
|
+
self._token_expires_at = self._extract_token_expiry(id_token)
|
|
278
|
+
|
|
279
|
+
# Log token acquisition at INFO level
|
|
280
|
+
logger.info(f'Obtained new Cognito ID token for user: {self._username}')
|
|
281
|
+
|
|
282
|
+
# Log token length for debugging
|
|
283
|
+
token_length = len(id_token) if id_token else 0
|
|
284
|
+
logger.debug(f'Token length: {token_length} characters')
|
|
285
|
+
|
|
286
|
+
return id_token
|
|
287
|
+
else:
|
|
288
|
+
logger.error('No ID token found in authentication result')
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
except client.exceptions.NotAuthorizedException as e:
|
|
292
|
+
logger.error(f'Authentication failed: {e}')
|
|
293
|
+
logger.error('Please check your Cognito credentials (client ID, username, password)')
|
|
294
|
+
logger.error(
|
|
295
|
+
'Make sure the user exists in the Cognito User Pool and the password is correct'
|
|
296
|
+
)
|
|
297
|
+
raise InvalidCredentialsError(
|
|
298
|
+
'Invalid Cognito credentials',
|
|
299
|
+
{
|
|
300
|
+
'error': str(e),
|
|
301
|
+
'help': 'Check your Cognito credentials and ensure the user exists in the User Pool',
|
|
302
|
+
},
|
|
303
|
+
)
|
|
304
|
+
except client.exceptions.UserNotConfirmedException as e:
|
|
305
|
+
logger.error(f'User not confirmed: {e}')
|
|
306
|
+
logger.error('The user exists but has not been confirmed in the Cognito User Pool')
|
|
307
|
+
logger.error(
|
|
308
|
+
'Please confirm the user in the AWS Console or use the AWS CLI to confirm the user'
|
|
309
|
+
)
|
|
310
|
+
raise ConfigurationError(
|
|
311
|
+
'User not confirmed',
|
|
312
|
+
{
|
|
313
|
+
'error': str(e),
|
|
314
|
+
'help': 'Confirm the user in the AWS Console or use the AWS CLI',
|
|
315
|
+
},
|
|
316
|
+
)
|
|
317
|
+
except client.exceptions.InvalidParameterException as e:
|
|
318
|
+
logger.error(f'Invalid parameter: {e}')
|
|
319
|
+
# Check if the error message contains information about which parameter is missing
|
|
320
|
+
error_msg = str(e)
|
|
321
|
+
if 'Missing required parameter' in error_msg:
|
|
322
|
+
logger.error('Missing required parameter for Cognito authentication')
|
|
323
|
+
logger.error(f'Client ID: {self._client_id}')
|
|
324
|
+
logger.error(f'Username provided: {bool(self._username)}')
|
|
325
|
+
logger.error(f'Password provided: {bool(self._password)}')
|
|
326
|
+
logger.error(f'User Pool ID provided: {bool(self._user_pool_id)}')
|
|
327
|
+
|
|
328
|
+
# Check specific parameters
|
|
329
|
+
if not self._client_id:
|
|
330
|
+
raise MissingCredentialsError(
|
|
331
|
+
'Missing Cognito client ID',
|
|
332
|
+
{
|
|
333
|
+
'error': error_msg,
|
|
334
|
+
'help': 'Provide client ID using --auth-cognito-client-id or AUTH_COGNITO_CLIENT_ID',
|
|
335
|
+
},
|
|
336
|
+
)
|
|
337
|
+
elif not self._username:
|
|
338
|
+
raise MissingCredentialsError(
|
|
339
|
+
'Missing Cognito username',
|
|
340
|
+
{
|
|
341
|
+
'error': error_msg,
|
|
342
|
+
'help': 'Provide username using --auth-cognito-username or AUTH_COGNITO_USERNAME',
|
|
343
|
+
},
|
|
344
|
+
)
|
|
345
|
+
elif not self._password:
|
|
346
|
+
raise MissingCredentialsError(
|
|
347
|
+
'Missing Cognito password',
|
|
348
|
+
{
|
|
349
|
+
'error': error_msg,
|
|
350
|
+
'help': 'Provide password using --auth-cognito-password or AUTH_COGNITO_PASSWORD',
|
|
351
|
+
},
|
|
352
|
+
)
|
|
353
|
+
elif not self._user_pool_id:
|
|
354
|
+
logger.error('User Pool ID might be required for this Cognito configuration')
|
|
355
|
+
raise ConfigurationError(
|
|
356
|
+
'Missing User Pool ID for Cognito authentication',
|
|
357
|
+
{
|
|
358
|
+
'error': error_msg,
|
|
359
|
+
'help': 'Provide User Pool ID using --auth-cognito-user-pool-id or AUTH_COGNITO_USER_POOL_ID',
|
|
360
|
+
},
|
|
361
|
+
)
|
|
362
|
+
else:
|
|
363
|
+
raise ConfigurationError(
|
|
364
|
+
'Missing required parameter for Cognito authentication',
|
|
365
|
+
{
|
|
366
|
+
'error': error_msg,
|
|
367
|
+
'help': 'Check the error message for details on which parameter is missing',
|
|
368
|
+
},
|
|
369
|
+
)
|
|
370
|
+
else:
|
|
371
|
+
raise ConfigurationError(
|
|
372
|
+
f'Invalid parameter for Cognito authentication: {error_msg}',
|
|
373
|
+
{
|
|
374
|
+
'error': error_msg,
|
|
375
|
+
'help': 'Check the error message for details on which parameter is invalid',
|
|
376
|
+
},
|
|
377
|
+
)
|
|
378
|
+
except client.exceptions.ResourceNotFoundException as e:
|
|
379
|
+
logger.error(f'Resource not found: {e}')
|
|
380
|
+
logger.error('The specified Cognito User Pool or Client ID does not exist')
|
|
381
|
+
raise ConfigurationError(
|
|
382
|
+
'Cognito resource not found',
|
|
383
|
+
{'error': str(e), 'help': 'Check your User Pool ID and Client ID'},
|
|
384
|
+
)
|
|
385
|
+
except Exception as e:
|
|
386
|
+
logger.error(f'Cognito authentication error: {e}')
|
|
387
|
+
logger.error(
|
|
388
|
+
'This could be due to network issues, AWS credentials, or Cognito configuration'
|
|
389
|
+
)
|
|
390
|
+
raise NetworkError(
|
|
391
|
+
'Cognito authentication failed',
|
|
392
|
+
{'error': str(e), 'help': 'Check your network connection and AWS credentials'},
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
def _refresh_cognito_token(self) -> Optional[str]:
|
|
396
|
+
"""Refresh the Cognito token using the refresh token.
|
|
397
|
+
|
|
398
|
+
Returns:
|
|
399
|
+
str: New Cognito ID token or None if refresh fails
|
|
400
|
+
|
|
401
|
+
Raises:
|
|
402
|
+
AuthenticationError: If token refresh fails
|
|
403
|
+
|
|
404
|
+
"""
|
|
405
|
+
client = boto3.client('cognito-idp', region_name=self._region)
|
|
406
|
+
|
|
407
|
+
try:
|
|
408
|
+
logger.debug(f'Refreshing token for user: {self._username}')
|
|
409
|
+
|
|
410
|
+
# Try with standard REFRESH_TOKEN_AUTH flow first
|
|
411
|
+
try:
|
|
412
|
+
logger.debug('Trying REFRESH_TOKEN_AUTH flow')
|
|
413
|
+
response = client.initiate_auth(
|
|
414
|
+
ClientId=self._client_id,
|
|
415
|
+
AuthFlow='REFRESH_TOKEN_AUTH',
|
|
416
|
+
AuthParameters={'REFRESH_TOKEN': self._refresh_token_value},
|
|
417
|
+
)
|
|
418
|
+
except client.exceptions.InvalidParameterException:
|
|
419
|
+
# If REFRESH_TOKEN_AUTH fails, try ADMIN_REFRESH_TOKEN_AUTH flow
|
|
420
|
+
# This requires user pool ID
|
|
421
|
+
if self._user_pool_id:
|
|
422
|
+
logger.debug('REFRESH_TOKEN_AUTH failed, trying ADMIN_REFRESH_TOKEN_AUTH flow')
|
|
423
|
+
logger.debug(f'Using user pool ID: {self._user_pool_id}')
|
|
424
|
+
|
|
425
|
+
# ADMIN_REFRESH_TOKEN_AUTH requires admin credentials
|
|
426
|
+
# This will use the AWS credentials from the environment
|
|
427
|
+
response = client.admin_initiate_auth(
|
|
428
|
+
UserPoolId=self._user_pool_id,
|
|
429
|
+
ClientId=self._client_id,
|
|
430
|
+
AuthFlow='REFRESH_TOKEN',
|
|
431
|
+
AuthParameters={'REFRESH_TOKEN': self._refresh_token_value},
|
|
432
|
+
)
|
|
433
|
+
else:
|
|
434
|
+
# Re-raise the original exception if we can't try ADMIN_REFRESH_TOKEN_AUTH
|
|
435
|
+
logger.error(
|
|
436
|
+
'REFRESH_TOKEN_AUTH failed and no user pool ID provided for ADMIN_REFRESH_TOKEN_AUTH'
|
|
437
|
+
)
|
|
438
|
+
raise
|
|
439
|
+
|
|
440
|
+
auth_result = response.get('AuthenticationResult', {})
|
|
441
|
+
|
|
442
|
+
# Extract token expiry from ID token
|
|
443
|
+
id_token = auth_result.get('IdToken')
|
|
444
|
+
if id_token:
|
|
445
|
+
self._token_expires_at = self._extract_token_expiry(id_token)
|
|
446
|
+
|
|
447
|
+
# Get the ID token
|
|
448
|
+
id_token = auth_result.get('IdToken')
|
|
449
|
+
if id_token:
|
|
450
|
+
# Extract token expiry
|
|
451
|
+
self._token_expires_at = self._extract_token_expiry(id_token)
|
|
452
|
+
|
|
453
|
+
# Log token refresh at INFO level
|
|
454
|
+
logger.info(f'Successfully refreshed Cognito ID token for user: {self._username}')
|
|
455
|
+
|
|
456
|
+
# Log token length for debugging
|
|
457
|
+
token_length = len(id_token) if id_token else 0
|
|
458
|
+
logger.debug(f'Token length: {token_length} characters')
|
|
459
|
+
|
|
460
|
+
return id_token
|
|
461
|
+
else:
|
|
462
|
+
logger.error('No ID token found in refresh result')
|
|
463
|
+
return None
|
|
464
|
+
|
|
465
|
+
except client.exceptions.NotAuthorizedException:
|
|
466
|
+
logger.warning('Refresh token expired, falling back to re-authentication')
|
|
467
|
+
return None # Will trigger a full re-authentication
|
|
468
|
+
except Exception as e:
|
|
469
|
+
logger.error(f'Token refresh error: {e}')
|
|
470
|
+
return None # Will trigger a full re-authentication
|
|
471
|
+
|
|
472
|
+
def _extract_token_expiry(self, token: str) -> int:
|
|
473
|
+
"""Extract expiry timestamp from token.
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
token: JWT token
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
int: Expiry timestamp
|
|
480
|
+
|
|
481
|
+
"""
|
|
482
|
+
try:
|
|
483
|
+
# Parse the JWT token without using the decode function
|
|
484
|
+
# JWT tokens are in the format: header.payload.signature
|
|
485
|
+
# We only need the payload part to extract the expiry
|
|
486
|
+
parts = token.split('.')
|
|
487
|
+
if len(parts) != 3:
|
|
488
|
+
raise ValueError('Invalid JWT token format')
|
|
489
|
+
|
|
490
|
+
# The payload is base64url encoded
|
|
491
|
+
# Add padding if needed
|
|
492
|
+
payload = parts[1]
|
|
493
|
+
padding = '=' * ((4 - len(payload) % 4) % 4) # Fix padding calculation
|
|
494
|
+
|
|
495
|
+
# Replace URL-safe characters and decode
|
|
496
|
+
payload = payload.replace('-', '+').replace('_', '/') + padding
|
|
497
|
+
|
|
498
|
+
try:
|
|
499
|
+
import base64
|
|
500
|
+
|
|
501
|
+
decoded_payload = base64.b64decode(payload).decode('utf-8')
|
|
502
|
+
import json
|
|
503
|
+
|
|
504
|
+
payload_data = json.loads(decoded_payload)
|
|
505
|
+
exp_time = payload_data.get('exp', 0)
|
|
506
|
+
|
|
507
|
+
# Log the expiry duration at INFO level
|
|
508
|
+
if exp_time > 0:
|
|
509
|
+
current_time = int(time.time())
|
|
510
|
+
duration_seconds = exp_time - current_time
|
|
511
|
+
duration_minutes = duration_seconds / 60
|
|
512
|
+
duration_hours = duration_minutes / 60
|
|
513
|
+
|
|
514
|
+
if duration_seconds > 0:
|
|
515
|
+
logger.info(
|
|
516
|
+
f'Token expires in {duration_hours:.2f} hours ({duration_minutes:.0f} minutes)'
|
|
517
|
+
)
|
|
518
|
+
else:
|
|
519
|
+
logger.info(f'Token is already expired by {-duration_seconds} seconds')
|
|
520
|
+
|
|
521
|
+
return exp_time
|
|
522
|
+
except Exception as e:
|
|
523
|
+
logger.warning(f'Failed to decode payload: {e}')
|
|
524
|
+
raise
|
|
525
|
+
except Exception as e:
|
|
526
|
+
logger.warning(f'Failed to extract token expiry: {e}')
|
|
527
|
+
# Default to 1 hour from now if extraction fails
|
|
528
|
+
return int(time.time()) + 3600
|
|
529
|
+
|
|
530
|
+
@property
|
|
531
|
+
def provider_name(self) -> str:
|
|
532
|
+
"""Get the name of the authentication provider.
|
|
533
|
+
|
|
534
|
+
Returns:
|
|
535
|
+
str: Name of the authentication provider
|
|
536
|
+
|
|
537
|
+
"""
|
|
538
|
+
return 'cognito'
|
|
@@ -0,0 +1,100 @@
|
|
|
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
|
+
"""Register authentication providers."""
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
from awslabs.openapi_mcp_server import logger
|
|
18
|
+
from awslabs.openapi_mcp_server.auth.auth_factory import register_auth_provider
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def register_auth_providers() -> None:
|
|
22
|
+
"""Register authentication providers based on configuration.
|
|
23
|
+
|
|
24
|
+
This function registers only the authentication provider that is specified
|
|
25
|
+
by the AUTH_TYPE environment variable or command-line argument.
|
|
26
|
+
If no auth type is specified, it registers all available providers.
|
|
27
|
+
"""
|
|
28
|
+
# Get the auth type from environment variable
|
|
29
|
+
auth_type = os.environ.get('AUTH_TYPE', '').lower()
|
|
30
|
+
|
|
31
|
+
# If no auth type is specified in the environment, register all providers
|
|
32
|
+
if not auth_type:
|
|
33
|
+
logger.debug('No auth type specified in environment, registering all providers')
|
|
34
|
+
register_all_providers()
|
|
35
|
+
else:
|
|
36
|
+
# Register only the specified provider
|
|
37
|
+
register_provider_by_type(auth_type)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def register_provider_by_type(auth_type: str) -> None:
|
|
41
|
+
"""Register a specific authentication provider by type.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
auth_type: The type of authentication provider to register
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
if auth_type == 'bearer':
|
|
48
|
+
from awslabs.openapi_mcp_server.auth.bearer_auth import BearerAuthProvider
|
|
49
|
+
|
|
50
|
+
register_auth_provider('bearer', BearerAuthProvider)
|
|
51
|
+
logger.info('Registered Bearer authentication provider')
|
|
52
|
+
elif auth_type == 'basic':
|
|
53
|
+
from awslabs.openapi_mcp_server.auth.basic_auth import BasicAuthProvider
|
|
54
|
+
|
|
55
|
+
register_auth_provider('basic', BasicAuthProvider)
|
|
56
|
+
logger.info('Registered Basic authentication provider')
|
|
57
|
+
elif auth_type == 'api_key':
|
|
58
|
+
from awslabs.openapi_mcp_server.auth.api_key_auth import ApiKeyAuthProvider
|
|
59
|
+
|
|
60
|
+
register_auth_provider('api_key', ApiKeyAuthProvider)
|
|
61
|
+
logger.info('Registered Api_Key authentication provider')
|
|
62
|
+
elif auth_type == 'cognito':
|
|
63
|
+
from awslabs.openapi_mcp_server.auth.cognito_auth import CognitoAuthProvider
|
|
64
|
+
|
|
65
|
+
register_auth_provider('cognito', CognitoAuthProvider)
|
|
66
|
+
logger.info('Registered Cognito authentication provider')
|
|
67
|
+
else:
|
|
68
|
+
logger.warning(f'Unknown auth type: {auth_type}, registering all providers')
|
|
69
|
+
register_all_providers()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def register_all_providers() -> None:
|
|
73
|
+
"""Register all available authentication providers."""
|
|
74
|
+
# Import all provider classes
|
|
75
|
+
from awslabs.openapi_mcp_server.auth.api_key_auth import ApiKeyAuthProvider
|
|
76
|
+
from awslabs.openapi_mcp_server.auth.basic_auth import BasicAuthProvider
|
|
77
|
+
from awslabs.openapi_mcp_server.auth.bearer_auth import BearerAuthProvider
|
|
78
|
+
|
|
79
|
+
# Register the standard providers
|
|
80
|
+
register_auth_provider('bearer', BearerAuthProvider)
|
|
81
|
+
logger.info('Registered Bearer authentication provider')
|
|
82
|
+
|
|
83
|
+
register_auth_provider('basic', BasicAuthProvider)
|
|
84
|
+
logger.info('Registered Basic authentication provider')
|
|
85
|
+
|
|
86
|
+
register_auth_provider('api_key', ApiKeyAuthProvider)
|
|
87
|
+
logger.info('Registered Api_Key authentication provider')
|
|
88
|
+
|
|
89
|
+
# Only register Cognito if it's available
|
|
90
|
+
try:
|
|
91
|
+
from awslabs.openapi_mcp_server.auth.cognito_auth import CognitoAuthProvider
|
|
92
|
+
|
|
93
|
+
register_auth_provider('cognito', CognitoAuthProvider)
|
|
94
|
+
logger.info('Registered Cognito authentication provider')
|
|
95
|
+
except ImportError:
|
|
96
|
+
logger.debug('Cognito authentication provider not available')
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# Don't register providers automatically when this module is imported
|
|
100
|
+
# This will be done explicitly in server.py
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
"""Patch module for OpenAPI MCP Server."""
|
|
15
|
+
|
|
16
|
+
# This package contains patches to improve the functionality of the OpenAPI MCP Server
|
|
17
|
+
# and other third-party libraries it depends on.
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
"""MCP prompt generation for OpenAPI specifications."""
|
|
15
|
+
|
|
16
|
+
from awslabs.openapi_mcp_server.prompts.prompt_manager import MCPPromptManager
|
|
17
|
+
|
|
18
|
+
__all__ = ['MCPPromptManager']
|