()
| 959 | } |
| 960 | |
| 961 | func (s *Server) setupRouter() { |
| 962 | r := chi.NewRouter() |
| 963 | |
| 964 | // Infrastructure middleware (applies to all routes including /metrics) |
| 965 | // CapturePeerAddr MUST run before TrustedProxyRealIP so downstream code |
| 966 | // that needs to verify the real TCP peer (e.g. the bootstrap loopback |
| 967 | // check) can read the untampered value from request context even on |
| 968 | // deployments with a trusted reverse proxy in front. |
| 969 | r.Use(CapturePeerAddr) |
| 970 | // RealIP is gated on PAD_TRUSTED_PROXIES. When unset (the default), proxy |
| 971 | // headers are ignored and the real TCP peer address is used everywhere. |
| 972 | // This prevents X-Forwarded-For spoofing from bypassing rate limits, the |
| 973 | // bootstrap loopback check, or audit logs on direct-exposed deployments. |
| 974 | r.Use(TrustedProxyRealIP(s.trustedProxyCIDRs)) |
| 975 | r.Use(chimiddleware.RequestID) |
| 976 | r.Use(StructuredLogger) |
| 977 | if s.metrics != nil { |
| 978 | r.Use(MetricsMiddleware(s.metrics)) |
| 979 | } |
| 980 | r.Use(chimiddleware.Recoverer) |
| 981 | |
| 982 | // Security headers (applies to all routes) |
| 983 | r.Use(SecurityHeaders) |
| 984 | if s.secureCookies { |
| 985 | r.Use(StrictTransportSecurity) |
| 986 | } |
| 987 | |
| 988 | // MCP Streamable HTTP transport + OAuth discovery endpoints |
| 989 | // (PLAN-943 TASK-950). Mounted outside the standard /api/v1 |
| 990 | // auth-required group because: |
| 991 | // |
| 992 | // - /mcp uses Bearer auth via its own MCPBearerAuth middleware, |
| 993 | // producing the spec-shape 401 + WWW-Authenticate that MCP |
| 994 | // clients expect (the API-stack 401 envelope is JSON-only and |
| 995 | // would fail Claude Desktop's discovery handshake). |
| 996 | // - /.well-known/oauth-protected-resource and |
| 997 | // /.well-known/oauth-authorization-server are public discovery |
| 998 | // documents (RFC 9728 / RFC 8414); routing them through |
| 999 | // TokenAuth+SessionAuth+RequireAuth would 401 unauth probes. |
| 1000 | // |
| 1001 | // No-op when SetMCPTransport hasn't been called or cloud mode is |
| 1002 | // off — see registerMCPRoutes for the gating. |
| 1003 | s.registerMCPRoutes(r) |
| 1004 | |
| 1005 | // OAuth 2.1 authorization-server flow endpoints (PLAN-943 |
| 1006 | // TASK-1025 sub-PR C). /oauth/{register,authorize,token, |
| 1007 | // authorize/decide} mounted alongside /mcp + /.well-known/*, |
| 1008 | // outside /api/v1's auth-required group. CSRF middleware runs |
| 1009 | // only on /api/* paths so /oauth/* is naturally exempt; the |
| 1010 | // consent-decision endpoint adds its own form-token check |
| 1011 | // using the existing __Host-pad_csrf cookie. |
| 1012 | // |
| 1013 | // SessionAuth runs in this group so /oauth/authorize can detect |
| 1014 | // whether the user is logged in via the __Host-pad_session |
| 1015 | // cookie. SessionAuth falls through gracefully when no cookie |
| 1016 | // is present (handlers see currentUser(r)==nil and redirect to |
| 1017 | // /login). RequireAuth is intentionally NOT used — /oauth/authorize |
| 1018 | // must be reachable anonymously to trigger the login redirect. |
no test coverage detected