From 59bf5810bad935176d3ee56c744aa202d9780776 Mon Sep 17 00:00:00 2001 From: Mart Aarma Date: Mon, 29 Nov 2021 09:22:44 +0200 Subject: [PATCH] feat: back-channel logout request client tls configuration --- consent/strategy_default.go | 25 ++++++-- driver/config/tls.go | 70 +++++++++++++++++++++++ driver/config/tls_test.go | 87 ++++++++++++++++++++++++++++ go.sum | 8 +++ spec/config.json | 111 ++++++++++++++++++++++++++++++++++-- 5 files changed, 292 insertions(+), 9 deletions(-) create mode 100644 driver/config/tls_test.go diff --git a/consent/strategy_default.go b/consent/strategy_default.go index ecbeccb76e9..60c3bd4cd61 100644 --- a/consent/strategy_default.go +++ b/consent/strategy_default.go @@ -14,6 +14,8 @@ import ( "github.com/twmb/murmur3" + "github.com/ory/x/httpx" + "github.com/ory/hydra/driver/config" "github.com/ory/x/errorsx" @@ -46,17 +48,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, } } @@ -696,7 +711,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..fefb22c1f7a --- /dev/null +++ b/driver/config/tls_test.go @@ -0,0 +1,87 @@ +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.sum b/go.sum index db4ff29fbfc..97fd2a4bb38 100644 --- a/go.sum +++ b/go.sum @@ -539,8 +539,15 @@ github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1 github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= 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-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.1 h1:Yc026VyMyIpq1UWRnakHRG01U8fJm+nEfEmjoAb00n8= +github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.1/go.mod h1:l8slYwnJA26yBz+ErHpp2IRCLr0vuOMGBORIz4rRiAs= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= 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= @@ -956,6 +963,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/santhosh-tekuri/jsonschema v1.2.4 h1:hNhW8e7t+H1vgY+1QeEQpveR6D4+OwKPXCfD2aieJis= github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4= diff --git a/spec/config.json b/spec/config.json index d0a02bbc6cd..149fc85a3f2 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", @@ -444,6 +535,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"