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

auth: new api auth implementation #36968

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
9c77db7
enhanced api
wbpcode Nov 2, 2024
e543fb6
complete basic development of api key auth
wbpcode Nov 3, 2024
58e74c0
complete the development and tests
wbpcode Nov 6, 2024
962515f
add change log
wbpcode Nov 6, 2024
1126c96
Merge branch 'main' of https://github.com/envoyproxy/envoy into dev-a…
wbpcode Nov 6, 2024
98e2ac2
fix metadata
wbpcode Nov 6, 2024
4d4605f
fix docs
wbpcode Nov 7, 2024
68232cd
revert unnecessary change
wbpcode Nov 7, 2024
0e2550e
Update docs/root/configuration/http/http_filters/api_key_auth_filter.rst
wbpcode Nov 8, 2024
9eea7c8
address comments
wbpcode Nov 8, 2024
96dbe4e
Update docs/root/configuration/http/http_filters/api_key_auth_filter.rst
wbpcode Nov 9, 2024
3d909d0
Update source/extensions/filters/http/api_key_auth/api_key_auth.h
wbpcode Nov 9, 2024
d90a0ee
Update docs/root/configuration/http/http_filters/api_key_auth_filter.rst
wbpcode Nov 9, 2024
a9a03ee
address comments
wbpcode Nov 9, 2024
a0263a2
Merge branch 'dev-api-auth' of https://github.com/wbpcode/envoy into …
wbpcode Nov 9, 2024
6d72835
Merge branch 'main' of https://github.com/envoyproxy/envoy into dev-a…
wbpcode Nov 9, 2024
1818af7
more clear comment
wbpcode Nov 9, 2024
b93795c
fix format and test
wbpcode Nov 9, 2024
98ed3ed
fix format
wbpcode Nov 9, 2024
4d9973f
Update api/envoy/extensions/filters/http/api_key_auth/v3/api_key_auth…
wbpcode Nov 13, 2024
750bc3d
Update api/envoy/extensions/filters/http/api_key_auth/v3/api_key_auth…
wbpcode Nov 13, 2024
ab295f3
address comments
wbpcode Nov 13, 2024
b204906
fix docs format
wbpcode Nov 13, 2024
6eb8935
fix format
wbpcode Nov 13, 2024
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 @@ -28,50 +28,22 @@ option (xds.annotations.v3.file_status).work_in_progress = true;
// authentication_header: "X-API-KEY"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is the authentication_header config field defined?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry, this should be updated.

// credentials:
wbpcode marked this conversation as resolved.
Show resolved Hide resolved
// entries:
// - api_key: 09876abcdefg
// - key: 09876abcdefg
// client_id: user
// key_sources:
// entries:
// - header: "X-API-KEY"
//
// If multiple fields of ``authentication_header``, ``authentication_query``, and ``authentication_cookie``
// are set. Then filter will try to fetch the API key from the following fields in order:
//
// 1. ``authentication_header``
// 2. ``authentication_query``
// 3. ``authentication_cookie``
//
// If none of the fields are set, then the filter will reject the request.
message ApiKeyAuth {
message Credential {
// The value of the unique API key.
string api_key = 1 [(validate.rules).string = {min_len: 1}];

// The unique id or identity that used to identify the client or consumer.
string client_id = 2 [(validate.rules).string = {min_len: 1}];
}

message Credentials {
repeated Credential entries = 1;
}

// Api credentials used to authenticate the clients.
// The credentials that are used to authenticate the clients.
Credentials credentials = 1 [(udpa.annotations.sensitive) = true];

// The header name to fetch the key.
// If multiple values are present in the given header, the filter rejects the request.
string authentication_header = 2
[(validate.rules).string =
{max_len: 1024 well_known_regex: HTTP_HEADER_NAME strict: false ignore_empty: true}];

// The query parameter name to fetch the key.
string authentication_query = 3 [(validate.rules).string = {max_len: 1024}];

// The cookie name to fetch the key.
string authentication_cookie = 4
[(validate.rules).string =
{max_len: 1024 well_known_regex: HTTP_HEADER_NAME strict: false ignore_empty: true}];
// The key sources to fetch the key from the coming request.
KeySources key_sources = 2;
}

// API key auth configuration of per route or per virtual host or per route configuration.
message ApiKeyAuthPerScope {
message ApiKeyAuthPerRoute {
// Route specific APIKeyAuth configuration. This is optional and could be used to override the
// filter level ApiKeyAuth configuration **partly**.
//
Expand All @@ -89,3 +61,50 @@ message ApiKeyAuthPerScope {
// <config_http_filters_rbac>` instead.
repeated string allowed_clients = 2;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this field to be somewhat confusing, as there isn't a list of allowed clients in the non-route-override case.

Copy link
Member Author

@wbpcode wbpcode Nov 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two fields in the ApiKeyAuthPerRoute. One override_config field which is used to selectively override the filter level config. One allowed_clients field which is route-specific config (only in the ApiKeyAuthPerRoute) that only could be configured on the route/vhost. The allowed_clients field is used to limit the accessing to specific route/vhost.

No allowed_clients (or empty allowed_clients) means the users needn't authorization for this route/vhost.

As the comment said, it provide optional simple authorization (Authentication used to find who send the request, authorization used to detect if the client has the permission to access the route. We find lots of users or even developers cannot distinguish the authentication and authorization, they only want simple authentication and limit the route accessing by a simple way, rbac is too complex/heavy for this type users.)

This provides very limited but simple authorization. If more complex authorization is required, 
then use the :ref:`HTTP RBAC filter <config_http_filters_rbac>` instead.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What should be the behavior if both override_config and allowed_clients are defined?

Also, re:

If the list is empty, then all authenticated clients are allowed

This is true if only override_config is defined.

Copy link
Member Author

@wbpcode wbpcode Nov 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are independent and won't effect each others. The override_config is used to override the filter level configuration. And the allowed_clients is used to control accessing to the route.

The filter will try to authenticate the client by the override_config (if exists) and filter level configuration first. After the client is authenticated, we will try to check if it's limited by the allowed_clients (if exists and non-empty).

}

// Single credential entry that contains the API key and the related client id.
message Credential {
// The value of the unique API key.
string key = 1 [(validate.rules).string = {min_len: 1}];
wbpcode marked this conversation as resolved.
Show resolved Hide resolved

// The unique id or identity that used to identify the client or consumer.
string client_id = 2 [(validate.rules).string = {min_len: 1}];
}

// The credentials that are used to authenticate the clients.
message Credentials {
// The list of credentials. Explicitly setting the Credentials message with zero entries is
// allowed and will result in the filter rejecting all requests.
wbpcode marked this conversation as resolved.
Show resolved Hide resolved
repeated Credential entries = 1;
}

message KeySource {
// The header name to fetch the key. If multiple header values are present, the first one will be
// used.
wbpcode marked this conversation as resolved.
Show resolved Hide resolved
//
// If set, takes precedence over ``query`` and ``cookie``.
string header = 1
[(validate.rules).string =
{max_len: 1024 well_known_regex: HTTP_HEADER_NAME strict: false ignore_empty: true}];

// The query parameter name to fetch the key. If multiple query values are present, the first one
// will be used.
//
// Only makes sense if ``header`` is not set. If set, takes precedence over ``cookie``.
string query = 2 [(validate.rules).string = {max_len: 1024}];

// The cookie name to fetch the key.
//
// Only makes sense if the ``header`` and ``query`` are not set.
string cookie = 3
[(validate.rules).string =
{max_len: 1024 well_known_regex: HTTP_HEADER_NAME strict: false ignore_empty: true}];
}
wbpcode marked this conversation as resolved.
Show resolved Hide resolved

// The key sources to fetch the key from the coming request.
message KeySources {
// The list of key sources. Explicitly setting the KeySources message with zero entries is
// not allowed. The order of the key sources is important and the filter will try to fetch the
// key one by one from the key sources in the order they are defined.
repeated KeySource entries = 1 [(validate.rules).repeated = {min_items: 1}];
}
2 changes: 1 addition & 1 deletion bazel/repositories_extra.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ PYTHON_MINOR_VERSION = _python_minor_version(PYTHON_VERSION)
# Envoy deps that rely on a first stage of dependency loading in envoy_dependencies().
def envoy_dependencies_extra(
python_version = PYTHON_VERSION,
ignore_root_user_error = False):
ignore_root_user_error = True):
wbpcode marked this conversation as resolved.
Show resolved Hide resolved
bazel_features_deps()
emsdk_deps()
raze_fetch_remote_crates()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,12 @@ An example scope specific configuration of the filter may look like the followin
route: { cluster: some_service }
typed_per_filter_config:
api_key_auth:
"@type": type.googleapis.com/envoy.extensions.filters.http.api_key_auth.v3.ApiKeyAuthPerScope
"@type": type.googleapis.com/envoy.extensions.filters.http.api_key_auth.v3.ApiKeyAuthPerRoute
wbpcode marked this conversation as resolved.
Show resolved Hide resolved
override_config:
credentials:
entries:
- api_key: one_key
client_id: one_client
- api_key: another_key
client_id: another_client
authentication_query: api_key
Expand All @@ -77,6 +79,33 @@ An example scope specific configuration of the filter may look like the followin
In this example we customize credential list and key source for ``/admin`` route, and disable
authentication for ``/static`` prefixed routes.

Conbining the per-route configuration example and the filter configuration example, given the following
wbpcode marked this conversation as resolved.
Show resolved Hide resolved
requests, the filter will behave as follows:

.. code-block:: text

# The request will be allowed because the API key is valid and the client is allowed.
GET /admin?api_key=another_key HTTP/1.1
host: example.com

# The request will be denied with 403 status code because the API key is valid but the client is
# not allowed.
GET /admin?api_key=one_key HTTP/1.1
host: example.com

# The request will be denied with 401 status code because the API key is invalid.
GET /admin?api_key=invalid_key HTTP/1.1
host: example.com

# The request will be allowed because the filter is disabled for specific route.
GET /static HTTP/1.1
host: example.com

# The request will be allowed because the API key is valid and no client validation is configured.
GET / HTTP/1.1
host: example.com
Authorization: "Bearer one_key"

Statistics
----------

Expand Down
20 changes: 10 additions & 10 deletions source/common/common/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -625,13 +625,13 @@ envoy_cc_library(
],
)

envoy_cc_library(
name = "xds_manager_lib",
srcs = ["xds_manager_impl.cc"],
hdrs = ["xds_manager_impl.h"],
deps = [
"//envoy/config:xds_manager_interface",
"//envoy/upstream:cluster_manager_interface",
"//source/common/common:thread_lib",
],
)
# envoy_cc_library(
# name = "xds_manager_lib",
# srcs = ["xds_manager_impl.cc"],
# hdrs = ["xds_manager_impl.h"],
# deps = [
# "//envoy/config:xds_manager_interface",
# "//envoy/upstream:cluster_manager_interface",
# "//source/common/common:thread_lib",
# ],
# )
2 changes: 1 addition & 1 deletion source/extensions/extensions_metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ envoy.filters.http.api_key_auth:
status: alpha
type_urls:
- envoy.extensions.filters.http.api_key_auth.v3.ApiKeyAuth
- envoy.extensions.filters.http.api_key_auth.v3.ApiKeyAuthPerScope
- envoy.extensions.filters.http.api_key_auth.v3.ApiKeyAuthPerRoute
envoy.filters.http.buffer:
categories:
- envoy.filters.http
Expand Down
129 changes: 69 additions & 60 deletions source/extensions/filters/http/api_key_auth/api_key_auth.cc
Original file line number Diff line number Diff line change
Expand Up @@ -15,73 +15,85 @@ namespace HttpFilters {
namespace ApiKeyAuth {

ApiKeyAuthConfig::ApiKeyAuthConfig(const ApiKeyAuthProto& proto_config)
: key_source_(proto_config.authentication_header(), proto_config.authentication_query(),
proto_config.authentication_cookie()) {
: key_sources_(proto_config.key_sources()) {
if (!proto_config.has_credentials()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What should be the behavior if there are no credentials? Is this here just so there's a route override?
I'm trying to understand whether no credentials and/or key_sources is a valid configuration.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there are no credentials but the route has provide one, then the filter still could works. This provide the biggest flexibility to let users to combine the filter config and route config.

And even there is no any credentials finally at both filter and route, the filter will handle that case correctly by return a 401 response because we cannot authenticate anyone at that case.

return;
}

ApiKeyMap api_key_map;
api_key_map.reserve(proto_config.credentials().entries_size());
Credentials credentials;
credentials.reserve(proto_config.credentials().entries_size());

for (const auto& credential : proto_config.credentials().entries()) {
if (api_key_map.contains(credential.api_key())) {
if (credentials.contains(credential.key())) {
throwEnvoyExceptionOrPanic("Duplicate API key.");
wbpcode marked this conversation as resolved.
Show resolved Hide resolved
}
api_key_map[credential.api_key()] = credential.client_id();
credentials[credential.key()] = credential.client_id();
}

api_key_map_ = std::move(api_key_map);
credentials_ = std::move(credentials);
}

ScopeConfig::ScopeConfig(const ApiKeyAuthPerScopeProto& proto)
RouteConfig::RouteConfig(const ApiKeyAuthPerRouteProto& proto)
: override_config_(proto.override_config()) {
allowed_clients_.insert(proto.allowed_clients().begin(), proto.allowed_clients().end());
}

KeySource::KeySource(absl::string_view header, absl::string_view query, absl::string_view cookie)
: header_(header), query_(query), cookie_(cookie) {}
KeySources::Source::Source(absl::string_view header, absl::string_view query,
absl::string_view cookie) {
if (!header.empty()) {
source_ = Http::LowerCaseString(header);
} else if (!query.empty()) {
source_ = std::string(query);
query_source_ = true;
} else {
source_ = std::string(cookie);
}
}

KeyResult KeySource::getApiKey(const Http::RequestHeaderMap& headers,
std::string& key_buffer) const {
// Try to get the API key from the header first if the header key is not empty.
if (!header_.get().empty()) {
if (const auto auth_header = headers.get(header_); !auth_header.empty()) {
if (auth_header.size() > 1) {
return {{}, true};
}
KeySources::KeySources(const KeySourcesProto& proto_config) {
key_sources_.reserve(proto_config.entries_size());
for (const auto& source : proto_config.entries()) {
key_sources_.emplace_back(source.header(), source.query(), source.cookie());
}
}

absl::string_view auth_header_view = auth_header[0]->value().getStringView();
if (absl::StartsWith(auth_header_view, "Bearer ")) {
auth_header_view = auth_header_view.substr(7);
absl::string_view KeySources::Source::getKey(const Http::RequestHeaderMap& headers,
std::string& buffer) const {
if (absl::holds_alternative<Http::LowerCaseString>(source_)) {
if (const auto header = headers.get(absl::get<Http::LowerCaseString>(source_));
!header.empty()) {
absl::string_view header_view = header[0]->value().getStringView();
if (absl::StartsWith(header_view, "Bearer ")) {
header_view = header_view.substr(7);
}
return {auth_header_view};
std::cout << "header_view: " << header_view << std::endl;
wbpcode marked this conversation as resolved.
Show resolved Hide resolved
return header_view;
}
}

// If the API key is not found in the header, try to get it from the query parameter
// if the query key is not empty.
if (!query_.empty()) {
auto query_params =
} else if (query_source_) {
auto params =
Http::Utility::QueryParamsMulti::parseAndDecodeQueryString(headers.getPathValue());
if (auto iter = query_params.data().find(query_); iter != query_params.data().end()) {
if (iter->second.size() > 1) {
return {{}, true};
}
if (auto iter = params.data().find(absl::get<std::string>(source_));
iter != params.data().end()) {
if (!iter->second.empty()) {
key_buffer = std::move(iter->second[0]);
return {absl::string_view{key_buffer}};
buffer = std::move(iter->second[0]);
return buffer;
}
}
} else {
buffer = Http::Utility::parseCookieValue(headers, absl::get<std::string>(source_));
return buffer;
}

// If the API key is not found in the header and query parameter, try to get it from the
// cookie if the cookie key is not empty.
if (!cookie_.empty()) {
key_buffer = Http::Utility::parseCookieValue(headers, cookie_);
return {absl::string_view{key_buffer}};
}
return {};
}

absl::string_view KeySources::getKey(const Http::RequestHeaderMap& headers,
std::string& buffer) const {
for (const auto& source : key_sources_) {
if (auto key = source.getKey(headers, buffer); !key.empty()) {
return key;
}
}
return {};
}

Expand All @@ -92,46 +104,43 @@ FilterConfig::FilterConfig(const ApiKeyAuthProto& proto_config, Stats::Scope& sc
ApiKeyAuthFilter::ApiKeyAuthFilter(FilterConfigSharedPtr config) : config_(std::move(config)) {}

Http::FilterHeadersStatus ApiKeyAuthFilter::decodeHeaders(Http::RequestHeaderMap& headers, bool) {
const ScopeConfig* override_config =
Http::Utility::resolveMostSpecificPerFilterConfig<ScopeConfig>(decoder_callbacks_);
const RouteConfig* override_config =
Http::Utility::resolveMostSpecificPerFilterConfig<RouteConfig>(decoder_callbacks_);

OptRef<const ApiKeyMap> api_key_map = config_->apiKeyMap();
OptRef<const KeySource> key_source = config_->keySource();
OptRef<const Credentials> credentials = config_->credentials();
OptRef<const KeySources> key_sources = config_->keySources();

// If there is an override config, then try to override the API key map and key source.
if (override_config != nullptr) {
OptRef<const ApiKeyMap> override_api_key_map = override_config->apiKeyMap();
if (override_api_key_map.has_value()) {
api_key_map = override_api_key_map;
OptRef<const Credentials> override_credentials = override_config->credentials();
if (override_credentials.has_value()) {
credentials = override_credentials;
}

OptRef<const KeySource> override_key_source = override_config->keySource();
if (override_key_source.has_value()) {
key_source = override_key_source;
OptRef<const KeySources> override_key_sources = override_config->keySources();
if (override_key_sources.has_value()) {
wbpcode marked this conversation as resolved.
Show resolved Hide resolved
key_sources = override_key_sources;
}
}

if (!key_source.has_value()) {
if (!key_sources.has_value()) {
return onDenied(Http::Code::Unauthorized, "Client authentication failed.",
"missing_key_source");
"missing_key_sources");
}
if (!api_key_map.has_value()) {
if (!credentials.has_value()) {
return onDenied(Http::Code::Unauthorized, "Client authentication failed.",
"missing_credentials");
}

std::string key_buffer;
const KeyResult key_result = key_source->getApiKey(headers, key_buffer);
absl::string_view key_result = key_sources->getKey(headers, key_buffer);

if (key_result.multiple_keys_error) {
return onDenied(Http::Code::Unauthorized, "Client authentication failed.", "multiple_api_key");
}
if (key_result.key_string_view.empty()) {
if (key_result.empty()) {
return onDenied(Http::Code::Unauthorized, "Client authentication failed.", "missing_api_key");
}

const auto credential = api_key_map->find(key_result.key_string_view);
if (credential == api_key_map->end()) {
const auto credential = credentials->find(key_result);
if (credential == credentials->end()) {
return onDenied(Http::Code::Unauthorized, "Client authentication failed.", "unkonwn_api_key");
}

Expand Down
Loading
Loading