Skip to content

Commit d76115f

Browse files
Yueren-WangYueren Wang
and
Yueren Wang
authored
OAuth2: Add samesite attribute support for all OAuth2 supported cookie types (envoyproxy#37952)
Commit Message: OAuth2: Add samesite attribute support for all OAuth2 supported cookie types Additional Description: The SameSite attribute offers three values to control whether cookies are shared within the same site or across different sites. It's an optional setting, with a "Disabled" option that omits the SameSite attribute altogether. By default, this setting is disabled to ensure no changes are made to existing deployments, but operators now have the option to enable SameSite. The six cookies supporting SameSite attribute are: bearer_token_cookie hmac_cookie expires_cookie id_token_cookie refresh_token_cookie nonce_cookie The samesite attribute value allowed are: Strict Lax None Disabled (Default, if no value is set in config) The operator can also optionally do not specify any SameSite attributes for cookie. This will result DISABLED value to be set for all cookie's SameSite attribute value. in this case no same site attribute will be returned by filter. The operator can also choose different same site attribute to be configured by different cookies. This means the SameSite attributes for different cookies listed above can be different. Also the operator can optionally specify SameSite attribute for some cookie but miss it for others. it is not mandatory to specify SameSite explicitly for all cookies Risk Level: Medium Testing: unit Docs Changes: proto is documented Release Notes: changelog entry added --------- Signed-off-by: Yueren Wang <yuerenwang@lyft.com> Signed-off-by: Yueren Wang <yuerenwang@tm4wwrxcwd.tailbaa43.ts.net> Co-authored-by: Yueren Wang <yuerenwang@tm4wwrxcwd.tailbaa43.ts.net>
1 parent 0274a68 commit d76115f

File tree

7 files changed

+624
-44
lines changed

7 files changed

+624
-44
lines changed

api/envoy/extensions/filters/http/oauth2/v3/oauth.proto

+39-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,41 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE;
2525
// [#extension: envoy.filters.http.oauth2]
2626
//
2727

28+
// OAuth cookie configuration attributes.
29+
//
30+
message CookieConfig {
31+
enum SameSite {
32+
DISABLED = 0;
33+
STRICT = 1;
34+
LAX = 2;
35+
NONE = 3;
36+
}
37+
38+
// The value used for the SameSite cookie attribute.
39+
SameSite same_site = 1 [(validate.rules).enum = {defined_only: true}];
40+
}
41+
42+
// [#next-free-field: 7]
43+
message CookieConfigs {
44+
// Configuration for the bearer token cookie.
45+
CookieConfig bearer_token_cookie_config = 1;
46+
47+
// Configuration for the OAuth HMAC cookie.
48+
CookieConfig oauth_hmac_cookie_config = 2;
49+
50+
// Configuration for the OAuth expires cookie.
51+
CookieConfig oauth_expires_cookie_config = 3;
52+
53+
// Configuration for the ID token cookie.
54+
CookieConfig id_token_cookie_config = 4;
55+
56+
// Configuration for the refresh token cookie.
57+
CookieConfig refresh_token_cookie_config = 5;
58+
59+
// Configuration for the OAuth nonce cookie.
60+
CookieConfig oauth_nonce_cookie_config = 6;
61+
}
62+
2863
// [#next-free-field: 6]
2964
message OAuth2Credentials {
3065
// [#next-free-field: 7]
@@ -84,7 +119,7 @@ message OAuth2Credentials {
84119

85120
// OAuth config
86121
//
87-
// [#next-free-field: 21]
122+
// [#next-free-field: 22]
88123
message OAuth2Config {
89124
enum AuthType {
90125
// The ``client_id`` and ``client_secret`` will be sent in the URL encoded request body.
@@ -186,6 +221,9 @@ message OAuth2Config {
186221
// will still process incoming Refresh Tokens as part of the HMAC if they are there. This is to ensure compatibility while switching this setting on. Future
187222
// sessions would not set the Refresh Token cookie header.
188223
bool disable_refresh_token_set_cookie = 20;
224+
225+
// Controls for attributes that can be set on the cookies.
226+
CookieConfigs cookie_configs = 21;
189227
}
190228

191229
// Filter config.

changelogs/current.yaml

+7
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ removed_config_or_runtime:
2424
Removed runtime flag ``envoy.reloadable_features.dns_details`` and legacy code paths.
2525
2626
new_features:
27+
- area: oauth2
28+
change: |
29+
Add the option to specify SameSite cookie attribute values for oauth2 supported cookies.
30+
To specify SameSite attribute, choose one of the values from ``strict``,``lax`` or ``none``. If not specified,
31+
a default value of ``disabled`` will be assigned and there will be no SameSite value in the cookie attribute. See
32+
:ref:`apply_on_stream_done <envoy_v3_api_field_extensions.filters.http.oauth2.v3.OAuth2Config.cookie_configs>`
33+
for more details.
2734
- area: spiffe
2835
change: |
2936
Added :ref:`trust_bundles

source/extensions/filters/http/oauth2/filter.cc

+109-33
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ Http::RegisterCustomInlineHeader<Http::CustomInlineHeaderRegistry::Type::Request
4141

4242
constexpr const char* CookieDeleteFormatString =
4343
"{}=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT";
44-
constexpr const char* CookieTailHttpOnlyFormatString = ";path=/;Max-Age={};secure;HttpOnly";
44+
constexpr const char* CookieTailHttpOnlyFormatString = ";path=/;Max-Age={};secure;HttpOnly{}";
4545
constexpr const char* CookieDomainFormatString = ";domain={}";
4646

4747
constexpr absl::string_view UnauthorizedBodyMessage = "OAuth flow failed.";
@@ -60,6 +60,9 @@ constexpr absl::string_view REDIRECT_FOR_CREDENTIALS = "oauth.missing_credential
6060
constexpr absl::string_view SIGN_OUT = "oauth.sign_out";
6161
constexpr absl::string_view DEFAULT_AUTH_SCOPE = "user";
6262

63+
constexpr absl::string_view SameSiteLax = ";SameSite=Lax";
64+
constexpr absl::string_view SameSiteStrict = ";SameSite=Strict";
65+
constexpr absl::string_view SameSiteNone = ";SameSite=None";
6366
constexpr absl::string_view HmacPayloadSeparator = "\n";
6467

6568
template <class T>
@@ -129,6 +132,28 @@ getAuthType(envoy::extensions::filters::http::oauth2::v3::OAuth2Config_AuthType
129132
}
130133
}
131134

135+
// Helper function to get SameSite attribute string from proto enum.
136+
std::string
137+
getSameSiteString(envoy::extensions::filters::http::oauth2::v3::CookieConfig_SameSite same_site) {
138+
switch (same_site) {
139+
PANIC_ON_PROTO_ENUM_SENTINEL_VALUES;
140+
case envoy::extensions::filters::http::oauth2::v3::CookieConfig_SameSite::
141+
CookieConfig_SameSite_STRICT:
142+
return std::string(SameSiteStrict);
143+
case envoy::extensions::filters::http::oauth2::v3::CookieConfig_SameSite::
144+
CookieConfig_SameSite_LAX:
145+
return std::string(SameSiteLax);
146+
case envoy::extensions::filters::http::oauth2::v3::CookieConfig_SameSite::
147+
CookieConfig_SameSite_NONE:
148+
return std::string(SameSiteNone);
149+
case envoy::extensions::filters::http::oauth2::v3::CookieConfig_SameSite::
150+
CookieConfig_SameSite_DISABLED:
151+
return EMPTY_STRING;
152+
}
153+
IS_ENVOY_BUG("unexpected same_site enum value");
154+
return EMPTY_STRING;
155+
}
156+
132157
Http::Utility::QueryParamsMulti buildAutorizationQueryParams(
133158
const envoy::extensions::filters::http::oauth2::v3::OAuth2Config& proto_config) {
134159
auto query_params =
@@ -246,7 +271,37 @@ FilterConfig::FilterConfig(
246271
use_refresh_token_(FilterConfig::shouldUseRefreshToken(proto_config)),
247272
disable_id_token_set_cookie_(proto_config.disable_id_token_set_cookie()),
248273
disable_access_token_set_cookie_(proto_config.disable_access_token_set_cookie()),
249-
disable_refresh_token_set_cookie_(proto_config.disable_refresh_token_set_cookie()) {
274+
disable_refresh_token_set_cookie_(proto_config.disable_refresh_token_set_cookie()),
275+
bearer_token_cookie_settings_(
276+
(proto_config.has_cookie_configs() &&
277+
proto_config.cookie_configs().has_bearer_token_cookie_config())
278+
? CookieSettings(proto_config.cookie_configs().bearer_token_cookie_config())
279+
: CookieSettings()),
280+
hmac_cookie_settings_(
281+
(proto_config.has_cookie_configs() &&
282+
proto_config.cookie_configs().has_oauth_hmac_cookie_config())
283+
? CookieSettings(proto_config.cookie_configs().oauth_hmac_cookie_config())
284+
: CookieSettings()),
285+
expires_cookie_settings_(
286+
(proto_config.has_cookie_configs() &&
287+
proto_config.cookie_configs().has_oauth_expires_cookie_config())
288+
? CookieSettings(proto_config.cookie_configs().oauth_expires_cookie_config())
289+
: CookieSettings()),
290+
id_token_cookie_settings_(
291+
(proto_config.has_cookie_configs() &&
292+
proto_config.cookie_configs().has_id_token_cookie_config())
293+
? CookieSettings(proto_config.cookie_configs().id_token_cookie_config())
294+
: CookieSettings()),
295+
refresh_token_cookie_settings_(
296+
(proto_config.has_cookie_configs() &&
297+
proto_config.cookie_configs().has_refresh_token_cookie_config())
298+
? CookieSettings(proto_config.cookie_configs().refresh_token_cookie_config())
299+
: CookieSettings()),
300+
nonce_cookie_settings_(
301+
(proto_config.has_cookie_configs() &&
302+
proto_config.cookie_configs().has_oauth_nonce_cookie_config())
303+
? CookieSettings(proto_config.cookie_configs().oauth_nonce_cookie_config())
304+
: CookieSettings()) {
250305
if (!context.clusterManager().clusters().hasCluster(oauth_token_endpoint_.cluster())) {
251306
throw EnvoyException(fmt::format("OAuth2 filter: unknown cluster '{}' in config. Please "
252307
"specify which cluster to direct OAuth requests to.",
@@ -544,8 +599,10 @@ void OAuth2Filter::redirectToOAuthServer(Http::RequestHeaderMap& headers) {
544599
if (!csrf_token_cookie_exists) {
545600
// Expire the CSRF token cookie in 10 minutes.
546601
// This should be enough time for the user to complete the OAuth flow.
547-
std::string expire_in = std::to_string(10 * 60);
548-
std::string cookie_tail_http_only = fmt::format(CookieTailHttpOnlyFormatString, expire_in);
602+
std::string csrf_expires = std::to_string(10 * 60);
603+
std::string same_site = getSameSiteString(config_->nonceCookieSettings().same_site_);
604+
std::string cookie_tail_http_only =
605+
fmt::format(CookieTailHttpOnlyFormatString, csrf_expires, same_site);
549606
if (!config_->cookieDomain().empty()) {
550607
cookie_tail_http_only = absl::StrCat(
551608
fmt::format(CookieDomainFormatString, config_->cookieDomain()), cookie_tail_http_only);
@@ -728,6 +785,43 @@ std::string OAuth2Filter::getExpiresTimeForIdToken(const std::string& id_token,
728785
return std::to_string(expires_in.count());
729786
}
730787

788+
// Helper function to build the cookie tail string.
789+
std::string OAuth2Filter::BuildCookieTail(int cookie_type) const {
790+
std::string same_site;
791+
std::string expires_time = expires_in_;
792+
793+
switch (cookie_type) {
794+
PANIC_ON_PROTO_ENUM_SENTINEL_VALUES;
795+
case 1: // BEARER_TOKEN TYPE
796+
same_site = getSameSiteString(config_->bearerTokenCookieSettings().same_site_);
797+
break;
798+
case 2: // OAUTH_HMAC TYPE
799+
same_site = getSameSiteString(config_->hmacCookieSettings().same_site_);
800+
break;
801+
case 3: // OAUTH_EXPIRES TYPE
802+
same_site = getSameSiteString(config_->expiresCookieSettings().same_site_);
803+
break;
804+
case 4: // ID_TOKEN TYPE
805+
same_site = getSameSiteString(config_->idTokenCookieSettings().same_site_);
806+
expires_time = expires_id_token_in_;
807+
break;
808+
case 5: // REFRESH_TOKEN TYPE
809+
same_site = getSameSiteString(config_->refreshTokenCookieSettings().same_site_);
810+
expires_time = expires_refresh_token_in_;
811+
break;
812+
case 6: // OAUTH_NONCE TYPE
813+
same_site = getSameSiteString(config_->refreshTokenCookieSettings().same_site_);
814+
break;
815+
}
816+
817+
std::string cookie_tail = fmt::format(CookieTailHttpOnlyFormatString, expires_time, same_site);
818+
if (!config_->cookieDomain().empty()) {
819+
cookie_tail =
820+
absl::StrCat(fmt::format(CookieDomainFormatString, config_->cookieDomain()), cookie_tail);
821+
}
822+
return cookie_tail;
823+
}
824+
731825
void OAuth2Filter::onGetAccessTokenSuccess(const std::string& access_code,
732826
const std::string& id_token,
733827
const std::string& refresh_token,
@@ -809,51 +903,33 @@ void OAuth2Filter::onRefreshAccessTokenFailure() {
809903
void OAuth2Filter::addResponseCookies(Http::ResponseHeaderMap& headers,
810904
const std::string& encoded_token) const {
811905
// We use HTTP Only cookies.
812-
std::string cookie_tail_http_only = fmt::format(CookieTailHttpOnlyFormatString, expires_in_);
813-
if (!config_->cookieDomain().empty()) {
814-
cookie_tail_http_only = absl::StrCat(
815-
fmt::format(CookieDomainFormatString, config_->cookieDomain()), cookie_tail_http_only);
816-
}
817-
818906
const CookieNames& cookie_names = config_->cookieNames();
819907

908+
// Set the cookies in the response headers.
820909
headers.addReferenceKey(
821910
Http::Headers::get().SetCookie,
822-
absl::StrCat(cookie_names.oauth_hmac_, "=", encoded_token, cookie_tail_http_only));
823-
headers.addReferenceKey(
824-
Http::Headers::get().SetCookie,
825-
absl::StrCat(cookie_names.oauth_expires_, "=", new_expires_, cookie_tail_http_only));
911+
absl::StrCat(cookie_names.oauth_hmac_, "=", encoded_token, BuildCookieTail(2))); // OAUTH_HMAC
912+
913+
headers.addReferenceKey(Http::Headers::get().SetCookie,
914+
absl::StrCat(cookie_names.oauth_expires_, "=", new_expires_,
915+
BuildCookieTail(3))); // OAUTH_EXPIRES
826916

827917
if (!access_token_.empty()) {
828-
headers.addReferenceKey(
829-
Http::Headers::get().SetCookie,
830-
absl::StrCat(cookie_names.bearer_token_, "=", access_token_, cookie_tail_http_only));
918+
headers.addReferenceKey(Http::Headers::get().SetCookie,
919+
absl::StrCat(cookie_names.bearer_token_, "=", access_token_,
920+
BuildCookieTail(1))); // BEARER_TOKEN
831921
}
832922

833923
if (!id_token_.empty()) {
834-
std::string id_token_cookie_tail_http_only =
835-
fmt::format(CookieTailHttpOnlyFormatString, expires_id_token_in_);
836-
if (!config_->cookieDomain().empty()) {
837-
id_token_cookie_tail_http_only =
838-
absl::StrCat(fmt::format(CookieDomainFormatString, config_->cookieDomain()),
839-
id_token_cookie_tail_http_only);
840-
}
841924
headers.addReferenceKey(
842925
Http::Headers::get().SetCookie,
843-
absl::StrCat(cookie_names.id_token_, "=", id_token_, id_token_cookie_tail_http_only));
926+
absl::StrCat(cookie_names.id_token_, "=", id_token_, BuildCookieTail(4))); // ID_TOKEN
844927
}
845928

846929
if (!refresh_token_.empty()) {
847-
std::string refresh_token_cookie_tail_http_only =
848-
fmt::format(CookieTailHttpOnlyFormatString, expires_refresh_token_in_);
849-
if (!config_->cookieDomain().empty()) {
850-
refresh_token_cookie_tail_http_only =
851-
absl::StrCat(fmt::format(CookieDomainFormatString, config_->cookieDomain()),
852-
refresh_token_cookie_tail_http_only);
853-
}
854930
headers.addReferenceKey(Http::Headers::get().SetCookie,
855931
absl::StrCat(cookie_names.refresh_token_, "=", refresh_token_,
856-
refresh_token_cookie_tail_http_only));
932+
BuildCookieTail(5))); // REFRESH_TOKEN
857933
}
858934
}
859935

source/extensions/filters/http/oauth2/filter.h

+28-1
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,26 @@ class FilterConfig {
168168
}
169169
bool shouldUseRefreshToken(
170170
const envoy::extensions::filters::http::oauth2::v3::OAuth2Config& proto_config) const;
171+
struct CookieSettings {
172+
CookieSettings(const envoy::extensions::filters::http::oauth2::v3::CookieConfig& config)
173+
: same_site_(config.same_site()) {}
174+
175+
// Default constructor
176+
CookieSettings()
177+
: same_site_(envoy::extensions::filters::http::oauth2::v3::CookieConfig_SameSite::
178+
CookieConfig_SameSite_DISABLED) {}
179+
180+
const envoy::extensions::filters::http::oauth2::v3::CookieConfig_SameSite same_site_;
181+
};
182+
183+
const CookieSettings& bearerTokenCookieSettings() const { return bearer_token_cookie_settings_; }
184+
const CookieSettings& hmacCookieSettings() const { return hmac_cookie_settings_; }
185+
const CookieSettings& expiresCookieSettings() const { return expires_cookie_settings_; }
186+
const CookieSettings& idTokenCookieSettings() const { return id_token_cookie_settings_; }
187+
const CookieSettings& refreshTokenCookieSettings() const {
188+
return refresh_token_cookie_settings_;
189+
}
190+
const CookieSettings& nonceCookieSettings() const { return nonce_cookie_settings_; }
171191

172192
private:
173193
static FilterStats generateStats(const std::string& prefix, Stats::Scope& scope);
@@ -199,6 +219,12 @@ class FilterConfig {
199219
const bool disable_access_token_set_cookie_ : 1;
200220
const bool disable_refresh_token_set_cookie_ : 1;
201221
absl::optional<RouteRetryPolicy> retry_policy_;
222+
const CookieSettings bearer_token_cookie_settings_;
223+
const CookieSettings hmac_cookie_settings_;
224+
const CookieSettings expires_cookie_settings_;
225+
const CookieSettings id_token_cookie_settings_;
226+
const CookieSettings refresh_token_cookie_settings_;
227+
const CookieSettings nonce_cookie_settings_;
202228
};
203229

204230
using FilterConfigSharedPtr = std::shared_ptr<FilterConfig>;
@@ -268,7 +294,7 @@ class OAuth2Filter : public Http::PassThroughFilter,
268294
Logger::Loggable<Logger::Id::oauth2> {
269295
public:
270296
OAuth2Filter(FilterConfigSharedPtr config, std::unique_ptr<OAuth2Client>&& oauth_client,
271-
TimeSource& time_source, Random::RandomGenerator& random);
297+
TimeSource& time_source, Random::RandomGenerator& randomhmacCookieSettings);
272298

273299
// Http::PassThroughFilter
274300
Http::FilterHeadersStatus decodeHeaders(Http::RequestHeaderMap& headers, bool) override;
@@ -331,6 +357,7 @@ class OAuth2Filter : public Http::PassThroughFilter,
331357
const std::chrono::seconds& expires_in) const;
332358
std::string getExpiresTimeForIdToken(const std::string& id_token,
333359
const std::chrono::seconds& expires_in) const;
360+
std::string BuildCookieTail(int cookie_type) const;
334361
void addResponseCookies(Http::ResponseHeaderMap& headers, const std::string& encoded_token) const;
335362
const std::string& bearerPrefix() const;
336363
CallbackValidationResult validateOAuthCallback(const Http::RequestHeaderMap& headers,

0 commit comments

Comments
 (0)