-
Notifications
You must be signed in to change notification settings - Fork 5.5k
fix oauth filter sds update #16253
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
fix oauth filter sds update #16253
Changes from 11 commits
5108686
948a0f4
6ee2773
d4b5c12
f8c5d39
f4b3755
d166c84
cd71d20
e87f907
4ac587a
05cee4f
21e7131
4da1c5b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ | |
| #include "common/http/message_impl.h" | ||
| #include "common/protobuf/message_validator_impl.h" | ||
| #include "common/protobuf/utility.h" | ||
| #include "common/secret/secret_manager_impl.h" | ||
|
|
||
| #include "extensions/filters/http/oauth2/filter.h" | ||
|
|
||
|
|
@@ -155,6 +156,81 @@ class OAuth2Test : public testing::Test { | |
| Event::SimulatedTimeSystem test_time_; | ||
| }; | ||
|
|
||
| // Verifies that the OAuth SDSSecretReader correctly updates dynamic generic secret. | ||
| TEST_F(OAuth2Test, SdsDynamicGenericSecret) { | ||
| NiceMock<Server::MockConfigTracker> config_tracker; | ||
| Secret::SecretManagerImpl secret_manager{config_tracker}; | ||
| envoy::config::core::v3::ConfigSource config_source; | ||
|
|
||
| NiceMock<Server::Configuration::MockTransportSocketFactoryContext> secret_context; | ||
| NiceMock<LocalInfo::MockLocalInfo> local_info; | ||
| Api::ApiPtr api = Api::createApiForTest(); | ||
| Stats::IsolatedStoreImpl stats; | ||
| NiceMock<Init::MockManager> init_manager; | ||
| Init::TargetHandlePtr init_handle; | ||
| NiceMock<Event::MockDispatcher> dispatcher; | ||
| EXPECT_CALL(secret_context, localInfo()).WillRepeatedly(ReturnRef(local_info)); | ||
| EXPECT_CALL(secret_context, api()).WillRepeatedly(ReturnRef(*api)); | ||
| EXPECT_CALL(secret_context, dispatcher()).WillRepeatedly(ReturnRef(dispatcher)); | ||
| EXPECT_CALL(secret_context, stats()).WillRepeatedly(ReturnRef(stats)); | ||
| EXPECT_CALL(secret_context, initManager()).WillRepeatedly(ReturnRef(init_manager)); | ||
| EXPECT_CALL(init_manager, add(_)) | ||
| .WillRepeatedly(Invoke([&init_handle](const Init::Target& target) { | ||
| init_handle = target.createHandle("test"); | ||
| })); | ||
|
|
||
| auto client_secret_provider = | ||
| secret_manager.findOrCreateGenericSecretProvider(config_source, "client", secret_context); | ||
| auto client_callback = secret_context.cluster_manager_.subscription_factory_.callbacks_; | ||
| auto token_secret_provider = | ||
| secret_manager.findOrCreateGenericSecretProvider(config_source, "token", secret_context); | ||
| auto token_callback = secret_context.cluster_manager_.subscription_factory_.callbacks_; | ||
|
|
||
| SDSSecretReader secret_reader(client_secret_provider, token_secret_provider, *api); | ||
| EXPECT_TRUE(secret_reader.clientSecret().empty()); | ||
| EXPECT_TRUE(secret_reader.tokenSecret().empty()); | ||
|
|
||
| const std::string yaml_client = R"EOF( | ||
| name: client | ||
| generic_secret: | ||
| secret: | ||
| inline_string: "client_test" | ||
| )EOF"; | ||
|
|
||
| envoy::extensions::transport_sockets::tls::v3::Secret typed_secret; | ||
| TestUtility::loadFromYaml(yaml_client, typed_secret); | ||
| const auto decoded_resources_client = TestUtility::decodeResources({typed_secret}); | ||
|
|
||
| client_callback->onConfigUpdate(decoded_resources_client.refvec_, ""); | ||
| EXPECT_EQ(secret_reader.clientSecret(), "client_test"); | ||
| EXPECT_EQ(secret_reader.tokenSecret(), ""); | ||
|
|
||
| const std::string yaml_token = R"EOF( | ||
| name: token | ||
| generic_secret: | ||
| secret: | ||
| inline_string: "token_test" | ||
| )EOF"; | ||
| TestUtility::loadFromYaml(yaml_token, typed_secret); | ||
| const auto decoded_resources_token = TestUtility::decodeResources({typed_secret}); | ||
|
|
||
| token_callback->onConfigUpdate(decoded_resources_token.refvec_, ""); | ||
| EXPECT_EQ(secret_reader.clientSecret(), "client_test"); | ||
| EXPECT_EQ(secret_reader.tokenSecret(), "token_test"); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you add another check where token stays the same, but the client secret is updated again? To make sure that an update to client secret doesn't overwrite the token. |
||
|
|
||
| const std::string yaml_client_recheck = R"EOF( | ||
| name: client | ||
| generic_secret: | ||
| secret: | ||
| inline_string: "client_test_recheck" | ||
| )EOF"; | ||
| TestUtility::loadFromYaml(yaml_client_recheck, typed_secret); | ||
| const auto decoded_resources_client_recheck = TestUtility::decodeResources({typed_secret}); | ||
|
|
||
| client_callback->onConfigUpdate(decoded_resources_client_recheck.refvec_, ""); | ||
| EXPECT_EQ(secret_reader.clientSecret(), "client_test_recheck"); | ||
| EXPECT_EQ(secret_reader.tokenSecret(), "token_test"); | ||
| } | ||
| // Verifies that we fail constructing the filter if the configured cluster doesn't exist. | ||
| TEST_F(OAuth2Test, InvalidCluster) { | ||
| ON_CALL(factory_context_.cluster_manager_, clusters()) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,11 @@ | ||
| #include "common/crypto/utility.h" | ||
| #include "common/protobuf/utility.h" | ||
|
|
||
| #include "source/extensions/filters/http/oauth2/oauth_response.pb.h" | ||
|
|
||
| #include "test/integration/http_integration.h" | ||
|
|
||
| #include "absl/strings/escaping.h" | ||
| #include "gtest/gtest.h" | ||
|
|
||
| namespace Envoy { | ||
|
|
@@ -35,7 +37,43 @@ class OauthIntegrationTest : public testing::Test, public HttpIntegrationTest { | |
| void initialize() override { | ||
| setUpstreamProtocol(FakeHttpConnection::Type::HTTP2); | ||
|
|
||
| config_helper_.addFilter(R"EOF( | ||
| TestEnvironment::writeStringToFileForTest("token_secret.yaml", R"EOF( | ||
| resources: | ||
| - "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret" | ||
| name: token | ||
| generic_secret: | ||
| secret: | ||
| inline_string: "token_secret")EOF", | ||
| false); | ||
|
|
||
| TestEnvironment::writeStringToFileForTest("token_secret_1.yaml", R"EOF( | ||
| resources: | ||
| - "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret" | ||
| name: token | ||
| generic_secret: | ||
| secret: | ||
| inline_string: "token_secret_1")EOF", | ||
| false); | ||
|
|
||
| TestEnvironment::writeStringToFileForTest("hmac_secret.yaml", R"EOF( | ||
| resources: | ||
| - "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret" | ||
| name: hmac | ||
| generic_secret: | ||
| secret: | ||
| inline_string: "hmac_secret")EOF", | ||
| false); | ||
|
|
||
| TestEnvironment::writeStringToFileForTest("hmac_secret_1.yaml", R"EOF( | ||
| resources: | ||
| - "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret" | ||
| name: hmac | ||
| generic_secret: | ||
| secret: | ||
| inline_string: "hmac_secret_1")EOF", | ||
| false); | ||
|
|
||
| config_helper_.addFilter(TestEnvironment::substitute(R"EOF( | ||
| name: oauth | ||
| typed_config: | ||
| "@type": type.googleapis.com/envoy.extensions.filters.http.oauth2.v3alpha.OAuth2 | ||
|
|
@@ -56,8 +94,14 @@ name: oauth | |
| client_id: foo | ||
| token_secret: | ||
| name: token | ||
| sds_config: | ||
| path: "{{ test_tmpdir }}/token_secret.yaml" | ||
| resource_api_version: V3 | ||
| hmac_secret: | ||
| name: hmac | ||
| sds_config: | ||
| path: "{{ test_tmpdir }}/hmac_secret.yaml" | ||
| resource_api_version: V3 | ||
| auth_scopes: | ||
| - user | ||
| - openid | ||
|
|
@@ -66,20 +110,12 @@ name: oauth | |
| - oauth2-resource | ||
| - http://example.com | ||
| - https://example.com | ||
| )EOF"); | ||
| )EOF")); | ||
|
|
||
| // Add the OAuth cluster. | ||
| config_helper_.addConfigModifier([&](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { | ||
| *bootstrap.mutable_static_resources()->add_clusters() = | ||
| config_helper_.buildStaticCluster("oauth", 0, "127.0.0.1"); | ||
|
|
||
| auto* token_secret = bootstrap.mutable_static_resources()->add_secrets(); | ||
| token_secret->set_name("token"); | ||
| token_secret->mutable_generic_secret()->mutable_secret()->set_inline_bytes("token_secret"); | ||
|
|
||
| auto* hmac_secret = bootstrap.mutable_static_resources()->add_secrets(); | ||
| hmac_secret->set_name("hmac"); | ||
| hmac_secret->mutable_generic_secret()->mutable_secret()->set_inline_bytes("hmac_secret"); | ||
| }); | ||
|
|
||
| setUpstreamCount(2); | ||
|
|
@@ -107,9 +143,66 @@ TEST_F(OauthIntegrationTest, UnauthenticatedFlow) { | |
| EXPECT_EQ("302", response->headers().getStatusValue()); | ||
| } | ||
|
|
||
| std::string parseSetCookieValue(const Http::HeaderMap& headers, const std::string& key) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you use the helper in http/utlity.h instead of redefining this here? |
||
|
|
||
| std::string ret; | ||
|
|
||
| headers.iterateReverse([&key, &ret](const Http::HeaderEntry& header) -> Http::HeaderMap::Iterate { | ||
| // Find the cookie headers in the request (typically, there's only one). | ||
| if (header.key() == Http::Headers::get().SetCookie.get()) { | ||
|
|
||
| // Split the cookie header into individual cookies. | ||
| for (const auto& s : StringUtil::splitToken(header.value().getStringView(), ";")) { | ||
| // Find the key part of the cookie (i.e. the name of the cookie). | ||
| size_t first_non_space = s.find_first_not_of(' '); | ||
| size_t equals_index = s.find('='); | ||
| if (equals_index == absl::string_view::npos) { | ||
| // The cookie is malformed if it does not have an `=`. Continue | ||
| // checking other cookies in this header. | ||
| continue; | ||
| } | ||
| const absl::string_view k = s.substr(first_non_space, equals_index - first_non_space); | ||
| // If the key matches, parse the value from the rest of the cookie string. | ||
| if (k == key) { | ||
| absl::string_view v = s.substr(equals_index + 1, s.size() - 1); | ||
|
|
||
| // Cookie values may be wrapped in double quotes. | ||
| // https://tools.ietf.org/html/rfc6265#section-4.1.1 | ||
| if (v.size() >= 2 && v.back() == '"' && v[0] == '"') { | ||
| v = v.substr(1, v.size() - 2); | ||
| } | ||
| ret = std::string{v}; | ||
| return Http::HeaderMap::Iterate::Break; | ||
| } | ||
| } | ||
| } | ||
| return Http::HeaderMap::Iterate::Continue; | ||
| }); | ||
|
|
||
| return ret; | ||
| } | ||
|
|
||
| TEST_F(OauthIntegrationTest, AuthenticationFlow) { | ||
| initialize(); | ||
|
|
||
| Filesystem::WatcherPtr watcher = dispatcher_->createFilesystemWatcher(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe extract this stuff into a setup function? Just to declutter the test file Also: does it make sense to do one authentication flow, reload the secrets, then do another one? That way we're verifying a real world secret rotation scenario |
||
| Filesystem::Watcher::OnChangedCb on_changed_cb_; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not used? |
||
| watcher->addWatch(TestEnvironment::temporaryPath("token_secret.yaml"), | ||
| Filesystem::Watcher::Events::MovedTo, | ||
| [&](uint32_t) -> void { dispatcher_->exit(); }); | ||
|
|
||
| watcher->addWatch(TestEnvironment::temporaryPath("hmac_secret.yaml"), | ||
| Filesystem::Watcher::Events::MovedTo, | ||
| [&](uint32_t) -> void { dispatcher_->exit(); }); | ||
|
|
||
| TestEnvironment::renameFile(TestEnvironment::temporaryPath("token_secret_1.yaml"), | ||
| TestEnvironment::temporaryPath("token_secret.yaml")); | ||
| dispatcher_->run(Event::Dispatcher::RunType::Block); | ||
| TestEnvironment::renameFile(TestEnvironment::temporaryPath("hmac_secret_1.yaml"), | ||
| TestEnvironment::temporaryPath("hmac_secret.yaml")); | ||
|
|
||
| dispatcher_->run(Event::Dispatcher::RunType::Block); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be simpler to wait for a stat to update that reflects the secret reload, assuming we have one? |
||
|
|
||
| codec_client_ = makeHttpConnection(lookupPort("http")); | ||
|
|
||
| Http::TestRequestHeaderMapImpl headers{ | ||
|
|
@@ -119,6 +212,8 @@ TEST_F(OauthIntegrationTest, AuthenticationFlow) { | |
| {"x-forwarded-proto", "http"}, | ||
| {":authority", "authority"}, | ||
| {"authority", "Bearer token"}}; | ||
| absl::string_view host_ = headers.Host()->value().getStringView(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The _ suffix is reserved for field members, so I'd call this |
||
|
|
||
| auto encoder_decoder = codec_client_->startRequest(headers); | ||
| request_encoder_ = &encoder_decoder.first; | ||
| auto response = std::move(encoder_decoder.second); | ||
|
|
@@ -127,6 +222,13 @@ TEST_F(OauthIntegrationTest, AuthenticationFlow) { | |
|
|
||
| ASSERT_TRUE(upstream_request_->waitForHeadersComplete()); | ||
|
|
||
| std::string request_body = upstream_request_->body().toString(); | ||
| const auto query_parameters = Http::Utility::parseFromBody(request_body); | ||
| auto it = query_parameters.find("client_secret"); | ||
|
|
||
| ASSERT_TRUE(it != query_parameters.end()); | ||
| EXPECT_EQ(it->second, "token_secret_1"); | ||
|
|
||
| upstream_request_->encodeHeaders( | ||
| Http::TestRequestHeaderMapImpl{{":status", "200"}, {"content-type", "application/json"}}, | ||
| false); | ||
|
|
@@ -140,6 +242,22 @@ TEST_F(OauthIntegrationTest, AuthenticationFlow) { | |
|
|
||
| // We should get an immediate redirect back. | ||
| response->waitForHeaders(); | ||
|
|
||
| const Http::ResponseHeaderMap& headers_ = response->headers(); | ||
| std::string expires_ = parseSetCookieValue(headers_, "OauthExpires"); | ||
| std::string token_ = parseSetCookieValue(headers_, "BearerToken"); | ||
| std::string hmac_ = parseSetCookieValue(headers_, "OauthHMAC"); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove the _ suffix since these aren't field variables |
||
| auto& crypto_util = Envoy::Common::Crypto::UtilitySingleton::get(); | ||
| const auto hmac_payload = absl::StrCat(host_, expires_, token_); | ||
| std::string hmac_secret = "hmac_secret_1"; | ||
| std::vector<uint8_t> hmac_secret_vec(hmac_secret.begin(), hmac_secret.end()); | ||
| const auto pre_encoded_hmac = | ||
| Hex::encode(crypto_util.getSha256Hmac(hmac_secret_vec, hmac_payload)); | ||
| std::string encoded_hmac; | ||
| absl::Base64Escape(pre_encoded_hmac, &encoded_hmac); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Putting this in a helper function might help readability, its quite a bit of code just to verify the hmac |
||
|
|
||
| EXPECT_EQ(encoded_hmac, hmac_) << response->headers(); | ||
|
|
||
| EXPECT_EQ("302", response->headers().getStatusValue()); | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At least this one can use the store_ defined on the fixture class instead of redefining one here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it will be more readable if we leave it in the function scope to save the context.