Files
coder/coderd/httpmw/cors.go
Thomas Kosiewski 071383bbe8 feat: add RFC 9728 OAuth2 resource metadata support (#18920)
# Enhanced OAuth2 and MCP Compliance for API Authentication

This PR improves OAuth2 and MCP (Microsoft Cloud for Sovereignty)
compliance by:

1. Adding RFC 9728 compliant `WWW-Authenticate` headers with resource
metadata URLs
2. Passing the configured `AccessURL` to API key middleware for proper
audience validation
3. Creating specialized CORS handling for OAuth2 and MCP endpoints with
appropriate headers
4. Making the `state` parameter optional in OAuth2 authorization
requests

These changes ensure proper OAuth2 token audience validation against the
configured access URL and improve interoperability with OAuth2 clients
by providing better error responses and metadata discovery.

Signed-off-by: Thomas Kosiewski <tk@coder.com>
2025-07-19 22:05:15 +02:00

123 lines
3.3 KiB
Go

package httpmw
import (
"net/http"
"net/url"
"regexp"
"strings"
"github.com/go-chi/cors"
"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
)
const (
// Server headers.
AccessControlAllowOriginHeader = "Access-Control-Allow-Origin"
AccessControlAllowCredentialsHeader = "Access-Control-Allow-Credentials"
AccessControlAllowMethodsHeader = "Access-Control-Allow-Methods"
AccessControlAllowHeadersHeader = "Access-Control-Allow-Headers"
VaryHeader = "Vary"
// Client headers.
OriginHeader = "Origin"
AccessControlRequestMethodsHeader = "Access-Control-Request-Methods"
AccessControlRequestHeadersHeader = "Access-Control-Request-Headers"
)
//nolint:revive
func Cors(allowAll bool, origins ...string) func(next http.Handler) http.Handler {
if len(origins) == 0 {
// The default behavior is '*', so putting the empty string defaults to
// the secure behavior of blocking CORS requests.
origins = []string{""}
}
if allowAll {
origins = []string{"*"}
}
// Standard CORS for most endpoints
standardCors := cors.Handler(cors.Options{
AllowedOrigins: origins,
// We only need GET for latency requests
AllowedMethods: []string{http.MethodOptions, http.MethodGet},
AllowedHeaders: []string{"Accept", "Content-Type", "X-LATENCY-CHECK", "X-CSRF-TOKEN"},
// Do not send any cookies
AllowCredentials: false,
})
// Permissive CORS for OAuth2 and MCP endpoints
permissiveCors := cors.Handler(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{
http.MethodGet,
http.MethodPost,
http.MethodDelete,
http.MethodOptions,
},
AllowedHeaders: []string{
"Content-Type",
"Accept",
"Authorization",
"x-api-key",
"Mcp-Session-Id",
"MCP-Protocol-Version",
"Last-Event-ID",
},
ExposedHeaders: []string{
"Content-Type",
"Authorization",
"x-api-key",
"Mcp-Session-Id",
"MCP-Protocol-Version",
},
MaxAge: 86400, // 24 hours in seconds
AllowCredentials: false,
})
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Use permissive CORS for OAuth2, MCP, and well-known endpoints
if strings.HasPrefix(r.URL.Path, "/oauth2/") ||
strings.HasPrefix(r.URL.Path, "/api/experimental/mcp/") ||
strings.HasPrefix(r.URL.Path, "/.well-known/oauth-") {
permissiveCors(next).ServeHTTP(w, r)
return
}
// Use standard CORS for all other endpoints
standardCors(next).ServeHTTP(w, r)
})
}
}
func WorkspaceAppCors(regex *regexp.Regexp, app appurl.ApplicationURL) func(next http.Handler) http.Handler {
return cors.Handler(cors.Options{
AllowOriginFunc: func(_ *http.Request, rawOrigin string) bool {
origin, err := url.Parse(rawOrigin)
if rawOrigin == "" || origin.Host == "" || err != nil {
return false
}
subdomain, ok := appurl.ExecuteHostnamePattern(regex, origin.Host)
if !ok {
return false
}
originApp, err := appurl.ParseSubdomainAppURL(subdomain)
if err != nil {
return false
}
return ok && originApp.Username == app.Username
},
AllowedMethods: []string{
http.MethodHead,
http.MethodGet,
http.MethodPost,
http.MethodPut,
http.MethodPatch,
http.MethodDelete,
},
AllowedHeaders: []string{"*"},
AllowCredentials: true,
})
}