# 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`