Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d275e4d
per-route oauth2 configuration
zhaohuabing Apr 2, 2026
3bfa54c
remove the old constructor
zhaohuabing Apr 3, 2026
38df08f
format
zhaohuabing Apr 3, 2026
c6f7bed
add change log
zhaohuabing Apr 3, 2026
cc62326
Merge branch 'main' into oauth2-per-route-config
zhaohuabing Apr 3, 2026
0647aaa
add missing registry metadata
zhaohuabing Apr 3, 2026
c75b379
update oauth2 docs
zhaohuabing Apr 3, 2026
547e491
update oauth2 docs
zhaohuabing Apr 3, 2026
4ac7d32
update oauth2 docs
zhaohuabing Apr 3, 2026
44dbd9b
add coverage
zhaohuabing Apr 3, 2026
6674ed7
fix test
zhaohuabing Apr 3, 2026
4d6fd12
fix test
zhaohuabing Apr 5, 2026
3b9a58c
Merge remote-tracking branch 'origin/main' into oauth2-per-route-config
zhaohuabing Apr 7, 2026
f22ab98
fix format
zhaohuabing Apr 7, 2026
b364732
fix test
zhaohuabing Apr 7, 2026
6545c3b
simplify code
zhaohuabing Apr 7, 2026
ee16c0e
remove init manager for per route config
zhaohuabing Apr 7, 2026
7fa3277
address comments
zhaohuabing Apr 7, 2026
185886f
update
zhaohuabing Apr 7, 2026
c5d153d
address comment
zhaohuabing Apr 7, 2026
56994e5
add comment
zhaohuabing Apr 8, 2026
b3f8699
Update source/extensions/filters/http/oauth2/filter.cc
zhaohuabing Apr 8, 2026
d6eb7e2
Update source/extensions/filters/http/oauth2/filter.cc
zhaohuabing Apr 8, 2026
1637ee5
remove the FilterConfigPerRoute class
zhaohuabing Apr 8, 2026
b577f23
use raw pointer
zhaohuabing Apr 8, 2026
4922e49
update setActiveConfig
zhaohuabing Apr 8, 2026
9f15e00
simplify code
zhaohuabing Apr 8, 2026
9840fdd
simplify code
zhaohuabing Apr 8, 2026
92823f0
fix test
zhaohuabing Apr 8, 2026
70ca9fd
format
zhaohuabing Apr 8, 2026
a4a71e9
Merge remote-tracking branch 'origin/main' into oauth2-per-route-config
zhaohuabing Apr 8, 2026
85a4d9f
refactor
zhaohuabing Apr 8, 2026
8614972
Merge remote-tracking branch 'origin/main' into oauth2-per-route-config
zhaohuabing Apr 9, 2026
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
12 changes: 11 additions & 1 deletion api/envoy/extensions/filters/http/oauth2/v3/oauth.proto
Original file line number Diff line number Diff line change
Expand Up @@ -306,8 +306,18 @@ message OAuth2Config {
repeated config.route.v3.HeaderMatcher allow_failed_matcher = 27;
}

// Per-route OAuth2 config.
//
// This message supplies an OAuth2Config for the matched route.
// It overrides the filter-level config for requests matching the route.
// If neither the global config nor a per-route config is specified, OAuth2 is disabled for the route.
message OAuth2PerRoute {
Comment thread
zhaohuabing marked this conversation as resolved.
// Full OAuth2 config for this route.
OAuth2Config config = 1 [(validate.rules).message = {required: true}];
Comment thread
zhaohuabing marked this conversation as resolved.
}

// Filter config.
message OAuth2 {
// Leave this empty to disable OAuth2 for a specific route, using per filter config.
// The OAuth2 filter config.
OAuth2Config config = 1;
}
4 changes: 4 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ minor_behavior_changes:
Now all the dynamic module extension factories (HTTP, network, listener, UDP listener, and so on) will
serialize the ``google.protobuf.Struct`` configuration message to JSON string and pass it to the
dynamic module side as the configuration.
- area: oauth2
change: |
Added :ref:`per-route configuration support <envoy_v3_api_msg_extensions.filters.http.oauth2.v3.OAuth2PerRoute>`
to the OAuth2 HTTP filter.
- area: proto_api_scrubber
change: |
If :ref:`scrub_unknown_fields
Expand Down
148 changes: 148 additions & 0 deletions docs/root/configuration/http/http_filters/oauth2_filter.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ OAuth2
* This filter should be configured with the type URL ``type.googleapis.com/envoy.extensions.filters.http.oauth2.v3.OAuth2``.
* :ref:`v3 API reference <envoy_v3_api_msg_extensions.filters.http.oauth2.v3.OAuth2>`

The OAuth2 filter also defines a route-level config message,
:ref:`OAuth2PerRoute <envoy_v3_api_msg_extensions.filters.http.oauth2.v3.OAuth2PerRoute>`,
which may be attached to
:ref:`Route.typed_per_filter_config <envoy_v3_api_field_config.route.v3.Route.typed_per_filter_config>`.
The route-level config carries a complete :ref:`OAuth2Config <envoy_v3_api_msg_extensions.filters.http.oauth2.v3.OAuth2Config>`
and is intended to replace, not merge with, the filter-level config.

When using per-route configuration, keep the
:ref:`redirect_path_matcher <envoy_v3_api_field_extensions.filters.http.oauth2.v3.OAuth2Config.redirect_path_matcher>`
and :ref:`signout_path <envoy_v3_api_field_extensions.filters.http.oauth2.v3.OAuth2Config.signout_path>`
within the same route prefix as the protected resources. If cookie paths are customized, they
should cover that same prefix so that the callback and signout requests receive the OAuth cookies
needed to complete the flow. When multiple per-route OAuth2 configurations share the same host,
customizing :ref:`cookie_names <envoy_v3_api_field_extensions.filters.http.oauth2.v3.OAuth2Credentials.cookie_names>`
is recommended to avoid overlap between routes.

The OAuth filter's flow involves:

* An unauthenticated user arrives at myapp.com, and the oauth filter redirects them to the
Expand Down Expand Up @@ -229,6 +245,138 @@ can be defined in one shared file.
generic_secret:
secret: <Your hmac secret here>

The following example shows two independent route prefixes, ``/foo`` and ``/bar``, each with its
own OAuth2 client settings. The callback path and signout path stay under the same prefix as the
protected route, the cookie paths are scoped to that prefix, and the cookie names are customized so
that the two configurations remain independent on the same host:

.. code-block:: yaml

http_filters:
- name: envoy.filters.http.oauth2
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.oauth2.v3.OAuth2
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
route_config:
virtual_hosts:
- name: service
domains: ["*"]
routes:
- match:
prefix: "/foo"
route:
cluster: local_service
typed_per_filter_config:
envoy.filters.http.oauth2:
"@type": type.googleapis.com/envoy.extensions.filters.http.oauth2.v3.OAuth2PerRoute
config:
token_endpoint:
cluster: google_oauth2
uri: https://oauth2.googleapis.com/token
timeout: 3s
authorization_endpoint: https://accounts.google.com/o/oauth2/v2/auth
redirect_uri: "%REQ(x-forwarded-proto)%://%REQ(:authority)%/foo/callback"
redirect_path_matcher:
path:
exact: /foo/callback
signout_path:
path:
exact: /foo/signout
cookie_configs:
bearer_token_cookie_config:
path: "/foo"
oauth_hmac_cookie_config:
path: "/foo"
oauth_expires_cookie_config:
path: "/foo"
id_token_cookie_config:
path: "/foo"
refresh_token_cookie_config:
path: "/foo"
oauth_nonce_cookie_config:
path: "/foo"
code_verifier_cookie_config:
path: "/foo"
credentials:
client_id: foo
cookie_names:
bearer_token: FooBearerToken
oauth_hmac: FooOauthHMAC
oauth_expires: FooOauthExpires
id_token: FooIdToken
refresh_token: FooRefreshToken
oauth_nonce: FooOauthNonce
code_verifier: FooCodeVerifier
token_secret:
name: foo_client_secret
sds_config:
path: "/etc/foo-client-secret.yaml"
hmac_secret:
name: hmac
sds_config:
path: "/etc/hmac-secret.yaml"
auth_scopes:
- openid
- email
- match:
prefix: "/bar"
route:
cluster: local_service
typed_per_filter_config:
envoy.filters.http.oauth2:
"@type": type.googleapis.com/envoy.extensions.filters.http.oauth2.v3.OAuth2PerRoute
config:
token_endpoint:
cluster: google_oauth2
uri: https://oauth2.googleapis.com/token
timeout: 3s
authorization_endpoint: https://accounts.google.com/o/oauth2/v2/auth
redirect_uri: "%REQ(x-forwarded-proto)%://%REQ(:authority)%/bar/callback"
redirect_path_matcher:
path:
exact: /bar/callback
signout_path:
path:
exact: /bar/signout
cookie_configs:
bearer_token_cookie_config:
path: "/bar"
oauth_hmac_cookie_config:
path: "/bar"
oauth_expires_cookie_config:
path: "/bar"
id_token_cookie_config:
path: "/bar"
refresh_token_cookie_config:
path: "/bar"
oauth_nonce_cookie_config:
path: "/bar"
code_verifier_cookie_config:
path: "/bar"
credentials:
client_id: bar
cookie_names:
bearer_token: BarBearerToken
oauth_hmac: BarOauthHMAC
oauth_expires: BarOauthExpires
id_token: BarIdToken
refresh_token: BarRefreshToken
oauth_nonce: BarOauthNonce
code_verifier: BarCodeVerifier
token_secret:
name: bar_client_secret
sds_config:
path: "/etc/bar-client-secret.yaml"
hmac_secret:
name: hmac
sds_config:
path: "/etc/hmac-secret.yaml"
auth_scopes:
- openid
- email


Notes
-----
Expand Down
4 changes: 3 additions & 1 deletion envoy/secret/secret_manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -157,13 +157,15 @@ class SecretManager {
* @param config_name a name that uniquely refers to the SDS config source.
* @param secret_provider_context context that provides components for creating and initializing
* secret provider.
* @param init_manager if supplied, register to the initialization sequence; otherwise, start
* immediately
* @return GenericSecretConfigProviderSharedPtr the dynamic generic secret provider.
*/
virtual GenericSecretConfigProviderSharedPtr
findOrCreateGenericSecretProvider(const envoy::config::core::v3::ConfigSource& config_source,
const std::string& config_name,
Server::Configuration::ServerFactoryContext& server_context,
Init::Manager& init_manager) PURE;
OptRef<Init::Manager> init_manager) PURE;
};

using SecretManagerPtr = std::unique_ptr<SecretManager>;
Expand Down
3 changes: 2 additions & 1 deletion source/common/secret/secret_manager_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,8 @@ SecretManagerImpl::findOrCreateTlsSessionTicketKeysContextProvider(

GenericSecretConfigProviderSharedPtr SecretManagerImpl::findOrCreateGenericSecretProvider(
const envoy::config::core::v3::ConfigSource& sds_config_source, const std::string& config_name,
Server::Configuration::ServerFactoryContext& server_context, Init::Manager& init_manager) {
Server::Configuration::ServerFactoryContext& server_context,
OptRef<Init::Manager> init_manager) {
Comment thread
zhaohuabing marked this conversation as resolved.
return generic_secret_providers_.findOrCreate(sds_config_source, config_name, server_context,
init_manager, true);
}
Expand Down
2 changes: 1 addition & 1 deletion source/common/secret/secret_manager_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class SecretManagerImpl : public SecretManager {
findOrCreateGenericSecretProvider(const envoy::config::core::v3::ConfigSource& config_source,
const std::string& config_name,
Server::Configuration::ServerFactoryContext& server_context,
Init::Manager& init_manager) override;
OptRef<Init::Manager> init_manager) override;

private:
ProtobufTypes::MessagePtr dumpSecretConfigs(const Matchers::StringMatcher& name_matcher);
Expand Down
1 change: 1 addition & 0 deletions source/extensions/extensions_metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,7 @@ envoy.filters.http.oauth2:
status: stable
type_urls:
- envoy.extensions.filters.http.oauth2.v3.OAuth2
- envoy.extensions.filters.http.oauth2.v3.OAuth2PerRoute
envoy.filters.http.on_demand:
categories:
- envoy.filters.http
Expand Down
79 changes: 54 additions & 25 deletions source/extensions/filters/http/oauth2/config.cc
Original file line number Diff line number Diff line change
Expand Up @@ -27,33 +27,23 @@ namespace {
Secret::GenericSecretConfigProviderSharedPtr
secretsProvider(const envoy::extensions::transport_sockets::tls::v3::SdsSecretConfig& config,
Server::Configuration::ServerFactoryContext& server_context,
Init::Manager& init_manager) {
OptRef<Init::Manager> init_manager) {
if (config.has_sds_config()) {
return server_context.secretManager().findOrCreateGenericSecretProvider(
config.sds_config(), config.name(), server_context, init_manager);
} else {
return server_context.secretManager().findStaticGenericSecretProvider(config.name());
}
}
} // namespace

absl::StatusOr<Http::FilterFactoryCb> OAuth2Config::createFilterFactoryFromProtoTyped(
const envoy::extensions::filters::http::oauth2::v3::OAuth2& proto,
const std::string& stats_prefix, Server::Configuration::FactoryContext& context) {
if (!proto.has_config()) {
return absl::InvalidArgumentError("config must be present for global config");
}

const auto& proto_config = proto.config();
absl::StatusOr<FilterConfigSharedPtr>
createFilterConfig(const envoy::extensions::filters::http::oauth2::v3::OAuth2Config& proto_config,
Server::Configuration::ServerFactoryContext& server_context,
OptRef<Init::Manager> init_manager, Stats::Scope& scope,
const std::string& stats_prefix) {
const auto& credentials = proto_config.credentials();

const auto& client_secret = credentials.token_secret();
const auto& hmac_secret = credentials.hmac_secret();
const auto auth_type = proto_config.auth_type();

auto& server_context = context.serverFactoryContext();
auto& cluster_manager = context.serverFactoryContext().clusterManager();

// token_secret is required unless auth_type is TLS_CLIENT_AUTH
if (auth_type !=
envoy::extensions::filters::http::oauth2::v3::OAuth2Config_AuthType_TLS_CLIENT_AUTH) {
Expand All @@ -68,18 +58,20 @@ absl::StatusOr<Http::FilterFactoryCb> OAuth2Config::createFilterFactoryFromProto
ENVOY_LOG_MISC(debug,
"OAuth2 filter: token_secret is ignored when auth_type is TLS_CLIENT_AUTH");
}

Secret::GenericSecretConfigProviderSharedPtr secret_provider_client_secret = nullptr;
if (credentials.has_token_secret() &&
auth_type !=
envoy::extensions::filters::http::oauth2::v3::OAuth2Config_AuthType_TLS_CLIENT_AUTH) {
secret_provider_client_secret =
secretsProvider(client_secret, server_context, context.initManager());
secretsProvider(credentials.token_secret(), server_context, init_manager);
Comment thread
zhaohuabing marked this conversation as resolved.
if (secret_provider_client_secret == nullptr) {
return absl::InvalidArgumentError("invalid token secret configuration");
}
}

auto secret_provider_hmac_secret =
secretsProvider(hmac_secret, server_context, context.initManager());
secretsProvider(credentials.hmac_secret(), server_context, init_manager);
if (secret_provider_hmac_secret == nullptr) {
return absl::InvalidArgumentError("invalid HMAC secret configuration");
}
Expand All @@ -93,21 +85,58 @@ absl::StatusOr<Http::FilterFactoryCb> OAuth2Config::createFilterFactoryFromProto

auto secret_reader = std::make_shared<SDSSecretReader>(
std::move(secret_provider_client_secret), std::move(secret_provider_hmac_secret),
context.serverFactoryContext().threadLocal(), context.serverFactoryContext().api());
auto config = std::make_shared<FilterConfig>(proto_config, context.serverFactoryContext(),
secret_reader, context.scope(), stats_prefix);
server_context.threadLocal(), server_context.api());
return std::make_shared<FilterConfig>(proto_config, server_context, secret_reader, scope,
stats_prefix);
}
} // namespace

absl::StatusOr<Http::FilterFactoryCb> OAuth2Config::createFilterFactoryFromProtoTyped(
const envoy::extensions::filters::http::oauth2::v3::OAuth2& proto,
const std::string& stats_prefix, Server::Configuration::FactoryContext& context) {
auto& server_context = context.serverFactoryContext();
auto& cluster_manager = context.serverFactoryContext().clusterManager();
FilterConfigSharedPtr config = nullptr;
if (proto.has_config()) {
auto config_or_error = createFilterConfig(proto.config(), server_context, context.initManager(),
context.scope(), stats_prefix);
if (!config_or_error.ok()) {
return config_or_error.status();
}
config = config_or_error.value();
}

return
[&context, config, &cluster_manager](Http::FilterChainFactoryCallbacks& callbacks) -> void {
std::unique_ptr<OAuth2Client> oauth_client =
std::make_unique<OAuth2ClientImpl>(cluster_manager, config->oauthTokenEndpoint(),
config->retryPolicy(), config->defaultExpiresIn());
callbacks.addStreamFilter(std::make_shared<OAuth2Filter>(
config, std::move(oauth_client), context.serverFactoryContext().timeSource(),
config,
[&cluster_manager](const FilterConfig& active_config) -> std::shared_ptr<OAuth2Client> {
return std::make_shared<OAuth2ClientImpl>(
cluster_manager, active_config.oauthTokenEndpoint(), active_config.retryPolicy(),
active_config.defaultExpiresIn());
},
[](TimeSource& time_source,
const FilterConfig& active_config) -> std::shared_ptr<CookieValidator> {
return std::make_shared<OAuth2CookieValidator>(
time_source, active_config.cookieNames(), active_config.cookieDomain());
},
context.serverFactoryContext().timeSource(),
context.serverFactoryContext().api().randomGenerator()));
};
}

absl::StatusOr<Router::RouteSpecificFilterConfigConstSharedPtr>
OAuth2Config::createRouteSpecificFilterConfigTyped(
const envoy::extensions::filters::http::oauth2::v3::OAuth2PerRoute& proto,
Server::Configuration::ServerFactoryContext& context, ProtobufMessage::ValidationVisitor&) {
auto config_or_error =
createFilterConfig(proto.config(), context, absl::nullopt, context.scope(), "");
if (!config_or_error.ok()) {
return config_or_error.status();
}
return config_or_error.value();
}

/*
* Static registration for the OAuth2 filter. @see RegisterFactory.
*/
Expand Down
8 changes: 7 additions & 1 deletion source/extensions/filters/http/oauth2/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,20 @@ namespace HttpFilters {
namespace Oauth2 {

class OAuth2Config : public Extensions::HttpFilters::Common::ExceptionFreeFactoryBase<
envoy::extensions::filters::http::oauth2::v3::OAuth2> {
envoy::extensions::filters::http::oauth2::v3::OAuth2,
envoy::extensions::filters::http::oauth2::v3::OAuth2PerRoute> {
public:
OAuth2Config() : ExceptionFreeFactoryBase("envoy.filters.http.oauth2") {}

absl::StatusOr<Http::FilterFactoryCb>
createFilterFactoryFromProtoTyped(const envoy::extensions::filters::http::oauth2::v3::OAuth2&,
const std::string&,
Server::Configuration::FactoryContext&) override;

absl::StatusOr<Router::RouteSpecificFilterConfigConstSharedPtr>
createRouteSpecificFilterConfigTyped(
const envoy::extensions::filters::http::oauth2::v3::OAuth2PerRoute&,
Server::Configuration::ServerFactoryContext&, ProtobufMessage::ValidationVisitor&) override;
};

} // namespace Oauth2
Expand Down
Loading
Loading