# Authentication Middleware Documentation ## Overview The router now includes authentication middleware that checks for valid user cookies before processing any request (except public endpoints). ## How It Works ### Plug Pipeline Order ```elixir plug(Plug.Static, at: "/", from: "asset") # 1. Serve static files (no auth) plug(:match) # 2. Match route plug(Plug.Parsers, parsers: [:json]) # 3. Parse request body plug(:require_authentication) # 4. CHECK AUTHENTICATION ← NEW! plug(:dispatch) # 5. Execute route handler ``` ### Authentication Flow ``` Request → Is it /set? ┬─ Yes → Allow (skip auth) │ └─ No → Has cookie? ┬─ Yes → Valid? ┬─ Yes → Allow │ │ │ └─ No → 401 │ └─ No → 401 ``` ## Public Endpoints (No Authentication Required) Currently, only `/set` bypasses authentication. To add more: ```elixir # In lib/my_mcp_server_router.ex # Skip /set (login) defp require_authentication(%{path_info: ["set"]} = conn, _opts), do: conn # Skip /health (health check) defp require_authentication(%{path_info: ["health"]} = conn, _opts), do: conn # Skip /api/public/* (public API) defp require_authentication(%{path_info: ["api", "public" | _]} = conn, _opts), do: conn # All other routes require authentication defp require_authentication(conn, _opts) do # ... validation logic ... end ``` ## Protected Endpoints (Authentication Required) All routes not explicitly excluded require a valid encrypted cookie: - `/floki/index` - `/page` - `/message` (JSON-RPC) - `/sse` (Server-Sent Events) - `/save` - Any future routes you add ## Error Responses ### 401 - No Cookie **Request:** ```bash curl https://localhost/floki/index ``` **Response:** ```html Authentication Required

401 - Authentication Required

Authentication required. Please log in.

Click here to log in

``` ### 401 - Invalid/Tampered Cookie **Request:** ```bash curl -b "user=invalid_data" https://localhost/floki/index ``` **Response:** ```html
Invalid session. Please log in again.
``` **Log:** ``` [warning] Invalid authentication cookie detected ``` ## Testing ### 1. Test without cookie (should fail) ```bash curl -k https://localhost/floki/index # Expected: 401 Authentication Required ``` ### 2. Test login endpoint (should succeed) ```bash curl -k https://localhost/set # Expected: 200 "secure encrypted cookie set" # Cookie: user= ``` ### 3. Test with cookie (should succeed) ```bash # First get the cookie curl -k -c cookies.txt https://localhost/set # Then use it curl -k -b cookies.txt https://localhost/floki/index # Expected: 200 with page content ``` ### 4. Test with invalid cookie (should fail) ```bash curl -k -b "user=fake_cookie" https://localhost/floki/index # Expected: 401 Invalid session ``` ## Security Features ### Cookie Validation - **Encrypted**: Cookie value is encrypted with AES-GCM - **Tamper-proof**: Modified cookies are detected and rejected - **Secure flag**: Only sent over HTTPS - **HttpOnly flag**: Not accessible via JavaScript - **SameSite flag**: CSRF protection ### Logging - **Debug**: Successful authentication (user ID) - **Warning**: Invalid/tampered cookie attempts - Use these logs to detect brute-force or tampering attempts ### Protection Against ✅ **Session hijacking** - Encrypted cookies ✅ **Cookie tampering** - HMAC signature verification ✅ **XSS attacks** - HttpOnly flag ✅ **CSRF attacks** - SameSite=Lax flag ✅ **Replay attacks** - Add `max_age` expiration (already set: 30 days) ## Advanced Patterns ### Pattern 1: Different Auth for Different Routes ```elixir # Public routes defp require_authentication(%{path_info: ["api", "public" | _]} = conn, _opts), do: conn # Admin routes (could add additional role check here) defp require_authentication(%{path_info: ["admin" | _]} = conn, _opts) do conn = check_basic_auth(conn) check_admin_role(conn) # Additional check end # Regular routes defp require_authentication(conn, _opts) do check_basic_auth(conn) end ``` ### Pattern 2: API Key Authentication ```elixir defp require_authentication(%{path_info: ["api" | _]} = conn, _opts) do # Check for API key header instead of cookie case get_req_header(conn, "x-api-key") do [api_key] -> validate_api_key(conn, api_key) _ -> unauthorized_response(conn) end end ``` ### Pattern 3: Optional Authentication ```elixir defp require_authentication(conn, _opts) do case get_encrypted_cookie(conn) do {:ok, user_id} -> # Authenticated - add user to conn assign(conn, :user_id, user_id) {:error, _} -> # Not authenticated - but allow through as guest assign(conn, :user_id, nil) end end # Then in your route handlers, check if user is present get "/floki/index" do case conn.assigns[:user_id] do nil -> send_resp(conn, 200, "Guest view") user_id -> send_resp(conn, 200, "Authenticated view for #{user_id}") end end ``` ## Common Issues ### Issue: Static assets return 401 **Problem:** CSS/JS files are blocked **Solution:** Ensure `Plug.Static` is **before** `:require_authentication` in the pipeline ```elixir plug(Plug.Static, at: "/", from: "asset") # Must be FIRST plug(:match) plug(:require_authentication) # After Static ``` ### Issue: Need to exclude multiple routes **Solution:** Use pattern matching or a list of public paths ```elixir @public_paths [["set"], ["health"], ["api", "public"]] defp require_authentication(conn, _opts) do if conn.path_info in @public_paths do conn else check_authentication(conn) end end ``` ### Issue: Want to redirect instead of 401 **Solution:** Return 302 redirect in the plug ```elixir defp require_authentication(conn, _opts) do case validate_cookie(conn) do :ok -> conn :error -> conn |> put_resp_header("location", "/set") |> send_resp(302, "Redirecting to login...") |> halt() end end ``` ## Production Checklist - [x] Authentication middleware in place - [x] Encrypted cookies - [x] HTTPS required (secure flag) - [ ] Add session timeout/refresh logic - [ ] Store sessions in database (not just cookies) - [ ] Add rate limiting for authentication attempts - [ ] Set up monitoring for failed auth attempts - [ ] Add CSRF token validation for POST requests - [ ] Implement logout endpoint (clear cookie) - [ ] Add "remember me" functionality (longer-lived cookies) ## Next Steps 1. **Add logout route:** ```elixir get "/logout" do conn |> delete_resp_cookie("user", path: "/") |> send_resp(200, "Logged out successfully") end ``` 2. **Add session timeout:** ```elixir # In cookie options max_age: 60 * 60 * 24 * 30 # 30 days (current) # Or shorter for security: max_age: 60 * 60 * 8 # 8 hours ``` 3. **Add database-backed sessions:** ```elixir # Store session ID in cookie, actual data in database # This allows session revocation ``` ## Related Documentation - Encrypted cookies: `lib/my_mcp_server_router.ex:encrypt_cookie_value/1` - Authorization (permissions): `docs/AUTHORIZATION.md` - Main router: `lib/my_mcp_server_router.ex`