# Accessing Cookie Information for Authorization in MyMCPServer ## Overview This guide explains how user information from authenticated cookies flows from the HTTP Router through to MyMCPServer for authorization purposes. ## Architecture Flow ``` ┌─────────────────────────────────────────────────────────────────┐ │ 1. HTTP Request arrives with encrypted cookie │ │ POST /message │ │ Cookie: user= │ └────────────────────┬────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ 2. MyMCPServer.Router (lib/my_mcp_server_router.ex) │ │ - require_authentication plug validates cookie │ │ - get_user(conn) decrypts and loads user data │ │ - Extracts: %{id: user_id, keys: %{12345 => true, ...}} │ └────────────────────┬────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ 3. handle_jsonrpc/2 receives user context │ │ - Parses JSON-RPC request │ │ - Calls dispatch_method(method, params, user) │ └────────────────────┬────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ 4. dispatch_method/3 routes to correct handler │ │ - Calls get_tools(user), call_tool(name, args, user), etc. │ └────────────────────┬────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ 5. MyMCPServer.Manager (lib/my_mcp_server_manager.ex) │ │ - Receives user context │ │ - Adds user to state: Map.put(state, :current_user, user) │ │ - Calls MyMCPServer.handle_tool_call(name, args, state) │ └────────────────────┬────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ 6. MyMCPServer (lib/my_mcp_server.ex) │ │ - Accesses user: Map.get(state, :current_user) │ │ - Performs authorization checks │ │ - Executes tool/resource request │ │ - Returns result │ └─────────────────────────────────────────────────────────────────┘ ``` ## Step-by-Step Implementation ### Step 1: Router Extracts User from Cookie In `lib/my_mcp_server_router.ex`: ```elixir # The authentication plug already validated the cookie # Now extract user information post "/message" do # Get user from authenticated cookie user = get_user(conn) # user = %{id: 1234567890, keys: %{12345 => true, 11111 => true}} # Pass user to JSON-RPC handler response = handle_jsonrpc(conn.body_params, user) conn |> put_resp_content_type("application/json") |> send_resp(200, Jason.encode!(response)) end # Helper function that decrypts cookie and loads user data defp get_user(conn) do conn = fetch_cookies(conn) encrypted_user = conn.cookies["user"] user_id = case encrypted_user do nil -> 0 encrypted -> case decrypt_cookie_value(encrypted) do {:ok, value} -> String.to_integer(value) {:error, _} -> 0 end end # Load user permissions (in production, from database) users = %{@james => %{12345 => true, 11111 => true}} user_keys = Map.get(users, user_id, %{}) %{id: user_id, keys: user_keys} end ``` ### Step 2: Manager Adds User to State In `lib/my_mcp_server_manager.ex`: ```elixir def call_tool(name, arguments, user \\ %{}) do GenServer.call(__MODULE__, {:call_tool, name, arguments, user}) end def handle_call({:call_tool, name, arguments, user}, _from, manager_state) do # Add user context to MCP state mcp_state_with_user = Map.put(manager_state.mcp_state, :current_user, user) # Call MyMCPServer with user in state case MyMCPServer.handle_tool_call(name, arguments, mcp_state_with_user) do {:reply, result, new_mcp_state} -> # Clean up user from state before storing new_mcp_state = Map.delete(new_mcp_state, :current_user) new_manager_state = %{manager_state | mcp_state: new_mcp_state} {:reply, {:ok, result}, new_manager_state} {:error, error, new_mcp_state} -> new_mcp_state = Map.delete(new_mcp_state, :current_user) new_manager_state = %{manager_state | mcp_state: new_mcp_state} {:reply, {:error, error}, new_manager_state} end end ``` ### Step 3: MyMCPServer Accesses User for Authorization In `lib/my_mcp_server.ex`: ```elixir @impl Hermes.Server def handle_tool_call("add_numbers", %{"a" => a, "b" => b}, state) do # 1. Extract user information from state user = Map.get(state, :current_user, %{}) user_keys = Map.get(user, :keys, %{}) # 2. Check authorization using Auth.Authorization case Auth.Authorization.authorized?(user_keys, "index:view") do {:ok, true} -> # User has permission - execute the tool result = a + b Logger.info("User #{user.id} called add_numbers: #{result}") content = [%{type: "text", text: "Result: #{result}"}] {:reply, %{content: content}, state} {:error, _} -> # User lacks permission - deny request Logger.warning("Unauthorized tool call by user #{user.id}") {:error, %{code: -32000, message: "Unauthorized"}, state} end end ``` ## User Data Structure The `user` map passed through the system has this structure: ```elixir %{ id: 1769578105155195386, # User ID from cookie keys: %{ # Permission keys 12345 => true, # Has "index:view" permission 11111 => true # Has "admin:manage" permission } } ``` ## Authorization Patterns ### Pattern 1: Require Specific Permission ```elixir def handle_tool_call("admin_tool", _args, state) do user = Map.get(state, :current_user, %{}) user_keys = Map.get(user, :keys, %{}) case Auth.Authorization.authorized?(user_keys, "admin:manage") do {:ok, true} -> # Execute admin tool {:reply, result, state} {:error, _} -> {:error, %{code: -32000, message: "Admin permission required"}, state} end end ``` ### Pattern 2: Public Tool (No Authorization) ```elixir def handle_tool_call("public_tool", _args, state) do user = Map.get(state, :current_user, %{}) Logger.info("User #{user.id} using public tool") # No authorization check - anyone can use this {:reply, result, state} end ``` ### Pattern 3: Multiple Permissions (ANY) ```elixir def handle_tool_call("flexible_tool", _args, state) do user = Map.get(state, :current_user, %{}) user_keys = Map.get(user, :keys, %{}) # User needs EITHER permission if Auth.Authorization.authorized_any?(user_keys, ["index:view", "admin:manage"]) do {:reply, result, state} else {:error, %{code: -32000, message: "Insufficient permissions"}, state} end end ``` ### Pattern 4: Multiple Permissions (ALL) ```elixir def handle_tool_call("strict_tool", _args, state) do user = Map.get(state, :current_user, %{}) user_keys = Map.get(user, :keys, %{}) # User needs BOTH permissions if Auth.Authorization.authorized_all?(user_keys, ["index:view", "admin:manage"]) do {:reply, result, state} else {:error, %{code: -32000, message: "Multiple permissions required"}, state} end end ``` ### Pattern 5: Filter Lists by Permission ```elixir def handle_request(%{"method" => "tools/list"}, state) do user = Map.get(state, :current_user, %{}) user_keys = Map.get(user, :keys, %{}) all_tools = [ %{name: "public_tool", required_permission: nil}, %{name: "user_tool", required_permission: "index:view"}, %{name: "admin_tool", required_permission: "admin:manage"} ] # Only show tools the user has permission for available_tools = all_tools |> Enum.filter(fn tool -> case tool.required_permission do nil -> true permission -> Auth.Authorization.authorized_bool?(user_keys, permission) end end) |> Enum.map(&Map.drop(&1, [:required_permission])) {:reply, %{tools: available_tools}, state} end ``` ## Complete Example ```elixir # lib/my_mcp_server.ex defmodule MyMCPServer do use Hermes.Server require Logger @impl Hermes.Server def handle_tool_call("sensitive_operation", args, state) do # 1. Extract user from state (injected by Manager) user = Map.get(state, :current_user, %{}) # 2. Log the attempt Logger.info("User #{user.id} attempting sensitive_operation") # 3. Extract user's permission keys user_keys = Map.get(user, :keys, %{}) # 4. Check authorization case Auth.Authorization.authorized?(user_keys, "admin:manage", audit: true) do {:ok, true} -> # 5a. Authorized - execute operation Logger.info("Authorization granted for user #{user.id}") result = perform_sensitive_operation(args) content = [%{type: "text", text: "Success: #{result}"}] {:reply, %{content: content}, state} {:error, :forbidden} -> # 5b. Not authorized - deny Logger.warning("Authorization denied for user #{user.id}") {:error, %{ code: -32000, message: "Unauthorized: admin:manage permission required" }, state} {:error, :unknown_permission} -> # 5c. Configuration error Logger.error("Permission configuration error") {:error, %{code: -32603, message: "Internal server error"}, state} end end defp perform_sensitive_operation(args) do # Your business logic here "operation completed" end end ``` ## Testing Authorization ### Test 1: Authorized User ```elixir # User has permission key 12345 (index:view) user = %{id: 1769578105155195386, keys: %{12345 => true}} # Call should succeed {:ok, result} = MyMCPServer.Manager.call_tool("add_numbers", %{"a" => 5, "b" => 3}, user) # => {:ok, %{content: [%{type: "text", text: "Result: 8"}]}} ``` ### Test 2: Unauthorized User ```elixir # User with no permissions user = %{id: 0, keys: %{}} # Call should fail {:error, error} = MyMCPServer.Manager.call_tool("add_numbers", %{"a" => 5, "b" => 3}, user) # => {:error, %{code: -32000, message: "Unauthorized"}} ``` ### Test 3: Admin Tool ```elixir # User with admin permission (11111) admin_user = %{id: 1769578105155195386, keys: %{11111 => true}} # Admin tool should succeed {:ok, result} = MyMCPServer.Manager.call_tool("increment_counter", %{}, admin_user) # Regular user without admin permission regular_user = %{id: 123, keys: %{12345 => true}} # Should fail {:error, error} = MyMCPServer.Manager.call_tool("increment_counter", %{}, regular_user) # => {:error, %{code: -32000, message: "Unauthorized: admin permission required"}} ``` ## Logging and Auditing All authorization checks can be audited: ```elixir def handle_tool_call("sensitive_tool", args, state) do user = Map.get(state, :current_user, %{}) user_keys = Map.get(user, :keys, %{}) # Enable auditing case Auth.Authorization.authorized?(user_keys, "admin:manage", audit: true) do {:ok, true} -> # Audit log automatically created: # [AUDIT] Authorization granted # Permission: admin:manage # User Keys: [12345, 11111] # Timestamp: 2026-01-30T... {:reply, result, state} {:error, _} -> # Audit log for denial also created {:error, error, state} end end ``` ## Security Best Practices ### 1. Always Check Permissions ```elixir # ❌ BAD: No authorization check def handle_tool_call("delete_user", args, state) do delete_user(args) {:reply, %{success: true}, state} end # ✅ GOOD: Check authorization first def handle_tool_call("delete_user", args, state) do user = Map.get(state, :current_user, %{}) user_keys = Map.get(user, :keys, %{}) case Auth.Authorization.authorized?(user_keys, "admin:manage") do {:ok, true} -> delete_user(args) {:error, _} -> {:error, unauthorized_error(), state} end end ``` ### 2. Log Authorization Decisions ```elixir case Auth.Authorization.authorized?(user_keys, permission, audit: true) do {:ok, true} -> Logger.info("User #{user.id} authorized for #{operation}") # ... {:error, _} -> Logger.warning("User #{user.id} denied for #{operation}") # ... end ``` ### 3. Handle Missing User Context ```elixir # Always provide default user = Map.get(state, :current_user, %{id: 0, keys: %{}}) # Or fail explicitly if user required case Map.get(state, :current_user) do nil -> {:error, %{code: -32000, message: "Authentication required"}, state} user -> # Process with user end ``` ### 4. Don't Leak Permission Information ```elixir # ❌ BAD: Reveals permission name to unauthorized users {:error, %{message: "You need 'admin:secret_operation' permission"}, state} # ✅ GOOD: Generic error message {:error, %{message: "Unauthorized: insufficient permissions"}, state} ``` ## Summary | Component | Responsibility | |-----------|----------------| | **Router** | Extract user from cookie, pass to Manager | | **Manager** | Add user to state, forward to MyMCPServer | | **MyMCPServer** | Access user from state, perform authorization | | **Auth.Authorization** | Check permissions, create audit logs | **Key Point:** User information flows from the cookie through the entire request pipeline and is available in MyMCPServer's state as `:current_user`. ## Related Documentation - Cookie encryption: `lib/my_mcp_server_router.ex:encrypt_cookie_value/1` - Authorization system: `docs/AUTHORIZATION.md` - Authentication middleware: `docs/AUTHENTICATION.md`