From 4914aa4dc86e2c6cddcb4ab6b046cc4ab86c279e 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 | 24 ++++++-- driver/config/tls.go | 72 +++++++++++++++++++++++ driver/config/tls_test.go | 87 ++++++++++++++++++++++++++++ go.mod | 5 ++ go.sum | 8 +++ spec/config.json | 111 ++++++++++++++++++++++++++++++++++-- 6 files changed, 298 insertions(+), 9 deletions(-) create mode 100644 driver/config/tls_test.go diff --git a/consent/strategy_default.go b/consent/strategy_default.go index f54f655bdb3..e2e37b216f1 100644 --- a/consent/strategy_default.go +++ b/consent/strategy_default.go @@ -22,6 +22,7 @@ package consent import ( "context" + "github.com/ory/x/httpx" "net/http" "net/url" "strconv" @@ -60,17 +61,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, } } @@ -674,7 +688,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 a8b2f090f73..cd7fceda0df 100644 --- a/driver/config/tls.go +++ b/driver/config/tls.go @@ -3,6 +3,11 @@ package config import ( "context" "crypto/tls" + "fmt" + "strings" + + "github.com/hashicorp/go-secure-stdlib/tlsutil" + "github.com/pkg/errors" "github.com/ory/x/tlsx" ) @@ -21,6 +26,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 { @@ -58,6 +92,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) Certificate() ([]tls.Certificate, error) { return tlsx.Certificate(c.certString, c.keyString, c.certPath, c.keyPath) } 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.mod b/go.mod index 267d34f0cf9..f5f8ad0eda4 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,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.1 github.com/instana/testify v1.6.2-0.20200721153833-94b1851f4d65 github.com/jackc/pgx/v4 v4.16.1 github.com/jmoiron/sqlx v1.3.5 @@ -147,6 +148,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.10.2 // 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.12 // indirect @@ -206,6 +210,7 @@ require ( github.com/prometheus/common v0.32.1 // indirect github.com/prometheus/procfs v0.6.0 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect + github.com/ryanuber/go-glob v1.0.0 // indirect github.com/santhosh-tekuri/jsonschema v1.2.4 // indirect github.com/seatgeek/logrus-gelf-formatter v0.0.0-20210414080842-5b05eb8ff761 // indirect github.com/segmentio/backo-go v0.0.0-20200129164019-23eae7c10bd3 // indirect diff --git a/go.sum b/go.sum index 7187ed5d677..accfc0bb044 100644 --- a/go.sum +++ b/go.sum @@ -827,8 +827,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= @@ -1376,6 +1383,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/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= github.com/santhosh-tekuri/jsonschema v1.2.4 h1:hNhW8e7t+H1vgY+1QeEQpveR6D4+OwKPXCfD2aieJis= diff --git a/spec/config.json b/spec/config.json index d9d8f7769ee..f54c5a709da 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": { @@ -324,7 +415,7 @@ } }, "tls": { - "$ref": "#/definitions/tls_config" + "$ref": "#/definitions/server_tls_config" } } }, @@ -368,14 +459,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", @@ -432,6 +523,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"