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:
parent
07b1cb84b2
commit
80ed791b01
8 changed files with 551 additions and 93 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue