From 088339fc287a24de23ffcfe7985287cef2a3b2fa Mon Sep 17 00:00:00 2001 From: Maksim Nabokikh Date: Mon, 11 Mar 2024 22:48:20 +0100 Subject: [PATCH] Add headers control to dex web server (#3339) Customization of headers in the authentication server is crucial for enforcing stringent security measures by allowing the inclusion of specific headers required for authentication protocols and compliance standards. This customization ensures that authentication requests are processed securely, mitigating potential vulnerabilities and ensuring adherence to security policies. Signed-off-by: m.nabokikh --- cmd/dex/config.go | 48 ++++++++++++++++++++++++++++++++++++++++ cmd/dex/config_test.go | 5 +++++ cmd/dex/serve.go | 1 + examples/config-dev.yaml | 7 ++++++ server/server.go | 18 ++++++++++++--- server/server_test.go | 22 ++++++++++++++++++ 6 files changed, 98 insertions(+), 3 deletions(-) diff --git a/cmd/dex/config.go b/cmd/dex/config.go index 9c5c1039..33c0db47 100644 --- a/cmd/dex/config.go +++ b/cmd/dex/config.go @@ -4,6 +4,7 @@ import ( "encoding/base64" "encoding/json" "fmt" + "net/http" "os" "strings" @@ -153,6 +154,7 @@ type OAuth2 struct { type Web struct { HTTP string `json:"http"` HTTPS string `json:"https"` + Headers Headers `json:"headers"` TLSCert string `json:"tlsCert"` TLSKey string `json:"tlsKey"` TLSMinVersion string `json:"tlsMinVersion"` @@ -161,6 +163,52 @@ type Web struct { AllowedHeaders []string `json:"allowedHeaders"` } +type Headers struct { + // Set the Content-Security-Policy header to HTTP responses. + // Unset if blank. + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy + ContentSecurityPolicy string `json:"Content-Security-Policy"` + // Set the X-Frame-Options header to HTTP responses. + // Unset if blank. Accepted values are deny and sameorigin. + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options + XFrameOptions string `json:"X-Frame-Options"` + // Set the X-Content-Type-Options header to HTTP responses. + // Unset if blank. Accepted value is nosniff. + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options + XContentTypeOptions string `json:"X-Content-Type-Options"` + // Set the X-XSS-Protection header to all responses. + // Unset if blank. + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection + XXSSProtection string `json:"X-XSS-Protection"` + // Set the Strict-Transport-Security header to HTTP responses. + // Unset if blank. + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security + StrictTransportSecurity string `json:"Strict-Transport-Security"` +} + +func (h *Headers) ToHTTPHeader() http.Header { + if h == nil { + return make(map[string][]string) + } + header := make(map[string][]string) + if h.ContentSecurityPolicy != "" { + header["Content-Security-Policy"] = []string{h.ContentSecurityPolicy} + } + if h.XFrameOptions != "" { + header["X-Frame-Options"] = []string{h.XFrameOptions} + } + if h.XContentTypeOptions != "" { + header["X-Content-Type-Options"] = []string{h.XContentTypeOptions} + } + if h.XXSSProtection != "" { + header["X-XSS-Protection"] = []string{h.XXSSProtection} + } + if h.StrictTransportSecurity != "" { + header["Strict-Transport-Security"] = []string{h.StrictTransportSecurity} + } + return header +} + // Telemetry is the config format for telemetry including the HTTP server config. type Telemetry struct { HTTP string `json:"http"` diff --git a/cmd/dex/config_test.go b/cmd/dex/config_test.go index 16803d6d..e3169650 100644 --- a/cmd/dex/config_test.go +++ b/cmd/dex/config_test.go @@ -74,6 +74,8 @@ web: https: 127.0.0.1:5556 tlsMinVersion: 1.3 tlsMaxVersion: 1.2 + headers: + Strict-Transport-Security: "max-age=31536000; includeSubDomains" frontend: dir: ./web @@ -149,6 +151,9 @@ logger: HTTPS: "127.0.0.1:5556", TLSMinVersion: "1.3", TLSMaxVersion: "1.2", + Headers: Headers{ + StrictTransportSecurity: "max-age=31536000; includeSubDomains", + }, }, Frontend: server.WebConfig{ Dir: "./web", diff --git a/cmd/dex/serve.go b/cmd/dex/serve.go index 3494443e..9461a622 100644 --- a/cmd/dex/serve.go +++ b/cmd/dex/serve.go @@ -278,6 +278,7 @@ func runServe(options serveOptions) error { SkipApprovalScreen: c.OAuth2.SkipApprovalScreen, AlwaysShowLoginScreen: c.OAuth2.AlwaysShowLoginScreen, PasswordConnector: c.OAuth2.PasswordConnector, + Headers: c.Web.Headers.ToHTTPHeader(), AllowedOrigins: c.Web.AllowedOrigins, AllowedHeaders: c.Web.AllowedHeaders, Issuer: c.Issuer, diff --git a/examples/config-dev.yaml b/examples/config-dev.yaml index 956aa84c..8f1018ff 100644 --- a/examples/config-dev.yaml +++ b/examples/config-dev.yaml @@ -52,6 +52,13 @@ web: # https: 127.0.0.1:5554 # tlsCert: /etc/dex/tls.crt # tlsKey: /etc/dex/tls.key + # headers: + # X-Frame-Options: "DENY" + # X-Content-Type-Options: "nosniff" + # X-XSS-Protection: "1; mode=block" + # Content-Security-Policy: "default-src 'self'" + # Strict-Transport-Security: "max-age=31536000; includeSubDomains" + # Configuration for dex appearance # frontend: diff --git a/server/server.go b/server/server.go index bb9da17b..1eaf1915 100644 --- a/server/server.go +++ b/server/server.go @@ -72,6 +72,9 @@ type Config struct { // flow. If no response types are supplied this value defaults to "code". SupportedResponseTypes []string + // Headers is a map of headers to be added to the all responses. + Headers http.Header + // List of allowed origins for CORS requests on discovery, token and keys endpoint. // If none are indicated, CORS requests are disabled. Passing in "*" will allow any // domain. @@ -345,9 +348,18 @@ func newServer(ctx context.Context, c Config, rotationStrategy rotationStrategy) } } + handlerWithHeaders := func(handlerName string, handler http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + for k, v := range c.Headers { + w.Header()[k] = v + } + instrumentHandlerCounter(handlerName, handler)(w, r) + } + } + r := mux.NewRouter().SkipClean(true).UseEncodedPath() handle := func(p string, h http.Handler) { - r.Handle(path.Join(issuerURL.Path, p), instrumentHandlerCounter(p, h)) + r.Handle(path.Join(issuerURL.Path, p), handlerWithHeaders(p, h)) } handleFunc := func(p string, h http.HandlerFunc) { handle(p, h) @@ -365,7 +377,7 @@ func newServer(ctx context.Context, c Config, rotationStrategy rotationStrategy) ) handler = cors(handler) } - r.Handle(path.Join(issuerURL.Path, p), instrumentHandlerCounter(p, handler)) + r.Handle(path.Join(issuerURL.Path, p), handlerWithHeaders(p, handler)) } r.NotFoundHandler = http.NotFoundHandler() @@ -388,7 +400,7 @@ func newServer(ctx context.Context, c Config, rotationStrategy rotationStrategy) // TODO(nabokihms): "/device/token" endpoint is deprecated, consider using /token endpoint instead handleFunc("/device/token", s.handleDeviceTokenDeprecated) handleFunc(deviceCallbackURI, s.handleDeviceCallback) - r.HandleFunc(path.Join(issuerURL.Path, "/callback"), func(w http.ResponseWriter, r *http.Request) { + handleFunc("/callback", func(w http.ResponseWriter, r *http.Request) { // Strip the X-Remote-* headers to prevent security issues on // misconfigured authproxy connector setups. for key := range r.Header { diff --git a/server/server_test.go b/server/server_test.go index f39d64d3..25d1909d 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -1799,3 +1799,25 @@ func TestServerSupportedGrants(t *testing.T) { }) } } + +func TestHeaders(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + httpServer, _ := newTestServer(ctx, t, func(c *Config) { + c.Headers = map[string][]string{ + "Strict-Transport-Security": {"max-age=31536000; includeSubDomains"}, + } + }) + defer httpServer.Close() + + p, err := oidc.NewProvider(ctx, httpServer.URL) + if err != nil { + t.Fatalf("failed to get provider: %v", err) + } + + resp, err := http.Get(p.Endpoint().TokenURL) + require.NoError(t, err) + + require.Equal(t, "max-age=31536000; includeSubDomains", resp.Header.Get("Strict-Transport-Security")) +}