diff --git a/api/envoy/extensions/filters/http/jwt_authn/v3/config.proto b/api/envoy/extensions/filters/http/jwt_authn/v3/config.proto index 9e658ed8627f..9718dbe0550a 100644 --- a/api/envoy/extensions/filters/http/jwt_authn/v3/config.proto +++ b/api/envoy/extensions/filters/http/jwt_authn/v3/config.proto @@ -52,7 +52,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // cache_duration: // seconds: 300 // -// [#next-free-field: 13] +// [#next-free-field: 14] message JwtProvider { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.http.jwt_authn.v2alpha.JwtProvider"; @@ -181,6 +181,19 @@ message JwtProvider { // repeated string from_params = 7; + // JWT is sent in a cookie. `from_cookies` represents the cookie names to extract from. + // + // For example, if config is: + // + // .. code-block:: yaml + // + // from_cookies: + // - auth-token + // + // Then JWT will be extracted from `auth-token` cookie in the request. + // + repeated string from_cookies = 13; + // This field specifies the header name to forward a successfully verified JWT payload to the // backend. The forwarded data is:: // diff --git a/docs/root/configuration/http/http_filters/jwt_authn_filter.rst b/docs/root/configuration/http/http_filters/jwt_authn_filter.rst index 905aaa6aecc4..b98e5f73edb9 100644 --- a/docs/root/configuration/http/http_filters/jwt_authn_filter.rst +++ b/docs/root/configuration/http/http_filters/jwt_authn_filter.rst @@ -40,6 +40,7 @@ JwtProvider * *forward*: if true, JWT will be forwarded to the upstream. * *from_headers*: extract JWT from HTTP headers. * *from_params*: extract JWT from query parameters. +* *from_cookies*: extract JWT from HTTP request cookies. * *forward_payload_header*: forward the JWT payload in the specified HTTP header. * *jwt_cache_config*: Enables JWT cache, its size can be specified by *jwt_cache_size*. Only valid JWT tokens are cached. diff --git a/docs/root/version_history/current.rst b/docs/root/version_history/current.rst index 71c41c24694b..e4968a532ac7 100644 --- a/docs/root/version_history/current.rst +++ b/docs/root/version_history/current.rst @@ -93,6 +93,7 @@ New Features * http: added support for :ref:`max_requests_per_connection ` for both upstream and downstream connections. * http: sanitizing the referer header as documented :ref:`here `. This feature can be temporarily turned off by setting runtime guard ``envoy.reloadable_features.sanitize_http_header_referer`` to false. * jwt_authn: added support for :ref:`Jwt Cache ` and its size can be specified by :ref:`jwt_cache_size `. +* jwt_authn: added support for extracting JWTs from request cookies using :ref:`from_cookies `. * listener: new listener metric ``downstream_cx_transport_socket_connect_timeout`` to track transport socket timeouts. * rbac: added :ref:`destination_port_range ` for matching range of destination ports. * route config: added :ref:`dynamic_metadata ` for routing based on dynamic metadata. diff --git a/generated_api_shadow/envoy/extensions/filters/http/jwt_authn/v3/config.proto b/generated_api_shadow/envoy/extensions/filters/http/jwt_authn/v3/config.proto index 9e658ed8627f..9718dbe0550a 100644 --- a/generated_api_shadow/envoy/extensions/filters/http/jwt_authn/v3/config.proto +++ b/generated_api_shadow/envoy/extensions/filters/http/jwt_authn/v3/config.proto @@ -52,7 +52,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // cache_duration: // seconds: 300 // -// [#next-free-field: 13] +// [#next-free-field: 14] message JwtProvider { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.http.jwt_authn.v2alpha.JwtProvider"; @@ -181,6 +181,19 @@ message JwtProvider { // repeated string from_params = 7; + // JWT is sent in a cookie. `from_cookies` represents the cookie names to extract from. + // + // For example, if config is: + // + // .. code-block:: yaml + // + // from_cookies: + // - auth-token + // + // Then JWT will be extracted from `auth-token` cookie in the request. + // + repeated string from_cookies = 13; + // This field specifies the header name to forward a successfully verified JWT payload to the // backend. The forwarded data is:: // diff --git a/source/extensions/filters/http/jwt_authn/BUILD b/source/extensions/filters/http/jwt_authn/BUILD index 50f5952b2f70..915011c82f4b 100644 --- a/source/extensions/filters/http/jwt_authn/BUILD +++ b/source/extensions/filters/http/jwt_authn/BUILD @@ -16,6 +16,7 @@ envoy_cc_library( deps = [ "//source/common/http:header_utility_lib", "//source/common/http:utility_lib", + "@com_google_absl//absl/container:btree", "@envoy_api//envoy/extensions/filters/http/jwt_authn/v3:pkg_cc_proto", ], ) diff --git a/source/extensions/filters/http/jwt_authn/extractor.cc b/source/extensions/filters/http/jwt_authn/extractor.cc index 8b78af98d0a9..bfa03f0ac4f0 100644 --- a/source/extensions/filters/http/jwt_authn/extractor.cc +++ b/source/extensions/filters/http/jwt_authn/extractor.cc @@ -10,7 +10,7 @@ #include "source/common/http/utility.h" #include "source/common/singleton/const_singleton.h" -#include "absl/container/node_hash_set.h" +#include "absl/container/btree_map.h" #include "absl/strings/match.h" using envoy::extensions::filters::http::jwt_authn::v3::JwtProvider; @@ -111,6 +111,18 @@ class JwtParamLocation : public JwtLocationBase { } }; +// The JwtLocation for cookie extraction. +class JwtCookieLocation : public JwtLocationBase { +public: + JwtCookieLocation(const std::string& token, const JwtIssuerChecker& issuer_checker) + : JwtLocationBase(token, issuer_checker) {} + + void removeJwt(Http::HeaderMap&) const override { + // TODO(theshubhamp): remove JWT from cookies. + NOT_IMPLEMENTED_GCOVR_EXCL_LINE; + } +}; + /** * The class implements Extractor interface * @@ -133,6 +145,8 @@ class ExtractorImpl : public Logger::Loggable, public Extractor const std::string& value_prefix); // add a query param config void addQueryParamConfig(const std::string& issuer, const std::string& param); + // add a query param config + void addCookieConfig(const std::string& issuer, const std::string& cookie); // ctor helper for a jwt provider config void addProvider(const JwtProvider& provider); @@ -164,6 +178,14 @@ class ExtractorImpl : public Logger::Loggable, public Extractor // The map of a parameter key to set of issuers specified the parameter std::map param_locations_; + // CookieMap value type to store issuers that specified this cookie. + struct CookieLocationSpec { + // Issuers that specified this param. + JwtIssuerChecker issuer_checker_; + }; + // The map of a cookie key to set of issuers specified the cookie. + absl::btree_map cookie_locations_; + std::vector forward_payload_headers_; }; @@ -183,6 +205,9 @@ void ExtractorImpl::addProvider(const JwtProvider& provider) { for (const std::string& param : provider.from_params()) { addQueryParamConfig(provider.issuer(), param); } + for (const std::string& cookie : provider.from_cookies()) { + addCookieConfig(provider.issuer(), cookie); + } // If not specified, use default locations. if (provider.from_headers().empty() && provider.from_params().empty()) { addHeaderConfig(provider.issuer(), Http::CustomHeaders::get().Authorization, @@ -210,6 +235,11 @@ void ExtractorImpl::addQueryParamConfig(const std::string& issuer, const std::st param_location_spec.issuer_checker_.add(issuer); } +void ExtractorImpl::addCookieConfig(const std::string& issuer, const std::string& cookie) { + auto& cookie_location_spec = cookie_locations_[cookie]; + cookie_location_spec.issuer_checker_.add(issuer); +} + std::vector ExtractorImpl::extract(const Http::RequestHeaderMap& headers) const { std::vector tokens; @@ -235,22 +265,37 @@ ExtractorImpl::extract(const Http::RequestHeaderMap& headers) const { } } - // If no query parameter locations specified, or Path() is null, bail out - if (param_locations_.empty() || headers.Path() == nullptr) { - return tokens; + // Check query parameter locations only if query parameter locations specified and Path() is not + // null + if (!param_locations_.empty() && headers.Path() != nullptr) { + const auto& params = Http::Utility::parseAndDecodeQueryString(headers.getPathValue()); + for (const auto& location_it : param_locations_) { + const auto& param_key = location_it.first; + const auto& location_spec = location_it.second; + const auto& it = params.find(param_key); + if (it != params.end()) { + tokens.push_back(std::make_unique( + it->second, location_spec.issuer_checker_, param_key)); + } + } } - // Check query parameter locations. - const auto& params = Http::Utility::parseAndDecodeQueryString(headers.getPathValue()); - for (const auto& location_it : param_locations_) { - const auto& param_key = location_it.first; - const auto& location_spec = location_it.second; - const auto& it = params.find(param_key); - if (it != params.end()) { - tokens.push_back(std::make_unique( - it->second, location_spec.issuer_checker_, param_key)); + // Check cookie locations. + if (!cookie_locations_.empty()) { + const auto& cookies = Http::Utility::parseCookies( + headers, [&](absl::string_view k) -> bool { return cookie_locations_.contains(k); }); + + for (const auto& location_it : cookie_locations_) { + const auto& cookie_key = location_it.first; + const auto& location_spec = location_it.second; + const auto& it = cookies.find(cookie_key); + if (it != cookies.end()) { + tokens.push_back( + std::make_unique(it->second, location_spec.issuer_checker_)); + } } } + return tokens; } diff --git a/test/extensions/filters/http/jwt_authn/extractor_test.cc b/test/extensions/filters/http/jwt_authn/extractor_test.cc index b7dee6776f55..2adaf35e7e46 100644 --- a/test/extensions/filters/http/jwt_authn/extractor_test.cc +++ b/test/extensions/filters/http/jwt_authn/extractor_test.cc @@ -54,6 +54,15 @@ const char ExampleConfig[] = R"( from_headers: - name: prefix-header value_prefix: '"CCCDDD"' + provider9: + issuer: issuer9 + from_cookies: + - token-cookie + - token-cookie-2 + provider10: + issuer: issuer10 + from_cookies: + - token-cookie-3 )"; class ExtractorTest : public testing::Test { @@ -265,6 +274,30 @@ TEST_F(ExtractorTest, TestCustomParamToken) { tokens[0]->removeJwt(headers); } +// Test extracting token from a cookie +TEST_F(ExtractorTest, TestCookieToken) { + auto headers = TestRequestHeaderMapImpl{ + {"cookie", "token-cookie=token-cookie-value; token-cookie-2=token-cookie-value-2"}, + {"cookie", "token-cookie-3=\"token-cookie-value-3\""}}; + auto tokens = extractor_->extract(headers); + EXPECT_EQ(tokens.size(), 3); + + // only issuer9 has specified "token-cookie" cookie location. + EXPECT_EQ(tokens[0]->token(), "token-cookie-value"); + EXPECT_TRUE(tokens[0]->isIssuerAllowed("issuer9")); + EXPECT_FALSE(tokens[0]->isIssuerAllowed("issuer10")); + + // only issuer9 has specified "token-cookie-2" cookie location. + EXPECT_EQ(tokens[1]->token(), "token-cookie-value-2"); + EXPECT_TRUE(tokens[1]->isIssuerAllowed("issuer9")); + EXPECT_FALSE(tokens[1]->isIssuerAllowed("issuer10")); + + // only issuer10 has specified "token-cookie-3" cookie location. + EXPECT_EQ(tokens[2]->token(), "token-cookie-value-3"); + EXPECT_TRUE(tokens[2]->isIssuerAllowed("issuer10")); + EXPECT_FALSE(tokens[2]->isIssuerAllowed("issuer9")); +} + // Test extracting multiple tokens. TEST_F(ExtractorTest, TestMultipleTokens) { auto headers = TestRequestHeaderMapImpl{