Skip to content

Commit

Permalink
oauth2 filter: Make OAuth scopes configurable. (envoyproxy#14168)
Browse files Browse the repository at this point in the history
New optional parameter 'auth_scopes' added to the filter. The default value is 'user' (if not provided) to avoid breaking changes to users updating to the latest version.

Signed-off-by: andreyprezotto <andreypp@gmail.com>

Co-authored-by: Nitin Goyal <nitingoyal.dev@gmail.com>
Signed-off-by: Auni Ahsan <auni@google.com>
  • Loading branch information
2 people authored and auni53 committed Jan 25, 2021
1 parent 481e3e6 commit dee43e8
Show file tree
Hide file tree
Showing 12 changed files with 184 additions and 33 deletions.
7 changes: 6 additions & 1 deletion api/envoy/extensions/filters/http/oauth2/v3alpha/oauth.proto
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ message OAuth2Credentials {

// OAuth config
//
// [#next-free-field: 9]
// [#next-free-field: 10]
message OAuth2Config {
// Endpoint on the authorization server to retrieve the access token from.
config.core.v3.HttpUri token_endpoint = 1;
Expand Down Expand Up @@ -74,6 +74,11 @@ message OAuth2Config {

// Any request that matches any of the provided matchers will be passed through without OAuth validation.
repeated config.route.v3.HeaderMatcher pass_through_matcher = 8;

// Optional list of OAuth scopes to be claimed in the authorization request. If not specified,
// defaults to "user" scope.
// OAuth RFC https://tools.ietf.org/html/rfc6749#section-3.3
repeated string auth_scopes = 9;
}

// Filter config.
Expand Down
7 changes: 6 additions & 1 deletion api/envoy/extensions/filters/http/oauth2/v4alpha/oauth.proto

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions docs/root/configuration/http/http_filters/oauth2_filter.rst
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ The following is an example configuring the filter.
name: hmac
sds_config:
path: "/etc/envoy/hmac.yaml"
# (Optional): defaults to 'user' scope if not provided
auth_scopes:
- user
- openid
- email

Below is a complete code example of how we employ the filter as one of
:ref:`HttpConnectionManager HTTP filters
Expand Down Expand Up @@ -114,6 +119,11 @@ Below is a complete code example of how we employ the filter as one of
name: hmac
sds_config:
path: "/etc/envoy/hmac.yaml"
# (Optional): defaults to 'user' scope if not provided
auth_scopes:
- user
- openid
- email
- name: envoy.router
tracing: {}
codec_type: "AUTO"
Expand Down
1 change: 1 addition & 0 deletions docs/root/version_history/current.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Minor Behavior Changes
----------------------
*Changes that may cause incompatibilities for some users, but should not for most*

* oauth filter: added the optional parameter :ref:`auth_scopes <envoy_v3_api_field_extensions.filters.http.oauth2.v3alpha.OAuth2Config.auth_scopes>` with default value of 'user' if not provided. Enables this value to be overridden in the Authorization request to the OAuth provider.
* tcp: setting NODELAY in the base connection class. This should have no effect for TCP or HTTP proxying, but may improve throughput in other areas. This behavior can be temporarily reverted by setting `envoy.reloadable_features.always_nodelay` to false.
* upstream: host weight changes now cause a full load balancer rebuild as opposed to happening
atomically inline. This change has been made to support load balancer pre-computation of data
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 26 additions & 4 deletions source/extensions/filters/http/oauth2/filter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ constexpr const char* CookieTailHttpOnlyFormatString =
";version=1;path=/;Max-Age={};secure;HttpOnly";

const char* AuthorizationEndpointFormat =
"{}?client_id={}&scope=user&response_type=code&redirect_uri={}&state={}";
"{}?client_id={}&scope={}&response_type=code&redirect_uri={}&state={}";

constexpr absl::string_view UnauthorizedBodyMessage = "OAuth flow failed.";

Expand All @@ -60,6 +60,7 @@ constexpr absl::string_view REDIRECT_RACE = "oauth.race_redirect";
constexpr absl::string_view REDIRECT_LOGGED_IN = "oauth.logged_in";
constexpr absl::string_view REDIRECT_FOR_CREDENTIALS = "oauth.missing_credentials";
constexpr absl::string_view SIGN_OUT = "oauth.sign_out";
constexpr absl::string_view DEFAULT_AUTH_SCOPE = "user";

template <class T>
std::vector<Http::HeaderUtility::HeaderData> headerMatchers(const T& matcher_protos) {
Expand All @@ -73,6 +74,25 @@ std::vector<Http::HeaderUtility::HeaderData> headerMatchers(const T& matcher_pro
return matchers;
}

// Transforms the proto list of 'auth_scopes' into a vector of std::string, also
// handling the default value logic.
std::vector<std::string>
authScopesList(const Protobuf::RepeatedPtrField<std::string>& auth_scopes_protos) {
std::vector<std::string> scopes;

// If 'auth_scopes' is empty it must return a list with the default value.
if (auth_scopes_protos.empty()) {
scopes.emplace_back(DEFAULT_AUTH_SCOPE);
} else {
scopes.reserve(auth_scopes_protos.size());

for (const auto& scope : auth_scopes_protos) {
scopes.emplace_back(scope);
}
}
return scopes;
}

// Sets the auth token as the Bearer token in the authorization header.
void setBearerToken(Http::RequestHeaderMap& headers, const std::string& token) {
headers.setInline(authorization_handle.handle(), absl::StrCat("Bearer ", token));
Expand All @@ -90,6 +110,8 @@ FilterConfig::FilterConfig(
redirect_matcher_(proto_config.redirect_path_matcher()),
signout_path_(proto_config.signout_path()), secret_reader_(secret_reader),
stats_(FilterConfig::generateStats(stats_prefix, scope)),
encoded_auth_scopes_(Http::Utility::PercentEncoding::encode(
absl::StrJoin(authScopesList(proto_config.auth_scopes()), " "), ":/=&? ")),
forward_bearer_token_(proto_config.forward_bearer_token()),
pass_through_header_matchers_(headerMatchers(proto_config.pass_through_matcher())) {
if (!cluster_manager.clusters().hasCluster(oauth_token_endpoint_.cluster())) {
Expand Down Expand Up @@ -275,9 +297,9 @@ Http::FilterHeadersStatus OAuth2Filter::decodeHeaders(Http::RequestHeaderMap& he
const std::string escaped_redirect_uri =
Http::Utility::PercentEncoding::encode(redirect_uri, ":/=&?");

const std::string new_url =
fmt::format(AuthorizationEndpointFormat, config_->authorizationEndpoint(),
config_->clientId(), escaped_redirect_uri, escaped_state);
const std::string new_url = fmt::format(
AuthorizationEndpointFormat, config_->authorizationEndpoint(), config_->clientId(),
config_->encodedAuthScopes(), escaped_redirect_uri, escaped_state);
response_headers->setLocation(new_url);
decoder_callbacks_->encodeHeaders(std::move(response_headers), true, REDIRECT_FOR_CREDENTIALS);

Expand Down
2 changes: 2 additions & 0 deletions source/extensions/filters/http/oauth2/filter.h
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ class FilterConfig {
std::string clientSecret() const { return secret_reader_->clientSecret(); }
std::string tokenSecret() const { return secret_reader_->tokenSecret(); }
FilterStats& stats() { return stats_; }
const std::string& encodedAuthScopes() const { return encoded_auth_scopes_; }

private:
static FilterStats generateStats(const std::string& prefix, Stats::Scope& scope);
Expand All @@ -135,6 +136,7 @@ class FilterConfig {
const Matchers::PathMatcher signout_path_;
std::shared_ptr<SecretReader> secret_reader_;
FilterStats stats_;
const std::string encoded_auth_scopes_;
const bool forward_bearer_token_ : 1;
const std::vector<Http::HeaderUtility::HeaderData> pass_through_header_matchers_;
};
Expand Down
1 change: 1 addition & 0 deletions source/extensions/filters/http/oauth2/oauth_client.cc
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ void OAuth2ClientImpl::onSuccess(const Http::AsyncClient::Request&,
const auto response_code = message->headers().Status()->value().getStringView();
if (response_code != "200") {
ENVOY_LOG(debug, "Oauth response code: {}", response_code);
ENVOY_LOG(debug, "Oauth response body: {}", message->bodyAsString());
parent_->sendUnauthorizedResponse();
return;
}
Expand Down
8 changes: 8 additions & 0 deletions test/extensions/filters/http/oauth2/config_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ void expectInvalidSecretConfig(const std::string& failed_secret_name,
signout_path:
path:
exact: /signout
auth_scopes:
- user
- openid
- email
)EOF";

OAuth2Config factory;
Expand Down Expand Up @@ -87,6 +91,10 @@ TEST(ConfigTest, CreateFilter) {
signout_path:
path:
exact: /signout
auth_scopes:
- user
- openid
- email
)EOF";

OAuth2Config factory;
Expand Down
Loading

0 comments on commit dee43e8

Please sign in to comment.