Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add headers control to dex web server #3339

Merged
merged 1 commit into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions cmd/dex/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"os"
"strings"

Expand Down Expand Up @@ -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"`
Expand All @@ -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"`
Expand Down
5 changes: 5 additions & 0 deletions cmd/dex/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions cmd/dex/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions examples/config-dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
18 changes: 15 additions & 3 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand All @@ -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()

Expand All @@ -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 {
Expand Down
22 changes: 22 additions & 0 deletions server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
}
Loading