Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import "envoy/config/core/v3/base.proto";

import "udpa/annotations/sensitive.proto";
import "udpa/annotations/status.proto";
import "validate/validate.proto";

option java_package = "io.envoyproxy.envoy.extensions.filters.http.basic_auth.v3";
option java_outer_classname = "BasicAuthProto";
Expand Down Expand Up @@ -33,4 +34,11 @@ message BasicAuth {
// The value needs to be the htpasswd format.
// Reference to https://httpd.apache.org/docs/2.4/programs/htpasswd.html
config.core.v3.DataSource users = 1 [(udpa.annotations.sensitive) = true];

// This field specifies the header name to forward a successfully authenticated user to
// the backend. The header will be added to the request with the username as the value.
//
// If it is not specified, the username will not be forwarded.
string forward_username_header = 2
[(validate.rules).string = {well_known_regex: HTTP_HEADER_NAME strict: false}];
}
4 changes: 4 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,10 @@ removed_config_or_runtime:
removed ``envoy_reloadable_features_initialize_upstream_filters`` and legacy code paths.

new_features:
- area: basic_auth
change: |
Added :ref:`forward_username_header <envoy_v3_api_field_extensions.filters.http.basic_auth.v3.BasicAuth.forward_username_header>`
config to forward the username to the backend.
- area: ext_proc
change: |
Added
Expand Down
10 changes: 8 additions & 2 deletions source/extensions/filters/http/basic_auth/basic_auth_filter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ std::string computeSHA1(absl::string_view password) {

} // namespace

FilterConfig::FilterConfig(UserMap&& users, const std::string& stats_prefix, Stats::Scope& scope)
: users_(std::move(users)), stats_(generateStats(stats_prefix + "basic_auth.", scope)) {}
FilterConfig::FilterConfig(UserMap&& users, const std::string& forward_username_header,
const std::string& stats_prefix, Stats::Scope& scope)
: users_(std::move(users)), forward_username_header_(forward_username_header),
stats_(generateStats(stats_prefix + "basic_auth.", scope)) {}

bool FilterConfig::validateUser(absl::string_view username, absl::string_view password) const {
auto user = users_.find(username);
Expand Down Expand Up @@ -75,6 +77,10 @@ Http::FilterHeadersStatus BasicAuthFilter::decodeHeaders(Http::RequestHeaderMap&
"invalid_credential_for_basic_auth");
}

if (!config_->forwardUsernameHeader().empty()) {
headers.setCopy(Http::LowerCaseString(config_->forwardUsernameHeader()), username);
}

config_->stats().allowed_.inc();
return Http::FilterHeadersStatus::Continue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,19 @@ using UserMap = absl::flat_hash_map<std::string, User>;
*/
class FilterConfig {
public:
FilterConfig(UserMap&& users, const std::string& stats_prefix, Stats::Scope& scope);
FilterConfig(UserMap&& users, const std::string& forward_username_header,
const std::string& stats_prefix, Stats::Scope& scope);
const BasicAuthStats& stats() const { return stats_; }
bool validateUser(absl::string_view username, absl::string_view password) const;
const std::string& forwardUsernameHeader() const { return forward_username_header_; }

private:
static BasicAuthStats generateStats(const std::string& prefix, Stats::Scope& scope) {
return BasicAuthStats{ALL_BASIC_AUTH_STATS(POOL_COUNTER_PREFIX(scope, prefix))};
}

const UserMap users_;
const std::string forward_username_header_;
BasicAuthStats stats_;
};
using FilterConfigConstSharedPtr = std::shared_ptr<const FilterConfig>;
Expand Down
4 changes: 2 additions & 2 deletions source/extensions/filters/http/basic_auth/config.cc
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ Http::FilterFactoryCb BasicAuthFilterFactory::createFilterFactoryFromProtoTyped(
UserMap users = readHtpasswd(THROW_OR_RETURN_VALUE(
Config::DataSource::read(proto_config.users(), false, context.serverFactoryContext().api()),
std::string));
FilterConfigConstSharedPtr config =
std::make_unique<FilterConfig>(std::move(users), stats_prefix, context.scope());
FilterConfigConstSharedPtr config = std::make_unique<FilterConfig>(
std::move(users), proto_config.forward_username_header(), stats_prefix, context.scope());
return [config](Http::FilterChainFactoryCallbacks& callbacks) -> void {
callbacks.addStreamDecoderFilter(std::make_shared<BasicAuthFilter>(config));
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ name: envoy.filters.http.basic_auth
inline_string: |-
user1:{SHA}tESsBmE/yNY3lb6a0L6vVQEZNqw=
user2:{SHA}EJ9LPFDXsN9ynSmbxvjp75Bmlx8=
forward_username_header: x-username
)EOF";
config_helper_.prependFilter(filter_config);
initialize();
Expand Down Expand Up @@ -51,6 +52,11 @@ TEST_P(BasicAuthIntegrationTestAllProtocols, ValidCredential) {
});

waitForNextUpstreamRequest();

const auto username_entry = upstream_request_->headers().get(Http::LowerCaseString("x-username"));
EXPECT_FALSE(username_entry.empty());
EXPECT_EQ(username_entry[0]->value().getStringView(), "user1");

upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, true);
ASSERT_TRUE(response->waitForEndStream());
ASSERT_TRUE(response->complete());
Expand Down Expand Up @@ -112,6 +118,33 @@ TEST_P(BasicAuthIntegrationTestAllProtocols, NoneExistedUser) {
EXPECT_EQ("401", response->headers().getStatusValue());
EXPECT_EQ("User authentication failed. Invalid username/password combination.", response->body());
}

// Request with existing username header
TEST_P(BasicAuthIntegrationTestAllProtocols, ExistingUsernameHeader) {
initializeFilter();
codec_client_ = makeHttpConnection(lookupPort("http"));

auto response = codec_client_->makeHeaderOnlyRequest(Http::TestRequestHeaderMapImpl{
{":method", "GET"},
{":path", "/"},
{":scheme", "http"},
{":authority", "host"},
{"Authorization", "Basic dXNlcjE6dGVzdDE="}, // user1, test1
{"x-username", "existingUsername"},
});

waitForNextUpstreamRequest();

const auto username_entry = upstream_request_->headers().get(Http::LowerCaseString("x-username"));
EXPECT_FALSE(username_entry.empty());
EXPECT_EQ(username_entry[0]->value().getStringView(), "user1");

upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, true);
ASSERT_TRUE(response->waitForEndStream());
ASSERT_TRUE(response->complete());
EXPECT_EQ("200", response->headers().getStatusValue());
}

} // namespace
} // namespace BasicAuth
} // namespace HttpFilters
Expand Down
15 changes: 14 additions & 1 deletion test/extensions/filters/http/basic_auth/filter_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ class FilterTest : public testing::Test {
UserMap users;
users.insert({"user1", {"user1", "tESsBmE/yNY3lb6a0L6vVQEZNqw="}}); // user1:test1
users.insert({"user2", {"user2", "EJ9LPFDXsN9ynSmbxvjp75Bmlx8="}}); // user2:test2
config_ = std::make_unique<FilterConfig>(std::move(users), "stats", *stats_.rootScope());
config_ = std::make_unique<FilterConfig>(std::move(users), "x-username", "stats",
*stats_.rootScope());
filter_ = std::make_shared<BasicAuthFilter>(config_);
filter_->setDecoderFilterCallbacks(decoder_filter_callbacks_);
}
Expand All @@ -35,12 +36,14 @@ TEST_F(FilterTest, BasicAuth) {

EXPECT_EQ(Http::FilterHeadersStatus::Continue,
filter_->decodeHeaders(request_headers_user1, true));
EXPECT_EQ("user1", request_headers_user1.get_("x-username"));

// user2:test2
Http::TestRequestHeaderMapImpl request_headers_user2{{"Authorization", "Basic dXNlcjI6dGVzdDI="}};

EXPECT_EQ(Http::FilterHeadersStatus::Continue,
filter_->decodeHeaders(request_headers_user2, true));
EXPECT_EQ("user2", request_headers_user2.get_("x-username"));
}

TEST_F(FilterTest, UserNotExist) {
Expand Down Expand Up @@ -130,6 +133,16 @@ TEST_F(FilterTest, HasAuthHeaderButNoColon) {
filter_->decodeHeaders(request_headers_user1, true));
}

TEST_F(FilterTest, ExistingUsernameHeader) {
// user1:test1
Http::TestRequestHeaderMapImpl request_headers_user1{{"Authorization", "Basic dXNlcjE6dGVzdDE="},
{"x-username", "existingUsername"}};

EXPECT_EQ(Http::FilterHeadersStatus::Continue,
filter_->decodeHeaders(request_headers_user1, true));
EXPECT_EQ("user1", request_headers_user1.get_("x-username"));
}

} // namespace BasicAuth
} // namespace HttpFilters
} // namespace Extensions
Expand Down