Skip to content

Commit 79dca9a

Browse files
committed
working version of user and agent auth
1 parent 1745f59 commit 79dca9a

File tree

8 files changed

+157
-39
lines changed

8 files changed

+157
-39
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ celerybeat.pid
132132

133133
# Environments
134134
.env
135+
.env.user
136+
.env.agent
135137
.env.docker
136138
.venv
137139
env/

agents/.env.template

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,12 @@ COGNITO_CLIENT_SECRET=your_cognito_client_secret_here
1111
COGNITO_USER_POOL_ID=your_cognito_user_pool_id_here
1212

1313
# AWS Region for Cognito
14-
AWS_REGION=us-east-1
14+
AWS_REGION=us-east-1
15+
16+
# Cognito Domain (without https:// prefix, just the domain name)
17+
# Example: mcp-gateway or your-custom-domain
18+
# COGNITO_DOMAIN=
19+
20+
# Secret key for session cookie signing (must match registry SECRET_KEY), string of hex characters
21+
# To generate: python -c 'import secrets; print(secrets.token_hex(32))'
22+
SECRET_KEY=your-secret-key-here

agents/agent_w_auth.py

Lines changed: 85 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,58 @@
7777
# Get logger
7878
logger = logging.getLogger(__name__)
7979

80-
def load_env_config() -> Dict[str, Optional[str]]:
80+
def get_auth_mode_from_args() -> bool:
8181
"""
82-
Load configuration from .env file if available.
82+
Parse command line arguments to determine authentication mode.
83+
This is done before loading environment variables to choose the correct .env file.
84+
85+
Returns:
86+
bool: True if using session cookie authentication, False for M2M authentication
87+
"""
88+
parser = argparse.ArgumentParser(add_help=False)
89+
parser.add_argument('--use-session-cookie', action='store_true',
90+
help='Use session cookie authentication instead of M2M')
91+
args, _ = parser.parse_known_args()
92+
return args.use_session_cookie
93+
94+
def print_env_file_banner(env_file_name: str, use_session_cookie: bool, file_found: bool, file_path: str = None):
95+
"""
96+
Print a prominent banner showing which .env file is being used and why.
97+
98+
Args:
99+
env_file_name: Name of the .env file being used
100+
use_session_cookie: Whether session cookie authentication is being used
101+
file_found: Whether the .env file was found
102+
file_path: Full path to the .env file if found
103+
"""
104+
print("\n" + "="*80)
105+
print("🔧 ENVIRONMENT CONFIGURATION")
106+
print("="*80)
107+
108+
auth_mode = "Session Cookie Authentication" if use_session_cookie else "M2M Authentication"
109+
print(f"Authentication Mode: {auth_mode}")
110+
print(f"Expected .env file: {env_file_name}")
111+
112+
if use_session_cookie:
113+
print("Reason: --use-session-cookie flag specified, using .env.user for user credentials")
114+
else:
115+
print("Reason: M2M authentication (default), using .env.agent for machine credentials")
116+
117+
if file_found and file_path:
118+
print(f"✅ Found and loaded: {file_path}")
119+
else:
120+
print(f"⚠️ File not found: {env_file_name}")
121+
print(" Falling back to system environment variables")
122+
123+
print("="*80 + "\n")
124+
125+
def load_env_config(use_session_cookie: bool) -> Dict[str, Optional[str]]:
126+
"""
127+
Load configuration from .env file based on authentication mode.
128+
Uses .env.user for session cookie auth, .env.agent for M2M auth.
129+
130+
Args:
131+
use_session_cookie: True for session cookie auth (.env.user), False for M2M auth (.env.agent)
83132
84133
Returns:
85134
Dict[str, Optional[str]]: Dictionary containing environment variables
@@ -92,29 +141,53 @@ def load_env_config() -> Dict[str, Optional[str]]:
92141
'domain': None
93142
}
94143

144+
# Choose .env file based on authentication mode
145+
env_file_name = '.env.user' if use_session_cookie else '.env.agent'
146+
95147
if DOTENV_AVAILABLE:
148+
file_found = False
149+
file_path = None
150+
96151
# Try to load from .env file in the current directory
97-
env_file = os.path.join(os.path.dirname(__file__), '.env')
152+
env_file = os.path.join(os.path.dirname(__file__), env_file_name)
98153
if os.path.exists(env_file):
99154
load_dotenv(env_file)
155+
file_found = True
156+
file_path = env_file
100157
logger.info(f"Loading environment variables from {env_file}")
101158
else:
102159
# Try to load from .env file in the parent directory
103-
env_file = os.path.join(os.path.dirname(__file__), '..', '.env')
160+
env_file = os.path.join(os.path.dirname(__file__), '..', env_file_name)
104161
if os.path.exists(env_file):
105162
load_dotenv(env_file)
163+
file_found = True
164+
file_path = env_file
106165
logger.info(f"Loading environment variables from {env_file}")
107166
else:
108167
# Try to load from current working directory
109-
load_dotenv()
110-
logger.info("Loading environment variables from current directory")
168+
env_file = os.path.join(os.getcwd(), env_file_name)
169+
if os.path.exists(env_file):
170+
load_dotenv(env_file)
171+
file_found = True
172+
file_path = env_file
173+
logger.info(f"Loading environment variables from {env_file}")
174+
else:
175+
# Fallback to default .env loading
176+
load_dotenv()
177+
logger.info("Loading environment variables from default .env file")
178+
179+
# Print banner showing which file is being used
180+
print_env_file_banner(env_file_name, use_session_cookie, file_found, file_path)
111181

112182
# Get values from environment
113183
env_config['client_id'] = os.getenv('COGNITO_CLIENT_ID')
114184
env_config['client_secret'] = os.getenv('COGNITO_CLIENT_SECRET')
115185
env_config['region'] = os.getenv('AWS_REGION')
116186
env_config['user_pool_id'] = os.getenv('COGNITO_USER_POOL_ID')
117187
env_config['domain'] = os.getenv('COGNITO_DOMAIN')
188+
else:
189+
# Print banner even when dotenv is not available
190+
print_env_file_banner(env_file_name, use_session_cookie, False)
118191

119192
return env_config
120193

@@ -126,8 +199,11 @@ def parse_arguments() -> argparse.Namespace:
126199
Returns:
127200
argparse.Namespace: The parsed command line arguments
128201
"""
129-
# Load environment configuration first
130-
env_config = load_env_config()
202+
# First, determine authentication mode to choose correct .env file
203+
use_session_cookie = get_auth_mode_from_args()
204+
205+
# Load environment configuration using the appropriate .env file
206+
env_config = load_env_config(use_session_cookie)
131207

132208
parser = argparse.ArgumentParser(description='LangGraph MCP Client with Cognito Authentication')
133209

@@ -145,7 +221,7 @@ def parse_arguments() -> argparse.Namespace:
145221

146222
# Authentication method arguments
147223
parser.add_argument('--use-session-cookie', action='store_true',
148-
help='Use session cookie authentication instead of M2M')
224+
help='Use session cookie authentication instead of M2M (loads .env.user instead of .env.agent)')
149225
parser.add_argument('--session-cookie-file', type=str, default='~/.mcp/session_cookie',
150226
help='Path to session cookie file (default: ~/.mcp/session_cookie)')
151227

auth_server/cli_auth.py renamed to agents/cli_user_auth.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from http.server import HTTPServer, BaseHTTPRequestHandler
2929
from itsdangerous import URLSafeTimedSerializer
3030
import requests
31+
from dotenv import load_dotenv
3132

3233
# Configure logging
3334
logging.basicConfig(
@@ -36,7 +37,18 @@
3637
)
3738
logger = logging.getLogger(__name__)
3839

40+
# Load environment variables from .env file
41+
# Look for .env file in the same directory as this script
42+
script_dir = Path(__file__).parent
43+
env_file = script_dir / '.env.user'
44+
if env_file.exists():
45+
load_dotenv(env_file)
46+
logger.info(f"Loaded environment variables from {env_file}")
47+
else:
48+
logger.warning(f"No .env file found at {env_file}")
49+
3950
# Configuration from environment
51+
COGNITO_USER_POOL_ID = os.environ.get('COGNITO_USER_POOL_ID')
4052
COGNITO_DOMAIN = os.environ.get('COGNITO_DOMAIN')
4153
COGNITO_CLIENT_ID = os.environ.get('COGNITO_CLIENT_ID')
4254
COGNITO_CLIENT_SECRET = os.environ.get('COGNITO_CLIENT_SECRET')
@@ -45,18 +57,19 @@
4557
AWS_REGION = os.environ.get('AWS_REGION', 'us-east-1')
4658

4759
# Validate required environment variables
48-
if not all([COGNITO_DOMAIN, COGNITO_CLIENT_ID, SECRET_KEY]):
60+
if not all([COGNITO_USER_POOL_ID, COGNITO_CLIENT_ID, SECRET_KEY]):
4961
logger.error("Missing required environment variables")
50-
logger.error("Required: COGNITO_DOMAIN, COGNITO_CLIENT_ID, SECRET_KEY")
62+
logger.error("Required: COGNITO_USER_POOL_ID, COGNITO_CLIENT_ID, SECRET_KEY")
5163
sys.exit(1)
5264

53-
# Build full domain URL if needed
54-
if not COGNITO_DOMAIN.startswith('https://'):
55-
# If just the domain prefix is provided, build the full URL
65+
# Construct the Cognito domain
66+
if COGNITO_DOMAIN:
67+
# Use custom domain if provided
5668
COGNITO_DOMAIN_URL = f"https://{COGNITO_DOMAIN}.auth.{AWS_REGION}.amazoncognito.com"
5769
else:
58-
# If full URL is provided, use as-is
59-
COGNITO_DOMAIN_URL = COGNITO_DOMAIN
70+
# Otherwise use user pool ID without underscores (standard format)
71+
user_pool_id_wo_underscore = COGNITO_USER_POOL_ID.replace('_', '')
72+
COGNITO_DOMAIN_URL = f"https://{user_pool_id_wo_underscore}.auth.{AWS_REGION}.amazoncognito.com"
6073

6174
# OAuth endpoints
6275
AUTHORIZE_URL = f"{COGNITO_DOMAIN_URL}/oauth2/authorize"
File renamed without changes.

auth_server/.env.template

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,16 @@ COGNITO_CLIENT_SECRET=your_cognito_client_secret_here
1313
# Cognito User Pool ID
1414
COGNITO_USER_POOL_ID=your_cognito_user_pool_id_here
1515

16+
# Cognito Domain (without https:// prefix, just the domain name)
17+
# Example: mcp-gateway or your-custom-domain
18+
COGNITO_DOMAIN=your_cognito_domain_here
19+
20+
# Secret key for session cookie signing (must match registry SECRET_KEY)
21+
# Generate a secure random string for production use
22+
# To generate: python -c 'import secrets; print(secrets.token_hex(32))'
23+
SECRET_KEY=your-secret-key-here
24+
25+
USER_POOL_ID=your_cognito_user_pool_id_here
26+
1627
# AWS Region for Cognito
1728
AWS_REGION=us-east-1

auth_server/pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ dependencies = [
2121
"pyjwt>=2.6.0",
2222
"cryptography>=40.0.0",
2323
"pyyaml>=6.0.0",
24-
"itsdangerous>=2.0.0"
24+
"httpx>=0.25.0",
25+
"itsdangerous>=2.1.0"
2526
]
2627

2728
[project.optional-dependencies]

auth_server/server.py

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,7 @@ def load_scopes_config():
4646

4747
def map_cognito_groups_to_scopes(groups: List[str]) -> List[str]:
4848
"""
49-
Map Cognito groups to MCP scopes using scopes.yml configuration.
50-
51-
Uses the same scope format as M2M tokens for consistency.
49+
Map Cognito groups to MCP scopes using the same format as M2M resource server scopes.
5250
5351
Args:
5452
groups: List of Cognito group names
@@ -57,28 +55,36 @@ def map_cognito_groups_to_scopes(groups: List[str]) -> List[str]:
5755
List of MCP scopes in resource server format
5856
"""
5957
scopes = []
60-
61-
# Use group mappings from scopes.yml if available
62-
group_mappings = SCOPES_CONFIG.get('group_mappings', {})
58+
logger.info(f"Mapping Cognito groups to MCP scopes: {groups}")
6359

6460
for group in groups:
65-
if group in group_mappings:
66-
# Use configured mapping
67-
scopes.extend(group_mappings[group])
68-
else:
69-
# Legacy fallback for backward compatibility
70-
logger.debug(f"Group '{group}' not in group_mappings, using legacy mapping")
71-
72-
if group == 'mcp-admin':
73-
scopes.extend(['mcp-servers-unrestricted/read',
74-
'mcp-servers-unrestricted/execute'])
75-
elif group == 'mcp-user':
76-
scopes.append('mcp-servers-restricted/read')
77-
elif group.startswith('mcp-server-'):
78-
scopes.append('mcp-servers-restricted/execute')
61+
if group == 'mcp-admin':
62+
# Admin gets unrestricted read and execute access
63+
scopes.append('mcp-servers-unrestricted/read')
64+
scopes.append('mcp-servers-unrestricted/execute')
65+
elif group == 'mcp-user':
66+
# Regular users get restricted read access by default
67+
scopes.append('mcp-servers-restricted/read')
68+
elif group.startswith('mcp-server-'):
69+
# Server-specific groups grant access based on server permissions
70+
# For now, grant restricted execute access for specific servers
71+
# This allows access to the servers defined in the restricted scope
72+
scopes.append('mcp-servers-restricted/execute')
73+
74+
# Note: The actual server access control is handled by the
75+
# validate_server_tool_access function which checks the scopes.yml
76+
# configuration. The group names are preserved in the 'groups' field
77+
# for potential future fine-grained access control.
7978

8079
# Remove duplicates while preserving order
81-
return list(dict.fromkeys(scopes))
80+
seen = set()
81+
unique_scopes = []
82+
for scope in scopes:
83+
if scope not in seen:
84+
seen.add(scope)
85+
unique_scopes.append(scope)
86+
87+
return unique_scopes
8288

8389
def validate_session_cookie(cookie_value: str) -> Dict[str, any]:
8490
"""
@@ -646,6 +652,7 @@ async def validate_request(request: Request):
646652
if cookie_value:
647653
try:
648654
validation_result = validate_session_cookie(cookie_value)
655+
logger.info(f"Session cookie validation result: {validation_result}")
649656
logger.info(f"Session cookie validation successful for user: {validation_result['username']}")
650657
except ValueError as e:
651658
logger.warning(f"Session cookie validation failed: {e}")

0 commit comments

Comments
 (0)