Add multi-user support - each API key gets own GeoGuessr session

Implements comprehensive multi-user support allowing multiple users to
access the same MCP server instance with their own independent GeoGuessr
accounts. Each API key now has its own session storage and context.

## Multi-User Architecture

### New Components

**User Context System** (src/geoguessr_mcp/auth/user_context.py):
- UserContext dataclass tracks API key and associated GeoGuessr session
- Properties for user_id, username, ncfa_cookie, is_authenticated
- Automatically attached to each request

**Multi-User Session Manager** (src/geoguessr_mcp/auth/multi_user_session.py):
- MultiUserSessionManager manages separate SessionManager per API key
- Maps API keys to their own GeoGuessr sessions
- Methods: get_user_context, login_user, logout_user, set_user_cookie
- Global instance: multi_user_session_manager

**Request Context** (src/geoguessr_mcp/auth/request_context.py):
- ContextVar for accessing current user context in tools
- Functions: get_current_user_context, require_user_context, set_current_user_context
- Enables tools to access user-specific sessions automatically

### Updated Components

**Authentication Middleware** (src/geoguessr_mcp/middleware/auth.py):
- Now creates user context for each authenticated request
- Attaches context to both request.state and ContextVar
- Supports both authenticated and unauthenticated modes
- Default user context created when auth is disabled

**Authentication Tools** (src/geoguessr_mcp/tools/auth_tools.py):
- Completely rewritten for multi-user support
- login(): Creates session tied to caller's API key
- logout(): Logs out only the calling user's session
- set_ncfa_cookie(): Sets cookie for calling user only
- get_auth_status(): Returns calling user's auth status
- All tools use get_current_user_context() automatically

**GeoGuessr Client** (src/geoguessr_mcp/api/geoguessr_client.py):
- _get_authenticated_client() checks user context first
- Falls back to session_manager for backward compatibility
- Automatically uses caller's session when available
- No changes needed in services (profile, game, analysis)

## How It Works

1. User connects with API key in Authorization header
2. Middleware validates API key and creates/retrieves UserContext
3. UserContext attached to request.state and ContextVar
4. Tools call get_current_user_context() to access caller's session
5. Client automatically uses correct session for API calls
6. Each user's session is completely isolated

## Usage Example

```bash
# Configure multiple API keys
MCP_AUTH_ENABLED=true
MCP_API_KEYS=alice_key,bob_key,charlie_key

# Alice connects with: Authorization: Bearer alice_key
# Bob connects with: Authorization: Bearer bob_key
# Each can login to their own GeoGuessr account
# Sessions are completely independent
```

## Key Features

- **Zero Interference**: Users don't affect each other's sessions
- **Automatic Routing**: Requests automatically use correct user's session
- **Hot Reload**: Add new API keys and restart in ~2-3 seconds
- **Backward Compatible**: Still works with single-user mode
- **Fallback Support**: GEOGUESSR_NCFA_COOKIE still works as default

## Documentation Updates

- README.md: Added Multi-User Mode section with examples
- README.md: Updated authentication section with multi-user details
- README.md: Added "Adding New Users" workflow
- Key Features section now highlights multi-user support

## Technical Details

- Uses Python ContextVar for request-scoped user context
- Each API key gets its own SessionManager instance
- Session storage is in-memory (persists across requests, not restarts)
- Default cookie (GEOGUESSR_NCFA_COOKIE) used as fallback for all users
- Fully async/await compatible throughout
This commit is contained in:
Claude 2025-11-29 22:30:55 +00:00
parent 07b1cb84b2
commit 80ed791b01
No known key found for this signature in database
8 changed files with 551 additions and 93 deletions

View file

@ -2,48 +2,48 @@
Tools for handling authentication and session management with GeoGuessr.
This module provides utilities for login, logout, setting authentication cookies,
and checking the current authentication status. Its primary purpose is to ensure
interactions with the GeoGuessr API are authenticated securely and conveniently.
and checking the current authentication status. With multi-user support, each
API key can have its own GeoGuessr session.
The module integrates with FastMCP to expose authentication methods as tools
and uses the `SessionManager` for session storage and validation.
and uses the multi-user session manager for per-user session storage.
Functions
---------
- register_auth_tools: Registers a set of authentication tools with FastMCP.
- get_current_session_token: Returns the currently active session token.
"""
import logging
import secrets
from datetime import datetime, timedelta, UTC
from typing import Optional
from mcp.server.fastmcp import FastMCP
from ..auth.session import SessionManager, UserSession
from ..config import settings
from ..auth import get_current_user_context, multi_user_session_manager
logger = logging.getLogger(__name__)
# Global session token storage
_current_session_token: Optional[str] = None
def register_auth_tools(mcp: FastMCP, session_manager=None):
"""
Register authentication-related tools.
def register_auth_tools(mcp: FastMCP, session_manager: SessionManager):
"""Register authentication-related tools."""
Note: session_manager parameter is kept for backward compatibility but not used.
The multi_user_session_manager is used instead.
"""
@mcp.tool()
async def login(email: str, password: str) -> dict:
"""
Authenticate with GeoGuessr using email and password.
Creates a session that will be used for all later API calls.
Credentials are only used to get an authentication token and are
Creates a session for YOUR account that will be used for all later API calls.
Your credentials are only used to get an authentication token and are
not stored on the server.
Each API key gets its own session, so multiple users can use the same
MCP server with their own GeoGuessr accounts.
Args:
email: Your GeoGuessr account email
password: Your GeoGuessr account password
@ -51,11 +51,19 @@ def register_auth_tools(mcp: FastMCP, session_manager: SessionManager):
Returns:
Session information including username and session token
"""
global _current_session_token
# Get the current user's context (API key)
user_context = get_current_user_context()
if not user_context:
return {
"success": False,
"error": "No user context available. Authentication is required.",
}
try:
session_token, session = await session_manager.login(email, password)
_current_session_token = session_token
# Login using the multi-user session manager
session_token, session = await multi_user_session_manager.login_user(
user_context.api_key, email, password
)
return {
"success": True,
@ -64,6 +72,7 @@ def register_auth_tools(mcp: FastMCP, session_manager: SessionManager):
"user_id": session.user_id,
"session_token": session_token,
"expires_at": session.expires_at.isoformat() if session.expires_at else None,
"multi_user_note": "Your session is tied to your API key. Other users with different API keys can have their own sessions.",
}
except ValueError as e:
return {"success": False, "error": str(e)}
@ -72,23 +81,32 @@ def register_auth_tools(mcp: FastMCP, session_manager: SessionManager):
return {"success": False, "error": f"An unexpected error occurred: {str(e)}"}
@mcp.tool()
async def logout() -> dict:
async def logout(session_token: str) -> dict:
"""
Logout from the current GeoGuessr session.
Logout from your current GeoGuessr session.
Invalidates the current session token.
Invalidates your session token. This only affects your session
(tied to your API key), not other users.
Args:
session_token: The session token to logout (from login response)
Returns:
Success status
"""
global _current_session_token
# Get the current user's context
user_context = get_current_user_context()
if not user_context:
return {"success": False, "error": "No user context available"}
if _current_session_token:
success = await session_manager.logout(_current_session_token)
_current_session_token = None
return {
"success": success,
"message": "Successfully logged out" if success else "No active session found",
}
success = await multi_user_session_manager.logout_user(
user_context.api_key, session_token
)
return {"success": False, "message": "No active session"}
return {
"success": success,
"message": "Successfully logged out" if success else "No active session found",
}
@mcp.tool()
async def set_ncfa_cookie(cookie: str) -> dict:
@ -98,83 +116,83 @@ def register_auth_tools(mcp: FastMCP, session_manager: SessionManager):
Use this if you've manually extracted the cookie from your browser.
The cookie will be validated before being accepted.
This cookie will be tied to YOUR API key, so it won't affect other users.
Args:
cookie: The _ncfa cookie value from your browser
Returns:
Success status and user information
"""
global _current_session_token
# Get the current user's context
user_context = get_current_user_context()
if not user_context:
return {"success": False, "error": "No user context available"}
# Validate the cookie
profile = await session_manager.validate_cookie(cookie)
# Set the cookie for this user
success = await multi_user_session_manager.set_user_cookie(user_context.api_key, cookie)
if not profile:
if not success:
return {"success": False, "error": "Invalid cookie - authentication failed"}
# Create a session from the cookie
session = UserSession(
ncfa_cookie=cookie,
user_id=profile.get("id", ""),
username=profile.get("nick", ""),
email="manual@cookie",
expires_at=datetime.now(UTC) + timedelta(days=30),
)
# Get updated session to show user info
session = await multi_user_session_manager.get_session_for_api_key(user_context.api_key)
# Store as a session
session_token = secrets.token_urlsafe(32)
async with session_manager._lock:
session_manager._sessions[session_token] = session
session_manager._user_sessions[session.user_id] = session_token
if session:
return {
"success": True,
"message": f"Cookie set successfully. Authenticated as {session.username}",
"username": session.username,
"user_id": session.user_id,
}
_current_session_token = session_token
return {
"success": True,
"message": f"Cookie set successfully. Authenticated as {session.username}",
"username": session.username,
"user_id": session.user_id,
"session_token": session_token,
}
return {"success": True, "message": "Cookie set successfully"}
@mcp.tool()
async def get_auth_status() -> dict:
"""
Check the current authentication status.
Check your current authentication status.
Returns information about the current session or available
authentication methods.
Returns information about your session (tied to your API key).
Each user with a different API key has their own independent session.
Returns:
Authentication status and user information
"""
global _current_session_token
# Get the current user's context
user_context = get_current_user_context()
if not user_context:
return {
"authenticated": False,
"message": "No user context available",
}
# Check for active session
if _current_session_token:
session = await session_manager.get_session(_current_session_token)
if session and session.is_valid():
return {
"authenticated": True,
"method": "session",
"username": session.username,
"user_id": session.user_id,
"expires_at": session.expires_at.isoformat() if session.expires_at else None,
}
# Get auth status for this user
status = await multi_user_session_manager.get_auth_status(user_context.api_key)
# Check for environment variable
env_cookie = settings.DEFAULT_NCFA_COOKIE
if env_cookie:
profile = await session_manager.validate_cookie(env_cookie)
if profile:
return {
"authenticated": True,
"method": "environment_variable",
"username": profile.get("nick", "Unknown"),
"user_id": profile.get("id", "Unknown"),
}
if status["authenticated"]:
return {
**status,
"message": f"Authenticated as {status['username']}",
"multi_user_info": "Your session is independent. Other API keys have their own sessions.",
}
return {
"authenticated": False,
**status,
"message": "Not authenticated. Use 'login' with credentials or 'set_ncfa_cookie' with a valid cookie.",
"available_methods": ["login(email, password)", "set_ncfa_cookie(cookie)"],
}
def get_current_session_token() -> Optional[str]:
"""Get the current session token for use by other tools."""
return _current_session_token
def get_current_session_token():
"""
Deprecated: This function is no longer used in multi-user mode.
Sessions are now managed per-API-key automatically.
Use get_current_user_context() instead to access user-specific session data.
"""
logger.warning(
"get_current_session_token() is deprecated in multi-user mode. "
"Use get_current_user_context() instead."
)
return None