Skip to content
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

jwt: do not concatenate duplicated headers #32248

Merged
merged 7 commits into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ bug_fixes:
Fix crash due to uncaught exception when the operating system does not support an address type (such as IPv6) that is
received in an mTLS client cert IP SAN. These SANs will be ignored. This applies only when using formatter
``%DOWNSTREAM_PEER_IP_SAN%``.
- area: jwt_authn
change: |
Fixed JWT extractor, which concatenated headers with a comma, resultig in invalid tokens.

removed_config_or_runtime:
# *Normally occurs at the end of the* :ref:`deprecation period <deprecated>`
Expand Down
25 changes: 13 additions & 12 deletions source/extensions/filters/http/jwt_authn/extractor.cc
Original file line number Diff line number Diff line change
Expand Up @@ -253,20 +253,21 @@ ExtractorImpl::extract(const Http::RequestHeaderMap& headers) const {
for (const auto& location_it : header_locations_) {
const auto& location_spec = location_it.second;
ENVOY_LOG(debug, "extract {}", location_it.first);
const auto result =
Http::HeaderUtility::getAllOfHeaderAsString(headers, location_spec->header_);
if (result.result().has_value()) {
auto value_str = result.result().value();
if (!location_spec->value_prefix_.empty()) {
const auto pos = value_str.find(location_spec->value_prefix_);
if (pos == absl::string_view::npos) {
// value_prefix not found anywhere in value_str, so skip
continue;
auto header_value = headers.get(location_spec->header_);
if (!header_value.empty()) {
for (size_t i = 0; i < header_value.size(); i++) {
auto value_str = header_value[i]->value().getStringView();
if (!location_spec->value_prefix_.empty()) {
const auto pos = value_str.find(location_spec->value_prefix_);
if (pos == absl::string_view::npos) {
// value_prefix not found anywhere in value_str, so skip
continue;
}
value_str = extractJWT(value_str, pos + location_spec->value_prefix_.length());
}
value_str = extractJWT(value_str, pos + location_spec->value_prefix_.length());
tokens.push_back(std::make_unique<const JwtHeaderLocation>(
std::string(value_str), location_spec->issuer_checker_, location_spec->header_));
}
tokens.push_back(std::make_unique<const JwtHeaderLocation>(
std::string(value_str), location_spec->issuer_checker_, location_spec->header_));
}
}

Expand Down
31 changes: 28 additions & 3 deletions test/extensions/filters/http/jwt_authn/extractor_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,30 @@ TEST_F(ExtractorTest, TestDefaultHeaderLocationWithValidJWT) {
EXPECT_TRUE(tokens[0]->isIssuerAllowed("issuer1"));
}

TEST_F(ExtractorTest, TestDuplicatedHeadersWithDuplicatedTokenPrefixes) {
auto headers = TestRequestHeaderMapImpl{{"Authorization", absl::StrCat("Bearer ", GoodToken)},
{"Authorization", absl::StrCat("Bearer ", GoodToken)}};
auto tokens = extractor_->extract(headers);
EXPECT_EQ(tokens.size(), 2);

// Only the issue1 is using default header location.
EXPECT_EQ(tokens[0]->token(), GoodToken);
EXPECT_TRUE(tokens[0]->isIssuerAllowed("issuer1"));
EXPECT_EQ(tokens[1]->token(), GoodToken);
EXPECT_TRUE(tokens[1]->isIssuerAllowed("issuer1"));
}

TEST_F(ExtractorTest, TestDuplicatedHeadersWithUniqueTokenPrefixes) {
auto headers = TestRequestHeaderMapImpl{{"Authorization", absl::StrCat("Bearer ", GoodToken)},
{"Authorization", absl::StrCat("Basic ", "basic")}};
auto tokens = extractor_->extract(headers);
EXPECT_EQ(tokens.size(), 1);

// Only the issue1 is using default header location.
EXPECT_EQ(tokens[0]->token(), GoodToken);
EXPECT_TRUE(tokens[0]->isIssuerAllowed("issuer1"));
}

// Test extracting JWT as Bearer token from the default header location: "Authorization" -
// using an actual (correctly-formatted) JWT but token is invalid, like: GoodToken +
// chars_after_space expected to get all token include characters after the space:
Expand Down Expand Up @@ -203,12 +227,13 @@ TEST_F(ExtractorTest, TestCustomHeaderToken) {
EXPECT_FALSE(headers.has(Http::LowerCaseString("token-header")));
}

// Make sure a double custom header concatenates the token
// Make sure a double custom header does not concatenate the token
TEST_F(ExtractorTest, TestDoubleCustomHeaderToken) {
auto headers = TestRequestHeaderMapImpl{{"token-header", "jwt_token"}, {"token-header", "foo"}};
auto tokens = extractor_->extract(headers);
EXPECT_EQ(tokens.size(), 1);
EXPECT_EQ(tokens[0]->token(), "jwt_token,foo");
EXPECT_EQ(tokens.size(), 2);
EXPECT_EQ(tokens[0]->token(), "jwt_token");
EXPECT_EQ(tokens[1]->token(), "foo");
}

// Test extracting token from the custom header: "prefix-header"
Expand Down
Loading