diff --git a/consent/strategy_default.go b/consent/strategy_default.go index 0de9ac2b168..15f0999ea3b 100644 --- a/consent/strategy_default.go +++ b/consent/strategy_default.go @@ -12,6 +12,8 @@ import ( "strings" "time" + "github.com/ory/x/httpx" + "github.com/gorilla/sessions" "github.com/pborman/uuid" "github.com/pkg/errors" @@ -38,17 +40,30 @@ const ( ) type DefaultStrategy struct { - c *config.DefaultProvider - r InternalRegistry + c *config.DefaultProvider + r InternalRegistry + httpClientOptions httpx.ResilientOptions } func NewStrategy( r InternalRegistry, c *config.DefaultProvider, ) *DefaultStrategy { + httpClientTlsConfig, err := c.TLSClientConfigWithDefaultFallback(config.KeyPrefixClientBackChannelLogout) + if err != nil { + r.Logger().WithError(err).Fatalf("Unable to setup back-channel logout http client TLS configuration.") + } + httpClientOptions := httpx.ResilientClientWithClient(&http.Client{ + Timeout: time.Minute, + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + TLSClientConfig: httpClientTlsConfig, + }, + }) return &DefaultStrategy{ - c: c, - r: r, + c: c, + r: r, + httpClientOptions: httpClientOptions, } } @@ -688,7 +703,7 @@ func (s *DefaultStrategy) executeBackChannelLogout(ctx context.Context, r *http. WithField("client_id", t.clientID). WithField("backchannel_logout_url", t.url) - res, err := s.r.HTTPClient(ctx).PostForm(t.url, url.Values{"logout_token": {t.token}}) + res, err := s.r.HTTPClient(ctx, s.httpClientOptions).PostForm(t.url, url.Values{"logout_token": {t.token}}) if err != nil { log.WithError(err).Error("Unable to execute OpenID Connect Back-Channel Logout Request") return diff --git a/driver/config/tls.go b/driver/config/tls.go index 078ef6ec26d..56b84938221 100644 --- a/driver/config/tls.go +++ b/driver/config/tls.go @@ -6,7 +6,10 @@ package config import ( "context" "crypto/tls" + "fmt" + "strings" + "github.com/hashicorp/go-secure-stdlib/tlsutil" "github.com/pkg/errors" "github.com/ory/x/logrusx" @@ -27,6 +30,35 @@ const ( KeyTLSCertPath = "serve." + KeySuffixTLSCertPath KeyTLSKeyPath = "serve." + KeySuffixTLSKeyPath KeyTLSEnabled = "serve." + KeySuffixTLSEnabled + + KeyClientTLSInsecureSkipVerify = "tls.insecure_skip_verify" + KeySuffixClientTLSCipherSuites = "tls.cipher_suites" + KeySuffixClientTLSMinVer = "tls.min_version" + KeySuffixClientTLSMaxVer = "tls.max_version" +) + +type ClientInterface interface { + Key(suffix string) string +} + +func (iface *clientPrefix) Key(suffix string) string { + return fmt.Sprintf("%s.%s", iface.prefix, suffix) +} + +type clientPrefix struct { + prefix string +} + +var ( + KeyPrefixClientDefault ClientInterface = &clientPrefix{ + prefix: "client.default", + } + KeyPrefixClientBackChannelLogout ClientInterface = &clientPrefix{ + prefix: "client.back_channel_logout", + } + KeyPrefixClientRefreshTokenHook ClientInterface = &clientPrefix{ + prefix: "client.refresh_token_hook", + } ) type TLSConfig interface { @@ -66,6 +98,44 @@ func (p *DefaultProvider) TLS(ctx context.Context, iface ServeInterface) TLSConf } } +func (p *DefaultProvider) TLSClientConfigDefault() (*tls.Config, error) { + return p.TLSClientConfigWithDefaultFallback(KeyPrefixClientDefault) +} + +func (p *DefaultProvider) TLSClientConfigWithDefaultFallback(iface ClientInterface) (*tls.Config, error) { + tlsClientConfig := new(tls.Config) + tlsClientConfig.InsecureSkipVerify = p.p.BoolF(KeyClientTLSInsecureSkipVerify, false) + + if p.p.Exists(KeyPrefixClientDefault.Key(KeySuffixClientTLSCipherSuites)) || p.p.Exists(iface.Key(KeySuffixClientTLSCipherSuites)) { + keyCipherSuites := p.p.StringsF(iface.Key(KeySuffixClientTLSCipherSuites), p.p.Strings(KeyPrefixClientDefault.Key(KeySuffixClientTLSCipherSuites))) + cipherSuites, err := tlsutil.ParseCiphers(strings.Join(keyCipherSuites[:], ",")) + if err != nil { + return nil, errors.WithMessage(err, "Unable to setup client TLS configuration") + } + tlsClientConfig.CipherSuites = cipherSuites + } + + if p.p.Exists(KeyPrefixClientDefault.Key(KeySuffixClientTLSMinVer)) || p.p.Exists(iface.Key(KeySuffixClientTLSMinVer)) { + keyMinVer := p.p.StringF(iface.Key(KeySuffixClientTLSMinVer), p.p.String(KeyPrefixClientDefault.Key(KeySuffixClientTLSMinVer))) + if tlsMinVer, found := tlsutil.TLSLookup[keyMinVer]; !found { + return nil, errors.Errorf("Unable to setup client TLS configuration. Invalid minimum TLS version: %s", keyMinVer) + } else { + tlsClientConfig.MinVersion = tlsMinVer + } + } + + if p.p.Exists(KeyPrefixClientDefault.Key(KeySuffixClientTLSMaxVer)) || p.p.Exists(iface.Key(KeySuffixClientTLSMaxVer)) { + keyMaxVer := p.p.StringF(iface.Key(KeySuffixClientTLSMaxVer), p.p.String(KeyPrefixClientDefault.Key(KeySuffixClientTLSMaxVer))) + if tlsMaxVer, found := tlsutil.TLSLookup[keyMaxVer]; !found { + return nil, errors.Errorf("Unable to setup client TLS configuration. Invalid maximum TLS version: %s", keyMaxVer) + } else { + tlsClientConfig.MaxVersion = tlsMaxVer + } + } + + return tlsClientConfig, nil +} + func (c *tlsConfig) GetCertificateFunc(stopReload <-chan struct{}, log *logrusx.Logger) (func(*tls.ClientHelloInfo) (*tls.Certificate, error), error) { if c.certPath != "" && c.keyPath != "" { // attempt to load from disk first (enables hot-reloading) ctx, cancel := context.WithCancel(context.Background()) diff --git a/driver/config/tls_test.go b/driver/config/tls_test.go new file mode 100644 index 00000000000..4a92da6db77 --- /dev/null +++ b/driver/config/tls_test.go @@ -0,0 +1,90 @@ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package config + +import ( + "context" + "crypto/tls" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/ory/x/configx" + "github.com/ory/x/logrusx" +) + +func TestTLSClientConfig_CipherSuite(t *testing.T) { + l := logrusx.New("", "") + c := MustNew(context.TODO(), l, configx.WithValue("client.default.tls.cipher_suites", []string{"TLS_AES_128_GCM_SHA256", "TLS_AES_256_GCM_SHA384"})) + + tlsClientConfig, err := c.TLSClientConfigDefault() + assert.NoError(t, err) + cipherSuites := tlsClientConfig.CipherSuites + + assert.Len(t, cipherSuites, 2) + assert.Equal(t, tls.TLS_AES_128_GCM_SHA256, cipherSuites[0]) + assert.Equal(t, tls.TLS_AES_256_GCM_SHA384, cipherSuites[1]) +} + +func TestTLSClientConfig_InvalidCipherSuite(t *testing.T) { + l := logrusx.New("", "") + c := MustNew(context.TODO(), l, configx.WithValue("client.default.tls.cipher_suites", []string{"TLS_AES_128_GCM_SHA256", "TLS_INVALID_CIPHER_SUITE"})) + + _, err := c.TLSClientConfigDefault() + + assert.EqualError(t, err, "Unable to setup client TLS configuration: unsupported cipher \"TLS_INVALID_CIPHER_SUITE\"") +} + +func TestTLSClientConfig_MinVersion(t *testing.T) { + l := logrusx.New("", "") + c := MustNew(context.TODO(), l, configx.WithValue("client.default.tls.min_version", "tls13")) + + tlsClientConfig, err := c.TLSClientConfigDefault() + + assert.NoError(t, err) + assert.Equal(t, uint16(tls.VersionTLS13), tlsClientConfig.MinVersion) +} + +func TestTLSClientConfig_InvalidMinVersion(t *testing.T) { + l := logrusx.New("", "") + c := MustNew(context.TODO(), l, configx.WithValue("client.default.tls.min_version", "tlsx")) + + _, err := c.TLSClientConfigDefault() + + assert.EqualError(t, err, "Unable to setup client TLS configuration. Invalid minimum TLS version: tlsx") +} + +func TestTLSClientConfig_MaxVersion(t *testing.T) { + l := logrusx.New("", "") + c := MustNew(context.TODO(), l, configx.WithValue("client.default.tls.max_version", "tls10")) + + tlsClientConfig, err := c.TLSClientConfigDefault() + + assert.NoError(t, err) + assert.Equal(t, uint16(tls.VersionTLS10), tlsClientConfig.MaxVersion) +} + +func TestTLSClientConfig_InvalidMaxTlsVersion(t *testing.T) { + l := logrusx.New("", "") + c := MustNew(context.TODO(), l, configx.WithValue("client.default.tls.max_version", "tlsx")) + + _, err := c.TLSClientConfigDefault() + + assert.EqualError(t, err, "Unable to setup client TLS configuration. Invalid maximum TLS version: tlsx") +} + +func TestTLSClientConfig_WithDefaultFallback(t *testing.T) { + l := logrusx.New("", "") + c := MustNew(context.TODO(), l) + ctx := context.Background() + c.MustSet(ctx, "client.default.tls.min_version", "tls11") + c.MustSet(ctx, "client.default.tls.max_version", "tls12") + c.MustSet(ctx, "client.back_channel_logout.tls.max_version", "tls13") + + tlsClientConfig, err := c.TLSClientConfigWithDefaultFallback(KeyPrefixClientBackChannelLogout) + + assert.NoError(t, err) + assert.Equal(t, uint16(tls.VersionTLS11), tlsClientConfig.MinVersion) + assert.Equal(t, uint16(tls.VersionTLS13), tlsClientConfig.MaxVersion) +} diff --git a/go.mod b/go.mod index f51d544667e..25694b28c00 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,7 @@ require ( github.com/gorilla/sessions v1.2.1 github.com/gtank/cryptopasta v0.0.0-20170601214702-1f550f6f2f69 github.com/hashicorp/go-retryablehttp v0.7.1 + github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.2 github.com/instana/testify v1.6.2-0.20200721153833-94b1851f4d65 github.com/jackc/pgx/v4 v4.17.2 github.com/julienschmidt/httprouter v1.3.0 @@ -138,6 +139,9 @@ require ( github.com/gorilla/handlers v1.5.1 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.12.0 // indirect + github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1 // indirect + github.com/hashicorp/go-secure-stdlib/strutil v0.1.1 // indirect + github.com/hashicorp/go-sockaddr v1.0.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/huandu/xstrings v1.3.2 // indirect github.com/imdario/mergo v0.3.13 // indirect @@ -189,6 +193,7 @@ require ( github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect + github.com/ryanuber/go-glob v1.0.0 // indirect github.com/seatgeek/logrus-gelf-formatter v0.0.0-20210414080842-5b05eb8ff761 // indirect github.com/segmentio/backo-go v1.0.1 // indirect github.com/sergi/go-diff v1.2.0 // indirect diff --git a/go.sum b/go.sum index 0155c3ecaf6..ef3da17e2a8 100644 --- a/go.sum +++ b/go.sum @@ -516,7 +516,14 @@ github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1 h1:78ki3QBevHwYrVxnyVeaEz+7WtifHhauYF23es/0KlI= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.1 h1:nd0HIW15E6FG1MsnArYaHfuw9C2zgzM8LxkG5Ty/788= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U= +github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.2 h1:phcbL8urUzF/kxA/Oj6awENaRwfWsjP59GW7u2qlDyY= +github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.2/go.mod h1:l8slYwnJA26yBz+ErHpp2IRCLr0vuOMGBORIz4rRiAs= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= @@ -912,6 +919,7 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sawadashota/encrypta v0.0.2 h1:R46/RxYmYdxI3VOt63B637OVBHzu+fazPyLo5CqK6QE= diff --git a/spec/config.json b/spec/config.json index 3d63b69a2c4..ad8c9ea9671 100644 --- a/spec/config.json +++ b/spec/config.json @@ -216,7 +216,7 @@ "1h5m1s" ] }, - "tls_config": { + "server_tls_config": { "type": "object", "description": "Configures HTTPS (HTTP over TLS). If configured, the server automatically supports HTTP/2.", "properties": { @@ -248,6 +248,97 @@ } } } + }, + "client_tls_config": { + "type": "object", + "additionalProperties": false, + "description": "Configures http client TLS settings.", + "properties": { + "insecure_skip_verify": { + "type": "boolean", + "description": "Controls whether a client verifies the server's certificate chain and host name." + }, + "min_version": { + "type": "string", + "description": "Minimum supported TLS version.", + "examples": [ + "tls10", + "tls11", + "tls12", + "tls13" + ] + }, + "max_version": { + "type": "string", + "description": "Maximum supported TLS version.", + "examples": [ + "tls10", + "tls11", + "tls12", + "tls13" + ] + }, + "cipher_suites": { + "type": "array", + "description": "A list of supported cipher suites.", + "items": { + "type": "string" + }, + "examples": [ + "TLS_RSA_WITH_RC4_128_SHA", + "TLS_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_RSA_WITH_AES_128_CBC_SHA", + "TLS_RSA_WITH_AES_256_CBC_SHA", + "TLS_RSA_WITH_AES_128_CBC_SHA256", + "TLS_RSA_WITH_AES_128_GCM_SHA256", + "TLS_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_RSA_WITH_RC4_128_SHA", + "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", + "TLS_AES_128_GCM_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256" + ] + } + } + }, + "grantJwt": { + "type": "object", + "additionalProperties": false, + "description": "Authorization Grants using JWT configuration", + "properties": { + "jti_optional": { + "type": "boolean", + "description": "If false, JTI claim must be present in JWT assertion.", + "default": false + }, + "iat_optional": { + "type": "boolean", + "description": "If false, IAT claim must be present in JWT assertion.", + "default": false + }, + "max_ttl": { + "description": "Configures what the maximum age of a JWT assertion can be. Uses JWT's EXP claim and JWT IAT claim to calculate assertion age. Assertion, that exceeds max age will be denied. Useful as a safety measure and recommended to not be set to 720h max.", + "default": "720h", + "allOf": [ + { + "$ref": "#/definitions/duration" + } + ] + } + } } }, "properties": { @@ -336,7 +427,7 @@ } }, "tls": { - "$ref": "#/definitions/tls_config" + "$ref": "#/definitions/server_tls_config" } } }, @@ -380,14 +471,14 @@ "tls": { "allOf": [ { - "$ref": "#/definitions/tls_config" + "$ref": "#/definitions/server_tls_config" } ] } } }, "tls": { - "$ref": "#/definitions/tls_config" + "$ref": "#/definitions/server_tls_config" }, "cookies": { "type": "object", @@ -456,6 +547,18 @@ } } }, + "client": { + "default": { + "tls": { + "$ref": "#/definitions/client_tls_config" + } + }, + "back_channel_logout": { + "tls": { + "$ref": "#/definitions/client_tls_config" + } + } + }, "dsn": { "type": "string", "description": "Sets the data source name. This configures the backend where Ory Hydra persists data. If dsn is `memory`, data will be written to memory and is lost when you restart this instance. Ory Hydra supports popular SQL databases. For more detailed configuration information go to: https://www.ory.sh/docs/hydra/dependencies-environment#sql"