# MCP Client Authentication ## Overview The MCP client (`MyMCPClient`) now automatically authenticates with the server on startup and includes authentication cookies in all requests. ## How It Works ### Authentication Flow ``` 1. Client starts up (MyMCPClient.init/1) ↓ 2. Calls authenticate(base_url) ↓ 3. GET https://localhost/set ↓ 4. Server responds with Set-Cookie: user= ↓ 5. Client extracts cookie value ↓ 6. Stores cookie in state: %{auth_cookie: "encrypted_value"} ↓ 7. All subsequent requests include: Cookie: user= ↓ 8. Server validates cookie via require_authentication plug ↓ 9. Request processed ✓ ``` ### Auto Re-authentication If a request receives a 401 status: 1. Client detects authentication expired 2. Automatically calls `/set` to get new cookie 3. Retries the original request with new cookie 4. Returns result to caller ## Code Changes ### State Structure **Before:** ```elixir defstruct [:base_url, :request_id] ``` **After:** ```elixir defstruct [:base_url, :request_id, :auth_cookie] ``` ### Initialization **Before:** ```elixir def init(base_url) do {:ok, %__MODULE__{base_url: base_url, request_id: 0}} end ``` **After:** ```elixir def init(base_url) do case authenticate(base_url) do {:ok, cookie} -> Logger.info("MyMCPClient authenticated successfully") {:ok, %__MODULE__{base_url: base_url, request_id: 0, auth_cookie: cookie}} {:error, reason} -> Logger.error("MyMCPClient authentication failed: #{inspect(reason)}") {:ok, %__MODULE__{base_url: base_url, request_id: 0, auth_cookie: nil}} end end ``` ### Request Sending **Before:** ```elixir Req.post(url, json: body) ``` **After:** ```elixir req_opts = [json: body] req_opts = if state.auth_cookie do Keyword.put(req_opts, :headers, [{"cookie", "user=#{state.auth_cookie}"}]) else req_opts end Req.post(url, req_opts) ``` ### New Functions 1. **`authenticate/1`** - Gets session cookie from server ```elixir defp authenticate(base_url) do url = "#{base_url}/set" case Req.get(url) do {:ok, %Req.Response{status: 200, headers: headers}} -> extract_cookie(headers) # ... error handling end end ``` 2. **`extract_cookie/1`** - Parses Set-Cookie header ```elixir defp extract_cookie(headers) do # Finds "set-cookie" header # Extracts value from "user=value; Path=/; ..." # Returns {:ok, value} or :error end ``` ## Logs ### Successful Authentication ``` [info] MyMCPClient authenticated successfully [debug] Extracted authentication cookie ``` ### Failed Authentication ``` [error] MyMCPClient authentication failed: :no_cookie # or [error] Failed to connect to authentication endpoint: :econnrefused ``` ### Re-authentication ``` [warning] Authentication expired, attempting to re-authenticate [info] Re-authentication successful, retrying request ``` ## Testing ### Test Client Authentication ```elixir # In IEx iex> MyMCPClient.initialize() # Should see: # [info] MyMCPClient authenticated successfully # {:ok, %{...}} iex> MyMCPClient.list_tools() # Should work without 401 error ``` ### Test Without Server ```elixir # Stop server iex> :observer.start() # Kill MyMCPServer.Router process # Try client request iex> MyMCPClient.list_tools() # Should see: # [error] Failed to connect to authentication endpoint: :econnrefused ``` ### Test Auto Re-authentication ```elixir # Manually invalidate cookie in server (or change @secret_key_base) # Then make a request iex> MyMCPClient.list_tools() # Should see: # [warning] Authentication expired, attempting to re-authenticate # [info] Re-authentication successful, retrying request # {:ok, %{...}} ``` ## Integration with Server ### Server Side (lib/my_mcp_server_router.ex) ```elixir # Authentication middleware checks cookie plug(:require_authentication) defp require_authentication(conn, _opts) do case decrypt_cookie_value(conn.cookies["user"]) do {:ok, user_id} -> conn # Allow {:error, _} -> send_resp(conn, 401, "Unauthorized") |> halt() end end ``` ### Client Side (lib/my_mcp_client.ex) ```elixir # Includes cookie in every request headers: [{"cookie", "user=#{state.auth_cookie}"}] ``` ## Configuration The client reads the base URL from application.ex: ```elixir # In lib/mcp/application.ex {MyMCPClient, base_url: "https://localhost"} ``` Make sure the base_url matches where your server is running: - Local: `https://localhost` or `https://localhost:443` - Remote: `https://your-domain.com` ## Security Considerations ### Cookie Handling - ✅ Cookie value is encrypted by server - ✅ Client only stores and forwards the value - ✅ HTTPS required (`secure: true` flag on server) - ✅ Cookie not accessible via JavaScript (`http_only: true`) ### Auto Re-authentication - ✅ Seamless user experience - ⚠️ Could mask authentication issues - ✅ Logged for debugging - 💡 Consider: Max retry limit to prevent infinite loops ### Cookie Storage - ✅ Stored in GenServer state (memory only) - ✅ Lost on process restart (intentional - forces re-auth) - ⚠️ Not persisted to disk - 💡 Consider: Optional persistence for long-running clients ## Troubleshooting ### Client fails to authenticate on startup **Symptom:** ``` [error] MyMCPClient authentication failed: :econnrefused ``` **Causes:** 1. Server not running 2. Wrong base_url 3. HTTPS certificate issues **Solution:** ```bash # Check server is running curl -k https://localhost/set # Check base_url in application.ex matches server # For self-signed certs, may need to disable SSL verification in Req ``` ### Requests return 401 despite authentication **Symptom:** ``` [warning] Authentication expired, attempting to re-authenticate ``` **Causes:** 1. Cookie expired (max_age reached) 2. Server secret key changed 3. Cookie corrupted **Solution:** - Check server logs for cookie validation errors - Verify @secret_key_base hasn't changed - Client will auto re-authenticate ### No Set-Cookie in response **Symptom:** ``` [error] No cookie found in /set response ``` **Causes:** 1. /set endpoint not setting cookie 2. Response intercepted/modified 3. Proxy stripping cookies **Solution:** ```bash # Verify server sends cookie curl -k -v https://localhost/set 2>&1 | grep -i "set-cookie" # Should see: Set-Cookie: user=... ``` ### Cookie not included in requests **Symptom:** Server logs show "No cookie present" **Causes:** 1. auth_cookie is nil in state 2. Headers not properly set 3. Req not sending cookie **Debug:** ```elixir # Check client state :sys.get_state(MyMCPClient) # Should see: auth_cookie: "some_encrypted_value" # If nil, authentication failed during init ``` ## Future Enhancements ### 1. Persistent Sessions ```elixir # Save cookie to disk for reuse across restarts defp save_cookie(cookie) do File.write!("~/.mcp_session", cookie) end defp load_cookie() do File.read("~/.mcp_session") end ``` ### 2. Multiple Retry Attempts ```elixir defp send_request(method, params, state, retry_count \\ 0) do # Limit re-authentication attempts if retry_count > 3 do {:error, :max_retries_exceeded} else # ... existing logic with retry_count end end ``` ### 3. Manual Re-authentication API ```elixir def re_authenticate do GenServer.call(__MODULE__, :re_authenticate) end def handle_call(:re_authenticate, _from, state) do case authenticate(state.base_url) do {:ok, cookie} -> {:reply, :ok, %{state | auth_cookie: cookie}} {:error, reason} -> {:reply, {:error, reason}, state} end end ``` ### 4. Session Expiry Tracking ```elixir defstruct [:base_url, :request_id, :auth_cookie, :cookie_expires_at] # Track when cookie expires and proactively refresh ``` ## Related Documentation - Server authentication middleware: `docs/AUTHENTICATION.md` - Authorization system: `docs/AUTHORIZATION.md` - Router implementation: `lib/my_mcp_server_router.ex` - Client implementation: `lib/my_mcp_client.ex`