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

HTTP Basic Auth filter #30079

Merged
merged 27 commits into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
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
2 changes: 2 additions & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@ extensions/filters/http/oauth2 @derekargueta @mattklein123
/*/extensions/filters/http/rate_limit_quota @tyxia @yanavlasov
# HTTP Bandwidth Limit
/*/extensions/filters/http/bandwidth_limit @nitgoy @mattklein123 @yanavlasov @tonya11en
# HTTP Basic Auth
/*/extensions/filters/http/basic_auth @zhaohuabing @wbpcode
# Original IP detection
/*/extensions/http/original_ip_detection/custom_header @alyssawilk @mattklein123
/*/extensions/http/original_ip_detection/xff @alyssawilk @mattklein123
Expand Down
1 change: 1 addition & 0 deletions api/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ proto_library(
"//envoy/extensions/filters/http/aws_lambda/v3:pkg",
"//envoy/extensions/filters/http/aws_request_signing/v3:pkg",
"//envoy/extensions/filters/http/bandwidth_limit/v3:pkg",
"//envoy/extensions/filters/http/basic_auth/v3:pkg",
"//envoy/extensions/filters/http/buffer/v3:pkg",
"//envoy/extensions/filters/http/cache/v3:pkg",
"//envoy/extensions/filters/http/cdn_loop/v3:pkg",
Expand Down
12 changes: 12 additions & 0 deletions api/envoy/extensions/filters/http/basic_auth/v3/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py.

load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package")

licenses(["notice"]) # Apache 2

api_proto_package(
deps = [
"//envoy/config/core/v3:pkg",
"@com_github_cncf_udpa//udpa/annotations:pkg",
],
)
36 changes: 36 additions & 0 deletions api/envoy/extensions/filters/http/basic_auth/v3/basic_auth.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
syntax = "proto3";

package envoy.extensions.filters.http.basic_auth.v3;

import "envoy/config/core/v3/base.proto";

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

option java_package = "io.envoyproxy.envoy.extensions.filters.http.basic_auth.v3";
option java_outer_classname = "BasicAuthProto";
option java_multiple_files = true;
option go_package = "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/basic_auth/v3;basic_authv3";
option (udpa.annotations.file_status).package_version_status = ACTIVE;

// [#protodoc-title: Basic Auth]
// Basic Auth :ref:`configuration overview <config_http_filters_basic_auth>`.
// [#extension: envoy.filters.http.basic_auth]

// Basic HTTP authentication.
//
// Example:
//
// .. code-block:: yaml
//
// users:
// inline_string: |-
// user1:{SHA}hashed_user1_password
// user2:{SHA}hashed_user2_password
//
message BasicAuth {
// Username-password pairs used to verify user credentials in the "Authorization" header.
// 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];
}
1 change: 1 addition & 0 deletions api/versioning/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ proto_library(
"//envoy/extensions/filters/http/aws_lambda/v3:pkg",
"//envoy/extensions/filters/http/aws_request_signing/v3:pkg",
"//envoy/extensions/filters/http/bandwidth_limit/v3:pkg",
"//envoy/extensions/filters/http/basic_auth/v3:pkg",
"//envoy/extensions/filters/http/buffer/v3:pkg",
"//envoy/extensions/filters/http/cache/v3:pkg",
"//envoy/extensions/filters/http/cdn_loop/v3:pkg",
Expand Down
4 changes: 4 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ removed_config_or_runtime:
runtime flag and legacy code path.

new_features:
- area: filters
change: |
Added :ref:`the Basic Auth filter <envoy_v3_api_msg_extensions.filters.http.basic_auth.v3.BasicAuth>`, which can be used to
authenticate user credentials in the HTTP Authentication heaer defined in `RFC7617 <https://tools.ietf.org/html/rfc7617>`_.
- area: upstream
change: |
Added :ref:`enable_full_scan <envoy_v3_api_msg_extensions.load_balancing_policies.least_request.v3.LeastRequest>`
Expand Down
46 changes: 46 additions & 0 deletions docs/root/configuration/http/http_filters/basic_auth_filter.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
.. _config_http_filters_basic_auth:

Basic Auth
==========

This HTTP filter can be used to authenticate user credentials in the HTTP Authentication header defined
in `RFC7617 <https://tools.ietf.org/html/rfc7617>`.

The filter will extract the username and password from the HTTP Authentication header and verify them
against the configured username and password list.

If the username and password are valid, the request will be forwared to the next filter in the filter chains.
If they're invalid or not provided in the HTTP request, the request will be denied with a 401 Unauthorized response.

Configuration
-------------

* This filter should be configured with the type URL ``type.googleapis.com/envoy.extensions.filters.http.basic_auth.v3.BasicAuth``.
* :ref:`v3 API reference <envoy_v3_api_msg_extensions.filters.http.basic_auth.v3.BasicAuth>`

``users`` is a list of username-password pairs used to verify user credentials in the "Authorization" header.
The value needs to be the `htpasswd <https://httpd.apache.org/docs/2.4/programs/htpasswd.html>` format.


An example configuration of the filter may look like the following:

.. code-block:: yaml

users:
inline_string: |-
user1:{SHA}hashed_user1_password
user2:{SHA}hashed_user2_password

Note that only SHA format is currently supported. Other formats may be added in the future.

Statistics
----------

The HTTP basic auth filter outputs statistics in the ``http.<stat_prefix>.basic_auth.`` namespace.

.. csv-table::
:header: Name, Type, Description
:widths: 1, 1, 2

allowed, Counter, Total number of allowed requests
denied, Counter, Total number of denied requests
1 change: 1 addition & 0 deletions docs/root/configuration/http/http_filters/http_filters.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ HTTP filters
aws_lambda_filter
aws_request_signing_filter
bandwidth_limit_filter
basic_auth_filter
buffer_filter
cache_filter
cdn_loop_filter
Expand Down
1 change: 1 addition & 0 deletions source/common/common/logger.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const static bool should_log = true;
FUNCTION(aws) \
FUNCTION(assert) \
FUNCTION(backtrace) \
FUNCTION(basic_auth) \
FUNCTION(cache_filter) \
FUNCTION(client) \
FUNCTION(config) \
Expand Down
1 change: 1 addition & 0 deletions source/extensions/extensions_build_config.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ EXTENSIONS = {
"envoy.filters.http.aws_lambda": "//source/extensions/filters/http/aws_lambda:config",
"envoy.filters.http.aws_request_signing": "//source/extensions/filters/http/aws_request_signing:config",
"envoy.filters.http.bandwidth_limit": "//source/extensions/filters/http/bandwidth_limit:config",
"envoy.filters.http.basic_auth": "//source/extensions/filters/http/basic_auth:config",
"envoy.filters.http.buffer": "//source/extensions/filters/http/buffer:config",
"envoy.filters.http.cache": "//source/extensions/filters/http/cache:config",
"envoy.filters.http.cdn_loop": "//source/extensions/filters/http/cdn_loop:config",
Expand Down
7 changes: 7 additions & 0 deletions source/extensions/extensions_metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,13 @@ envoy.filters.http.bandwidth_limit:
status: stable
type_urls:
- envoy.extensions.filters.http.bandwidth_limit.v3.BandwidthLimit
envoy.filters.http.basic_auth:
categories:
- envoy.filters.http
security_posture: robust_to_untrusted_downstream
status: alpha
type_urls:
- envoy.extensions.filters.http.basic_auth.v3.BasicAuth
envoy.filters.http.buffer:
categories:
- envoy.filters.http
Expand Down
39 changes: 39 additions & 0 deletions source/extensions/filters/http/basic_auth/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
load(
"//bazel:envoy_build_system.bzl",
"envoy_cc_extension",
"envoy_cc_library",
"envoy_extension_package",
)

licenses(["notice"]) # Apache 2

envoy_extension_package()

envoy_cc_library(
name = "basic_auth_lib",
srcs = ["basic_auth_filter.cc"],
hdrs = ["basic_auth_filter.h"],
external_deps = ["ssl"],
deps = [
"//envoy/server:filter_config_interface",
"//source/common/common:base64_lib",
"//source/common/config:utility_lib",
"//source/common/http:header_map_lib",
"//source/common/protobuf:utility_lib",
"//source/extensions/filters/http/common:pass_through_filter_lib",
],
)

envoy_cc_extension(
name = "config",
srcs = ["config.cc"],
hdrs = ["config.h"],
deps = [
":basic_auth_lib",
"//envoy/registry",
"//source/common/config:datasource_lib",
"//source/common/protobuf:utility_lib",
"//source/extensions/filters/http/common:factory_base_lib",
"@envoy_api//envoy/extensions/filters/http/basic_auth/v3:pkg_cc_proto",
],
)
91 changes: 91 additions & 0 deletions source/extensions/filters/http/basic_auth/basic_auth_filter.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#include "source/extensions/filters/http/basic_auth/basic_auth_filter.h"

#include <openssl/sha.h>

#include "source/common/common/base64.h"
#include "source/common/http/header_utility.h"
#include "source/common/http/headers.h"

namespace Envoy {
namespace Extensions {
namespace HttpFilters {
namespace BasicAuth {

namespace {

// Function to compute SHA1 hash
std::string computeSHA1(absl::string_view password) {
unsigned char hash[SHA_DIGEST_LENGTH];

// Calculate the SHA-1 hash
SHA1(reinterpret_cast<const unsigned char*>(password.data()), password.length(), hash);

// Encode the binary hash in Base64
return Base64::encode(reinterpret_cast<const char*>(hash), SHA_DIGEST_LENGTH);
}

} // namespace

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

bool FilterConfig::validateUser(absl::string_view username, absl::string_view password) const {
auto user = users_->find(username);
if (user == users_->end()) {
return false;
}

return computeSHA1(password) == user->second.hash;
}

BasicAuthFilter::BasicAuthFilter(FilterConfigConstSharedPtr config) : config_(std::move(config)) {}

Http::FilterHeadersStatus BasicAuthFilter::decodeHeaders(Http::RequestHeaderMap& headers, bool) {
auto auth_header = headers.get(Http::CustomHeaders::get().Authorization);
if (!auth_header.empty()) {
absl::string_view auth_value = auth_header[0]->value().getStringView();

if (absl::StartsWith(auth_value, "Basic ")) {
// Extract and decode the Base64 part of the header.
absl::string_view base64Token = auth_value.substr(6);
const std::string decoded = Base64::decodeWithoutPadding(base64Token);

// The decoded string is in the format "username:password".
const size_t colon_pos = decoded.find(':');

if (colon_pos != std::string::npos) {
absl::string_view decoded_view = decoded;
absl::string_view username = decoded_view.substr(0, colon_pos);
absl::string_view password = decoded_view.substr(colon_pos + 1);

if (config_->validateUser(username, password)) {
config_->stats().allowed_.inc();
return Http::FilterHeadersStatus::Continue;
} else {
config_->stats().denied_.inc();
decoder_callbacks_->sendLocalReply(
Http::Code::Unauthorized,
"User authentication failed. Invalid username/password combination", nullptr,
absl::nullopt, "invalid_credential_for_basic_auth");
return Http::FilterHeadersStatus::StopIteration;
}
}
}
}

config_->stats().denied_.inc();
decoder_callbacks_->sendLocalReply(Http::Code::Unauthorized,
"User authentication failed. Missing username and password",
nullptr, absl::nullopt, "no_credential_for_basic_auth");
return Http::FilterHeadersStatus::StopIteration;
}

void BasicAuthFilter::setDecoderFilterCallbacks(Http::StreamDecoderFilterCallbacks& callbacks) {
decoder_callbacks_ = &callbacks;
}

} // namespace BasicAuth
} // namespace HttpFilters
} // namespace Extensions
} // namespace Envoy
80 changes: 80 additions & 0 deletions source/extensions/filters/http/basic_auth/basic_auth_filter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#pragma once

#include "envoy/stats/stats_macros.h"

#include "source/common/common/logger.h"
#include "source/extensions/filters/http/common/pass_through_filter.h"

#include "absl/container/flat_hash_map.h"

namespace Envoy {
namespace Extensions {
namespace HttpFilters {
namespace BasicAuth {

/**
* All Basic Auth filter stats. @see stats_macros.h
*/
#define ALL_BASIC_AUTH_STATS(COUNTER) \
COUNTER(allowed) \
COUNTER(denied)

/**
* Struct definition for Basic Auth stats. @see stats_macros.h
*/
struct BasicAuthStats {
ALL_BASIC_AUTH_STATS(GENERATE_COUNTER_STRUCT)
};

/**
* Struct definition for username password pairs.
*/
struct User {
// the user name
std::string name;
// the hashed password, see https://httpd.apache.org/docs/2.4/misc/password_encryptions.html
std::string hash;
};

using UserMapConstPtr =
std::unique_ptr<const absl::flat_hash_map<std::string, User>>; // username, User

/**
* Configuration for the Basic Auth filter.
*/
class FilterConfig {
public:
FilterConfig(UserMapConstPtr users, 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;

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

UserMapConstPtr users_;
BasicAuthStats stats_;
};
using FilterConfigConstSharedPtr = std::shared_ptr<const FilterConfig>;

// The Envoy filter to process HTTP basic auth.
class BasicAuthFilter : public Http::PassThroughDecoderFilter,
public Logger::Loggable<Logger::Id::basic_auth> {
public:
BasicAuthFilter(FilterConfigConstSharedPtr config);

// Http::StreamDecoderFilter
Http::FilterHeadersStatus decodeHeaders(Http::RequestHeaderMap& headers, bool) override;
void setDecoderFilterCallbacks(Http::StreamDecoderFilterCallbacks& callbacks) override;

private:
// The callback function.
Http::StreamDecoderFilterCallbacks* decoder_callbacks_;
FilterConfigConstSharedPtr config_;
};

} // namespace BasicAuth
} // namespace HttpFilters
} // namespace Extensions
} // namespace Envoy
Loading