Skip to content

Commit

Permalink
jwt_authn: Support extraction of JWT from Cookies in JWT Extension (#…
Browse files Browse the repository at this point in the history
…17721)

Support extraction of JWT from Cookies in JWT Extension

Added "from_cookies" config directive to jwt_authn that enables JWT extraction from request cookies.

Risk Level: low
Testing: unit tests
Docs Changes: Updated `docs/root/configuration/http/http_filters/jwt_authn_filter.rst`
Release Notes: Updated `docs/root/version_history/current.rst`
Platform Specific Features: None

Fixes #17424

Signed-off-by: Shubham Patil <theshubhamp@gmail.com>
  • Loading branch information
theshubhamp authored Sep 3, 2021
1 parent 0feeb21 commit 143083c
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 15 deletions.
15 changes: 14 additions & 1 deletion api/envoy/extensions/filters/http/jwt_authn/v3/config.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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::
//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
1 change: 1 addition & 0 deletions docs/root/version_history/current.rst
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ New Features
* http: added support for :ref:`max_requests_per_connection <envoy_v3_api_field_config.core.v3.HttpProtocolOptions.max_requests_per_connection>` for both upstream and downstream connections.
* http: sanitizing the referer header as documented :ref:`here <config_http_conn_man_headers_referer>`. 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 <envoy_v3_api_field_extensions.filters.http.jwt_authn.v3.JwtProvider.jwt_cache_config>` and its size can be specified by :ref:`jwt_cache_size <envoy_v3_api_field_extensions.filters.http.jwt_authn.v3.JwtCacheConfig.jwt_cache_size>`.
* jwt_authn: added support for extracting JWTs from request cookies using :ref:`from_cookies <envoy_v3_api_field_extensions.filters.http.jwt_authn.v3.JwtProvider.from_cookies>`.
* listener: new listener metric ``downstream_cx_transport_socket_connect_timeout`` to track transport socket timeouts.
* matcher: added :ref:`invert <envoy_v3_api_field_type.matcher.v3.MetadataMatcher.invert>` for inverting the match result in the metadata matcher.
* overload: add a new overload action that resets streams using a lot of memory. To enable the tracking of allocated bytes in buffers that a stream is using we need to configure the minimum threshold for tracking via:ref:`buffer_factory_config <envoy_v3_api_field_config.overload.v3.OverloadManager.buffer_factory_config>`. We have an overload action ``Envoy::Server::OverloadActionNameValues::ResetStreams`` that takes advantage of the tracking to reset the most expensive stream first.
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions source/extensions/filters/http/jwt_authn/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
)
Expand Down
71 changes: 58 additions & 13 deletions source/extensions/filters/http/jwt_authn/extractor.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
*
Expand All @@ -133,6 +145,8 @@ class ExtractorImpl : public Logger::Loggable<Logger::Id::jwt>, 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);

Expand Down Expand Up @@ -164,6 +178,14 @@ class ExtractorImpl : public Logger::Loggable<Logger::Id::jwt>, public Extractor
// The map of a parameter key to set of issuers specified the parameter
std::map<std::string, ParamLocationSpec> 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<std::string, CookieLocationSpec> cookie_locations_;

std::vector<LowerCaseString> forward_payload_headers_;
};

Expand All @@ -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,
Expand Down Expand Up @@ -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<JwtLocationConstPtr>
ExtractorImpl::extract(const Http::RequestHeaderMap& headers) const {
std::vector<JwtLocationConstPtr> tokens;
Expand All @@ -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<const JwtParamLocation>(
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<const JwtParamLocation>(
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<const JwtCookieLocation>(it->second, location_spec.issuer_checker_));
}
}
}

return tokens;
}

Expand Down
33 changes: 33 additions & 0 deletions test/extensions/filters/http/jwt_authn/extractor_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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{
Expand Down

0 comments on commit 143083c

Please sign in to comment.