Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ minor_behavior_changes:
change: |
:ref:`Credential injector filter <envoy_v3_api_msg_extensions.filters.http.credential_injector.v3.CredentialInjector>` is no longer
a work in progress field.
- area: oauth2
change: |
The access token, id token and refresh token in the cookies are now encrypted using the HMAC secret. This behavior can
be reverted by setting the runtime guard ``envoy.reloadable_features.oauth2_encrypt_tokens`` to ``false``.
Comment thread
zhaohuabing marked this conversation as resolved.
- area: http3
change: |
Validate HTTP/3 pseudo headers. Can be disabled by setting ``envoy.restart_features.validate_http3_pseudo_headers`` to false.
Expand Down
1 change: 1 addition & 0 deletions source/common/runtime/runtime_features.cc
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ RUNTIME_GUARD(envoy_reloadable_features_local_reply_traverses_filter_chain_after
RUNTIME_GUARD(envoy_reloadable_features_mmdb_files_reload_enabled);
RUNTIME_GUARD(envoy_reloadable_features_no_extension_lookup_by_name);
RUNTIME_GUARD(envoy_reloadable_features_normalize_rds_provider_config);
RUNTIME_GUARD(envoy_reloadable_features_oauth2_encrypt_tokens);
RUNTIME_GUARD(envoy_reloadable_features_oauth2_use_refresh_token);
RUNTIME_GUARD(envoy_reloadable_features_original_dst_rely_on_idle_timeout);
RUNTIME_GUARD(envoy_reloadable_features_original_src_fix_port_exhaustion);
Expand Down
106 changes: 87 additions & 19 deletions source/extensions/filters/http/oauth2/filter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,7 @@ void OAuth2CookieValidator::setParams(const Http::RequestHeaderMap& headers,
});

expires_ = findValue(cookies, cookie_names_.oauth_expires_);
token_ = findValue(cookies, cookie_names_.bearer_token_);
access_token_ = findValue(cookies, cookie_names_.bearer_token_);
id_token_ = findValue(cookies, cookie_names_.id_token_);
refresh_token_ = findValue(cookies, cookie_names_.refresh_token_);
hmac_ = findValue(cookies, cookie_names_.oauth_hmac_);
Expand All @@ -540,9 +540,9 @@ bool OAuth2CookieValidator::hmacIsValid() const {
if (!cookie_domain_.empty()) {
cookie_domain = cookie_domain_;
}
return ((encodeHmacBase64(secret_, cookie_domain, expires_, token_, id_token_, refresh_token_) ==
hmac_) ||
(encodeHmacHexBase64(secret_, cookie_domain, expires_, token_, id_token_,
return ((encodeHmacBase64(secret_, cookie_domain, expires_, access_token_, id_token_,
refresh_token_) == hmac_) ||
(encodeHmacHexBase64(secret_, cookie_domain, expires_, access_token_, id_token_,
refresh_token_) == hmac_));
}

Expand Down Expand Up @@ -578,6 +578,12 @@ OAuth2Filter::OAuth2Filter(FilterConfigSharedPtr config,
* 5) user is unauthorized
*/
Http::FilterHeadersStatus OAuth2Filter::decodeHeaders(Http::RequestHeaderMap& headers, bool) {
// Decrypt the OAuth tokens and update the OAuth tokens in the request headers before forwarding
// the request upstream.
if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.oauth2_encrypt_tokens")) {
decryptAndUpdateOAuthTokenCookies(headers);
}

// Skip Filter and continue chain if a Passthrough header is matching
// Must be done before the sanitation of the authorization header,
// otherwise the authorization header might be altered or removed
Expand Down Expand Up @@ -713,7 +719,8 @@ Http::FilterHeadersStatus OAuth2Filter::decodeHeaders(Http::RequestHeaderMap& he

DecryptResult decrypt_result = decrypt(encrypted_code_verifier, config_->hmacSecret());
if (decrypt_result.error.has_value()) {
ENVOY_LOG(error, "decryption failed: {}", decrypt_result.error.value());
ENVOY_LOG(error, "failed to decrypt code verifier: {}, error: {}", encrypted_code_verifier,
decrypt_result.error.value());
sendUnauthorizedResponse();
return Http::FilterHeadersStatus::StopIteration;
}
Expand Down Expand Up @@ -754,6 +761,66 @@ bool OAuth2Filter::canSkipOAuth(Http::RequestHeaderMap& headers) const {
return false;
}

// Decrypt the OAuth tokens and updates the OAuth tokens in the request cookies before forwarding
// the request upstream.
void OAuth2Filter::decryptAndUpdateOAuthTokenCookies(Http::RequestHeaderMap& headers) const {
absl::flat_hash_map<std::string, std::string> cookies = Http::Utility::parseCookies(headers);
if (cookies.empty()) {
return;
}

const CookieNames& cookie_names = config_->cookieNames();

const std::string encrypted_access_token = findValue(cookies, cookie_names.bearer_token_);
const std::string encrypted_id_token = findValue(cookies, cookie_names.id_token_);
const std::string encrypted_refresh_token = findValue(cookies, cookie_names.refresh_token_);

if (!encrypted_access_token.empty()) {
cookies.insert_or_assign(cookie_names.bearer_token_, decryptToken(encrypted_access_token));
}

if (!encrypted_id_token.empty()) {
cookies.insert_or_assign(cookie_names.id_token_, decryptToken(encrypted_id_token));
}

if (!encrypted_refresh_token.empty()) {
cookies.insert_or_assign(cookie_names.refresh_token_, decryptToken(encrypted_refresh_token));
}

if (!encrypted_access_token.empty() || !encrypted_id_token.empty() ||
!encrypted_refresh_token.empty()) {
std::string new_cookies(absl::StrJoin(cookies, "; ", absl::PairFormatter("=")));
headers.setReferenceKey(Http::Headers::get().Cookie, new_cookies);
}
}

std::string OAuth2Filter::encryptToken(const std::string& token) const {
if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.oauth2_encrypt_tokens")) {
return encrypt(token, config_->hmacSecret(), random_);
}
return token;
}

std::string OAuth2Filter::decryptToken(const std::string& encrypted_token) const {
if (encrypted_token.empty()) {
return EMPTY_STRING;
}

DecryptResult decrypt_result = decrypt(encrypted_token, config_->hmacSecret());
if (decrypt_result.error.has_value()) {
ENVOY_LOG(error, "failed to decrypt token: {}, error: {}", encrypted_token,
decrypt_result.error.value());
// There are two cases:
// 1. The token is a legacy unencrypted token.
// In this case, we return the token as-is to allow the request to proceed.
// 2. The token is encrypted, but the decryption failed due to the HMAC secret is changed.
// In this case, we return the original encrypted token, the HMAC validation will fail
// and the user will be redirected to the OAuth server for re-authentication.
return encrypted_token;
}
return decrypt_result.plaintext;
}

bool OAuth2Filter::canRedirectToOAuthServer(Http::RequestHeaderMap& headers) const {
for (const auto& matcher : config_->denyRedirectMatchers()) {
if (matcher->matchesHeaders(headers)) {
Expand Down Expand Up @@ -827,9 +894,6 @@ void OAuth2Filter::redirectToOAuthServer(Http::RequestHeaderMap& headers) {

// Generate a PKCE code verifier and challenge for the OAuth flow.
const std::string code_verifier = generateCodeVerifier(random_);
// Encrypt the code verifier, using HMAC secret as the symmetric key.
const std::string encrypted_code_verifier =
encrypt(code_verifier, config_->hmacSecret(), random_);

const std::chrono::seconds code_verifier_token_expires_in =
config_->getCodeVerifierTokenExpiresIn();
Expand All @@ -842,9 +906,10 @@ void OAuth2Filter::redirectToOAuthServer(Http::RequestHeaderMap& headers) {
cookie_tail_http_only = absl::StrCat(
fmt::format(CookieDomainFormatString, config_->cookieDomain()), cookie_tail_http_only);
}
response_headers->addReferenceKey(Http::Headers::get().SetCookie,
absl::StrCat(config_->cookieNames().code_verifier_, "=",
encrypted_code_verifier, cookie_tail_http_only));
response_headers->addReferenceKey(
Http::Headers::get().SetCookie,
absl::StrCat(config_->cookieNames().code_verifier_, "=",
encrypt(code_verifier, config_->hmacSecret(), random_), cookie_tail_http_only));

const std::string code_challenge = generateCodeChallenge(code_verifier);
query_params.overwrite(queryParamsCodeChallenge, code_challenge);
Expand All @@ -866,7 +931,7 @@ void OAuth2Filter::redirectToOAuthServer(Http::RequestHeaderMap& headers) {
/**
* Modifies the state of the filter by adding response headers to the decoder_callbacks
*/
Http::FilterHeadersStatus OAuth2Filter::signOutUser(const Http::RequestHeaderMap& headers) {
Http::FilterHeadersStatus OAuth2Filter::signOutUser(const Http::RequestHeaderMap& headers) const {
Http::ResponseHeaderMapPtr response_headers{Http::createHeaderMap<Http::ResponseHeaderMapImpl>(
{{Http::Headers::get().Status, std::to_string(enumToInt(Http::Code::Found))}})};
std::string cookie_domain;
Expand Down Expand Up @@ -1163,7 +1228,8 @@ void OAuth2Filter::addResponseCookies(Http::ResponseHeaderMap& headers,

if (!access_token_.empty()) {
headers.addReferenceKey(Http::Headers::get().SetCookie,
absl::StrCat(cookie_names.bearer_token_, "=", access_token_,
absl::StrCat(cookie_names.bearer_token_, "=",
encryptToken(access_token_),
BuildCookieTail(1))); // BEARER_TOKEN
} else if (request_cookies.contains(cookie_names.bearer_token_)) {
headers.addReferenceKey(
Expand All @@ -1173,9 +1239,9 @@ void OAuth2Filter::addResponseCookies(Http::ResponseHeaderMap& headers,
}

if (!id_token_.empty()) {
headers.addReferenceKey(
Http::Headers::get().SetCookie,
absl::StrCat(cookie_names.id_token_, "=", id_token_, BuildCookieTail(4))); // ID_TOKEN
headers.addReferenceKey(Http::Headers::get().SetCookie,
absl::StrCat(cookie_names.id_token_, "=", encryptToken(id_token_),
BuildCookieTail(4))); // ID_TOKEN
} else if (request_cookies.contains(cookie_names.id_token_)) {
headers.addReferenceKey(
Http::Headers::get().SetCookie,
Expand All @@ -1185,7 +1251,8 @@ void OAuth2Filter::addResponseCookies(Http::ResponseHeaderMap& headers,

if (!refresh_token_.empty()) {
headers.addReferenceKey(Http::Headers::get().SetCookie,
absl::StrCat(cookie_names.refresh_token_, "=", refresh_token_,
absl::StrCat(cookie_names.refresh_token_, "=",
encryptToken(refresh_token_),
BuildCookieTail(5))); // REFRESH_TOKEN
} else if (request_cookies.contains(cookie_names.refresh_token_)) {
headers.addReferenceKey(
Expand All @@ -1206,8 +1273,9 @@ void OAuth2Filter::sendUnauthorizedResponse() {
// * Does the query parameters contain the code and state?
// * Does the state contain the original request URL and the CSRF token?
// * Does the CSRF token in the state match the one in the cookie?
CallbackValidationResult OAuth2Filter::validateOAuthCallback(const Http::RequestHeaderMap& headers,
const absl::string_view path_str) {
CallbackValidationResult
OAuth2Filter::validateOAuthCallback(const Http::RequestHeaderMap& headers,
const absl::string_view path_str) const {
// Return 401 unauthorized if the query parameters contain an error response.
const auto query_parameters = Http::Utility::QueryParamsMulti::parseQueryString(path_str);
if (query_parameters.getFirstValue(queryParamsError).has_value()) {
Expand Down
13 changes: 8 additions & 5 deletions source/extensions/filters/http/oauth2/filter.h
Original file line number Diff line number Diff line change
Expand Up @@ -268,13 +268,13 @@ class CookieValidator {
virtual bool canUpdateTokenByRefreshToken() const PURE;
};

class OAuth2CookieValidator : public CookieValidator {
class OAuth2CookieValidator : public CookieValidator, Logger::Loggable<Logger::Id::oauth2> {
public:
explicit OAuth2CookieValidator(TimeSource& time_source, const CookieNames& cookie_names,
const std::string& cookie_domain)
: time_source_(time_source), cookie_names_(cookie_names), cookie_domain_(cookie_domain) {}

const std::string& token() const override { return token_; }
const std::string& token() const override { return access_token_; }
const std::string& refreshToken() const override { return refresh_token_; }

void setParams(const Http::RequestHeaderMap& headers, const std::string& secret) override;
Expand All @@ -284,7 +284,7 @@ class OAuth2CookieValidator : public CookieValidator {
bool canUpdateTokenByRefreshToken() const override;

private:
std::string token_;
std::string access_token_;
std::string id_token_;
std::string refresh_token_;
std::string expires_;
Expand Down Expand Up @@ -368,7 +368,7 @@ class OAuth2Filter : public Http::PassThroughFilter,
bool canRedirectToOAuthServer(Http::RequestHeaderMap& headers) const;
void redirectToOAuthServer(Http::RequestHeaderMap& headers);

Http::FilterHeadersStatus signOutUser(const Http::RequestHeaderMap& headers);
Http::FilterHeadersStatus signOutUser(const Http::RequestHeaderMap& headers) const;

std::string getEncodedToken() const;
std::string getExpiresTimeForRefreshToken(const std::string& refresh_token,
Expand All @@ -379,9 +379,12 @@ class OAuth2Filter : public Http::PassThroughFilter,
void addResponseCookies(Http::ResponseHeaderMap& headers, const std::string& encoded_token) const;
const std::string& bearerPrefix() const;
CallbackValidationResult validateOAuthCallback(const Http::RequestHeaderMap& headers,
const absl::string_view path_str);
const absl::string_view path_str) const;
bool validateCsrfToken(const Http::RequestHeaderMap& headers,
const std::string& csrf_token) const;
void decryptAndUpdateOAuthTokenCookies(Http::RequestHeaderMap& headers) const;
std::string encryptToken(const std::string& token) const;
std::string decryptToken(const std::string& encrypted_token) const;
};

} // namespace Oauth2
Expand Down
Loading
Loading