diff --git a/api/envoy/extensions/filters/http/oauth2/v3/oauth.proto b/api/envoy/extensions/filters/http/oauth2/v3/oauth.proto index c0d1e538a55ac..7f6a50436b718 100644 --- a/api/envoy/extensions/filters/http/oauth2/v3/oauth.proto +++ b/api/envoy/extensions/filters/http/oauth2/v3/oauth.proto @@ -39,7 +39,7 @@ message CookieConfig { SameSite same_site = 1 [(validate.rules).enum = {defined_only: true}]; } -// [#next-free-field: 7] +// [#next-free-field: 8] message CookieConfigs { // Configuration for the bearer token cookie. CookieConfig bearer_token_cookie_config = 1; @@ -58,11 +58,14 @@ message CookieConfigs { // Configuration for the OAuth nonce cookie. CookieConfig oauth_nonce_cookie_config = 6; + + // Configuration for the code verifier cookie. + CookieConfig code_verifier_cookie_config = 7; } // [#next-free-field: 6] message OAuth2Credentials { - // [#next-free-field: 7] + // [#next-free-field: 8] message CookieNames { // Cookie name to hold OAuth bearer token value. When the authentication server validates the // client and returns an authorization token back to the OAuth filter, no matter what format @@ -91,6 +94,10 @@ message OAuth2Credentials { // Cookie name to hold the nonce value. Defaults to ``OauthNonce``. string oauth_nonce = 6 [(validate.rules).string = {well_known_regex: HTTP_HEADER_NAME ignore_empty: true}]; + + // Cookie name to hold the PKCE code verifier. Defaults to ``OauthCodeVerifier``. + string code_verifier = 7 + [(validate.rules).string = {well_known_regex: HTTP_HEADER_NAME ignore_empty: true}]; } // The client_id to be used in the authorize calls. This value will be URL encoded when sent to the OAuth server. diff --git a/changelogs/current.yaml b/changelogs/current.yaml index ed17868ce2bc3..bb78951b0a892 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -39,6 +39,9 @@ minor_behavior_changes: change: | The formatter ``%CEL%`` and ``%METADATA%`` will be treated as built-in formatters and could be used directly in the substitution format string if the related extensions are linked. +- area: oauth2 + change: | + Introduced PKCE(Proof Key for Code Exchange) support for OAuth2 authorization code flow. bug_fixes: # *Changes expected to improve the state of the world and are unlikely to have negative effects* diff --git a/source/extensions/filters/http/oauth2/filter.cc b/source/extensions/filters/http/oauth2/filter.cc index d07747443f783..15e5b91afa11d 100644 --- a/source/extensions/filters/http/oauth2/filter.cc +++ b/source/extensions/filters/http/oauth2/filter.cc @@ -27,6 +27,7 @@ #include "absl/strings/str_split.h" #include "jwt_verify_lib/jwt.h" #include "jwt_verify_lib/status.h" +#include "openssl/rand.h" using namespace std::chrono_literals; @@ -50,6 +51,8 @@ constexpr absl::string_view queryParamsError = "error"; constexpr absl::string_view queryParamsCode = "code"; constexpr absl::string_view queryParamsState = "state"; constexpr absl::string_view queryParamsRedirectUri = "redirect_uri"; +constexpr absl::string_view queryParamsCodeChallenge = "code_challenge"; +constexpr absl::string_view queryParamsCodeChallengeMethod = "code_challenge_method"; constexpr absl::string_view stateParamsUrl = "url"; constexpr absl::string_view stateParamsCsrfToken = "csrf_token"; @@ -227,6 +230,30 @@ bool validateCsrfTokenHmac(const std::string& hmac_secret, const std::string& cs return generateHmacBase64(hmac_secret_vec, token) == hmac; } +// Generates a PKCE code verifier with 32 octets of randomness. +// This follows recommendations in RFC 7636: +// https://datatracker.ietf.org/doc/html/rfc7636#section-7.1 +std::string generateCodeVerifier(Random::RandomGenerator& random) { + MemBlockBuilder mem_block(4); + // create 4 random uint64_t values to fill the buffer because RFC 7636 recommends 32 octets of + // randomness. + for (size_t i = 0; i < 4; i++) { + mem_block.appendOne(random.random()); + } + + std::unique_ptr data = mem_block.release(); + return Base64Url::encode(reinterpret_cast(data.get()), 4 * sizeof(uint64_t)); +} + +// Generates a PKCE code challenge from a code verifier. +std::string generateCodeChallenge(const std::string& code_verifier) { + auto& crypto_util = Envoy::Common::Crypto::UtilitySingleton::get(); + std::vector sha256_digest = + crypto_util.getSha256Digest(Buffer::OwnedImpl(code_verifier)); + std::string sha256_string(sha256_digest.begin(), sha256_digest.end()); + return Base64Url::encode(sha256_string.data(), sha256_string.size()); +} + /** * Encodes the state parameter for the OAuth2 flow. * The state parameter is a base64Url encoded JSON object containing the original request URL and a @@ -241,6 +268,129 @@ std::string encodeState(const std::string& original_request_url, const std::stri return Base64Url::encode(json.data(), json.size()); } +/** + * Encrypt a plaintext string using AES-256-CBC. + */ +std::string encrypt(const std::string& plaintext, const std::string& secret, + Random::RandomGenerator& random) { + // Generate the key from the secret using SHA-256 + std::vector key(SHA256_DIGEST_LENGTH); // AES-256 requires 256-bit (32 bytes) key + SHA256(reinterpret_cast(secret.c_str()), secret.size(), key.data()); + + // Generate a random IV + MemBlockBuilder mem_block(4); + // create 2 random uint64_t values to fill the buffer because AES-256-CBC requires 16 bytes IV + for (size_t i = 0; i < 2; i++) { + mem_block.appendOne(random.random()); + } + + std::unique_ptr data = mem_block.release(); + const unsigned char* raw_data = reinterpret_cast(data.get()); + + // AES uses 16-byte IV + std::vector iv(16); + iv.assign(raw_data, raw_data + 16); + + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + RELEASE_ASSERT(ctx, "Failed to create context"); + + std::vector ciphertext(plaintext.size() + EVP_MAX_BLOCK_LENGTH); + int len = 0, ciphertext_len = 0; + + // Initialize encryption operation + int result = EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), nullptr, key.data(), iv.data()); + RELEASE_ASSERT(result == 1, "Encryption initialization failed"); + + // Encrypt the plaintext + result = EVP_EncryptUpdate(ctx, ciphertext.data(), &len, + reinterpret_cast(plaintext.c_str()), + plaintext.size()); + RELEASE_ASSERT(result == 1, "Encryption update failed"); + + ciphertext_len += len; + + // Finalize encryption + result = EVP_EncryptFinal_ex(ctx, ciphertext.data() + len, &len); + RELEASE_ASSERT(result == 1, "Encryption finalization failed"); + + ciphertext_len += len; + + EVP_CIPHER_CTX_free(ctx); + + // AES uses 16-byte IV + ciphertext.resize(ciphertext_len); + + // Prepend the IV to the ciphertext + std::vector combined(iv.size() + ciphertext.size()); + std::copy(iv.begin(), iv.end(), combined.begin()); + std::copy(ciphertext.begin(), ciphertext.end(), combined.begin() + iv.size()); + + // Base64Url encode the IV + ciphertext + return Base64Url::encode(reinterpret_cast(combined.data()), combined.size()); +} + +struct DecryptResult { + std::string plaintext; + absl::optional error; +}; + +/** + * Decrypt an AES-256-CBC encrypted string. + */ +DecryptResult decrypt(const std::string& encrypted, const std::string& secret) { + // Decode the Base64Url-encoded input + std::string decoded = Base64Url::decode(encrypted); + std::vector combined(decoded.begin(), decoded.end()); + + if (combined.size() <= 16) { + return {"", "Invalid encrypted data"}; + } + + // Extract the IV (first 16 bytes) + std::vector iv(combined.begin(), combined.begin() + 16); + + // Extract the ciphertext (remaining bytes) + std::vector ciphertext(combined.begin() + 16, combined.end()); + + // Generate the key from the secret using SHA-256 + std::vector key(SHA256_DIGEST_LENGTH); + SHA256(reinterpret_cast(secret.c_str()), secret.size(), key.data()); + + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + RELEASE_ASSERT(ctx, "Failed to create context"); + + std::vector plaintext(ciphertext.size() + EVP_MAX_BLOCK_LENGTH); + int len = 0, plaintext_len = 0; + + // Initialize decryption operation + if (EVP_DecryptInit_ex(ctx, EVP_aes_256_cbc(), nullptr, key.data(), iv.data()) != 1) { + EVP_CIPHER_CTX_free(ctx); + return {"", "failed to initialize decryption"}; + } + + // Decrypt the ciphertext + if (EVP_DecryptUpdate(ctx, plaintext.data(), &len, ciphertext.data(), ciphertext.size()) != 1) { + EVP_CIPHER_CTX_free(ctx); + return {"", "failed to decrypt data"}; + } + plaintext_len += len; + + // Finalize decryption + if (EVP_DecryptFinal_ex(ctx, plaintext.data() + len, &len) != 1) { + EVP_CIPHER_CTX_free(ctx); + return {"", "failed to finalize decryption"}; + } + + plaintext_len += len; + + EVP_CIPHER_CTX_free(ctx); + + // Resize to actual plaintext length + plaintext.resize(plaintext_len); + + return {std::string(plaintext.begin(), plaintext.end()), std::nullopt}; +} + } // namespace FilterConfig::FilterConfig( @@ -518,8 +668,26 @@ Http::FilterHeadersStatus OAuth2Filter::decodeHeaders(Http::RequestHeaderMap& he Formatter::FormatterImpl::create(config_->redirectUri()), Formatter::FormatterPtr); const auto redirect_uri = formatter->formatWithContext({&headers}, decoder_callbacks_->streamInfo()); + + std::string encrypted_code_verifier = + Http::Utility::parseCookieValue(headers, config_->cookieNames().code_verifier_); + if (encrypted_code_verifier.empty()) { + ENVOY_LOG(error, "code verifier cookie is missing in the request"); + sendUnauthorizedResponse(); + return Http::FilterHeadersStatus::StopIteration; + } + + DecryptResult decrypt_result = decrypt(encrypted_code_verifier, config_->hmacSecret()); + if (decrypt_result.error.has_value()) { + ENVOY_LOG(error, "decryption failed: {}", decrypt_result.error.value()); + sendUnauthorizedResponse(); + return Http::FilterHeadersStatus::StopIteration; + } + + std::string code_verifier = decrypt_result.plaintext; + oauth_client_->asyncGetAccessToken(auth_code_, config_->clientId(), config_->clientSecret(), - redirect_uri, config_->authType()); + redirect_uri, code_verifier, config_->authType()); // pause while we await the next step from the OAuth server return Http::FilterHeadersStatus::StopAllIterationAndBuffer; @@ -580,22 +748,13 @@ void OAuth2Filter::redirectToOAuthServer(Http::RequestHeaderMap& headers) { // The CSRF token cookie contains the CSRF token that is used to prevent CSRF attacks for the // OAuth flow. It was named "oauth_nonce" because the CSRF token contains a generated nonce. // "oauth_csrf_token" would be a more accurate name for the cookie. - std::string csrf_token; - bool csrf_token_cookie_exists = false; - const auto csrf_token_cookie = - Http::Utility::parseCookies(headers, [this](absl::string_view key) { - return key == config_->cookieNames().oauth_nonce_; - }); - if (csrf_token_cookie.find(config_->cookieNames().oauth_nonce_) != csrf_token_cookie.end()) { - csrf_token = csrf_token_cookie.at(config_->cookieNames().oauth_nonce_); - csrf_token_cookie_exists = true; - } else { - // Generate a CSRF token to prevent CSRF attacks. - csrf_token = generateCsrfToken(config_->hmacSecret(), random_); - } - + std::string csrf_token = + Http::Utility::parseCookieValue(headers, config_->cookieNames().oauth_nonce_); + bool csrf_token_cookie_exists = !csrf_token.empty(); // Set the CSRF token cookie if it does not exist. if (!csrf_token_cookie_exists) { + // Generate a CSRF token to prevent CSRF attacks. + csrf_token = generateCsrfToken(config_->hmacSecret(), random_); // Expire the CSRF token cookie in 10 minutes. // This should be enough time for the user to complete the OAuth flow. std::string csrf_expires = std::to_string(10 * 60); @@ -629,6 +788,30 @@ void OAuth2Filter::redirectToOAuthServer(Http::RequestHeaderMap& headers) { const std::string escaped_redirect_uri = Http::Utility::PercentEncoding::urlEncode(redirect_uri); query_params.overwrite(queryParamsRedirectUri, escaped_redirect_uri); + // 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_); + + // Expire the code verifier cookie in 10 minutes. + // This should be enough time for the user to complete the OAuth flow. + std::string expire_in = std::to_string(10 * 60); + std::string same_site = getSameSiteString(config_->codeVerifierCookieSettings().same_site_); + std::string cookie_tail_http_only = + fmt::format(CookieTailHttpOnlyFormatString, expire_in, same_site); + if (!config_->cookieDomain().empty()) { + 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)); + + const std::string code_challenge = generateCodeChallenge(code_verifier); + query_params.overwrite(queryParamsCodeChallenge, code_challenge); + query_params.overwrite(queryParamsCodeChallengeMethod, "S256"); + // Copy the authorization endpoint URL to replace its query params. auto authorization_endpoint_url = config_->authorizationEndpointUrl(); const std::string path_and_query_params = query_params.replaceQueryString( @@ -676,6 +859,10 @@ Http::FilterHeadersStatus OAuth2Filter::signOutUser(const Http::RequestHeaderMap Http::Headers::get().SetCookie, absl::StrCat(fmt::format(CookieDeleteFormatString, config_->cookieNames().oauth_nonce_), cookie_domain)); + response_headers->addReferenceKey( + Http::Headers::get().SetCookie, + absl::StrCat(fmt::format(CookieDeleteFormatString, config_->cookieNames().code_verifier_), + cookie_domain)); response_headers->setLocation(new_path); decoder_callbacks_->encodeHeaders(std::move(response_headers), true, SIGN_OUT); diff --git a/source/extensions/filters/http/oauth2/filter.h b/source/extensions/filters/http/oauth2/filter.h index 8005356a26b1a..24080da62db74 100644 --- a/source/extensions/filters/http/oauth2/filter.h +++ b/source/extensions/filters/http/oauth2/filter.h @@ -90,17 +90,20 @@ struct CookieNames { cookie_names) : CookieNames(cookie_names.bearer_token(), cookie_names.oauth_hmac(), cookie_names.oauth_expires(), cookie_names.id_token(), - cookie_names.refresh_token(), cookie_names.oauth_nonce()) {} + cookie_names.refresh_token(), cookie_names.oauth_nonce(), + cookie_names.code_verifier()) {} CookieNames(const std::string& bearer_token, const std::string& oauth_hmac, const std::string& oauth_expires, const std::string& id_token, - const std::string& refresh_token, const std::string& oauth_nonce) + const std::string& refresh_token, const std::string& oauth_nonce, + const std::string& code_verifier) : bearer_token_(bearer_token.empty() ? BearerToken : bearer_token), oauth_hmac_(oauth_hmac.empty() ? OauthHMAC : oauth_hmac), oauth_expires_(oauth_expires.empty() ? OauthExpires : oauth_expires), id_token_(id_token.empty() ? IdToken : id_token), refresh_token_(refresh_token.empty() ? RefreshToken : refresh_token), - oauth_nonce_(oauth_nonce.empty() ? OauthNonce : oauth_nonce) {} + oauth_nonce_(oauth_nonce.empty() ? OauthNonce : oauth_nonce), + code_verifier_(code_verifier.empty() ? CodeVerifier : code_verifier) {} const std::string bearer_token_; const std::string oauth_hmac_; @@ -108,6 +111,7 @@ struct CookieNames { const std::string id_token_; const std::string refresh_token_; const std::string oauth_nonce_; + const std::string code_verifier_; static constexpr absl::string_view OauthExpires = "OauthExpires"; static constexpr absl::string_view BearerToken = "BearerToken"; @@ -115,6 +119,7 @@ struct CookieNames { static constexpr absl::string_view OauthNonce = "OauthNonce"; static constexpr absl::string_view IdToken = "IdToken"; static constexpr absl::string_view RefreshToken = "RefreshToken"; + static constexpr absl::string_view CodeVerifier = "CodeVerifier"; }; /** @@ -188,6 +193,9 @@ class FilterConfig { return refresh_token_cookie_settings_; } const CookieSettings& nonceCookieSettings() const { return nonce_cookie_settings_; } + const CookieSettings& codeVerifierCookieSettings() const { + return code_verifier_cookie_settings_; + } private: static FilterStats generateStats(const std::string& prefix, Stats::Scope& scope); @@ -225,6 +233,7 @@ class FilterConfig { const CookieSettings id_token_cookie_settings_; const CookieSettings refresh_token_cookie_settings_; const CookieSettings nonce_cookie_settings_; + const CookieSettings code_verifier_cookie_settings_; }; using FilterConfigSharedPtr = std::shared_ptr; diff --git a/source/extensions/filters/http/oauth2/oauth_client.cc b/source/extensions/filters/http/oauth2/oauth_client.cc index de3328b1138fd..562ccd48c0a8c 100644 --- a/source/extensions/filters/http/oauth2/oauth_client.cc +++ b/source/extensions/filters/http/oauth2/oauth_client.cc @@ -25,10 +25,11 @@ namespace Oauth2 { namespace { constexpr const char* UrlBodyTemplateWithCredentialsForAuthCode = - "grant_type=authorization_code&code={0}&client_id={1}&client_secret={2}&redirect_uri={3}"; + "grant_type=authorization_code&code={0}&client_id={1}&client_secret={2}&redirect_uri={3}&code_" + "verifier={4}"; constexpr const char* UrlBodyTemplateWithoutCredentialsForAuthCode = - "grant_type=authorization_code&code={0}&redirect_uri={1}"; + "grant_type=authorization_code&code={0}&redirect_uri={1}&code_verifier={2}"; constexpr const char* UrlBodyTemplateWithCredentialsForRefreshToken = "grant_type=refresh_token&refresh_token={0}&client_id={1}&client_secret={2}"; @@ -40,7 +41,8 @@ constexpr const char* UrlBodyTemplateWithoutCredentialsForRefreshToken = void OAuth2ClientImpl::asyncGetAccessToken(const std::string& auth_code, const std::string& client_id, const std::string& secret, - const std::string& cb_url, AuthType auth_type) { + const std::string& cb_url, + const std::string& code_verifier, AuthType auth_type) { ASSERT(state_ == OAuthState::Idle); state_ = OAuthState::PendingAccessToken; @@ -52,7 +54,8 @@ void OAuth2ClientImpl::asyncGetAccessToken(const std::string& auth_code, case AuthType::UrlEncodedBody: body = fmt::format(UrlBodyTemplateWithCredentialsForAuthCode, auth_code, Http::Utility::PercentEncoding::encode(client_id, ":/=&?"), - Http::Utility::PercentEncoding::encode(secret, ":/=&?"), encoded_cb_url); + Http::Utility::PercentEncoding::encode(secret, ":/=&?"), encoded_cb_url, + code_verifier); break; case AuthType::BasicAuth: const auto basic_auth_token = absl::StrCat(client_id, ":", secret); @@ -60,7 +63,8 @@ void OAuth2ClientImpl::asyncGetAccessToken(const std::string& auth_code, const auto basic_auth_header_value = absl::StrCat("Basic ", encoded_token); request->headers().appendCopy(Http::CustomHeaders::get().Authorization, basic_auth_header_value); - body = fmt::format(UrlBodyTemplateWithoutCredentialsForAuthCode, auth_code, encoded_cb_url); + body = fmt::format(UrlBodyTemplateWithoutCredentialsForAuthCode, auth_code, encoded_cb_url, + code_verifier); break; } diff --git a/source/extensions/filters/http/oauth2/oauth_client.h b/source/extensions/filters/http/oauth2/oauth_client.h index 6cf3ddfa122cb..16ba6e2105ea6 100644 --- a/source/extensions/filters/http/oauth2/oauth_client.h +++ b/source/extensions/filters/http/oauth2/oauth_client.h @@ -30,6 +30,7 @@ class OAuth2Client : public Http::AsyncClient::Callbacks { public: virtual void asyncGetAccessToken(const std::string& auth_code, const std::string& client_id, const std::string& secret, const std::string& cb_url, + const std::string& code_verifier, AuthType auth_type = AuthType::UrlEncodedBody) PURE; virtual void asyncRefreshAccessToken(const std::string& refresh_token, @@ -63,7 +64,7 @@ class OAuth2ClientImpl : public OAuth2Client, Logger::Loggable @@ -81,7 +85,7 @@ class MockOAuth2Client : public OAuth2Client { MOCK_METHOD(void, asyncGetAccessToken, (const std::string&, const std::string&, const std::string&, const std::string&, - Envoy::Extensions::HttpFilters::Oauth2::AuthType)); + const std::string&, Envoy::Extensions::HttpFilters::Oauth2::AuthType)); MOCK_METHOD(void, asyncRefreshAccessToken, (const std::string&, const std::string&, const std::string&, @@ -140,6 +144,9 @@ class OAuth2Test : public testing::TestWithParam { ::envoy::extensions::filters::http::oauth2::v3::CookieConfig_SameSite:: CookieConfig_SameSite_DISABLED, ::envoy::extensions::filters::http::oauth2::v3::CookieConfig_SameSite nonce_samesite = + ::envoy::extensions::filters::http::oauth2::v3::CookieConfig_SameSite:: + CookieConfig_SameSite_DISABLED, + ::envoy::extensions::filters::http::oauth2::v3::CookieConfig_SameSite code_verifier_samesite = ::envoy::extensions::filters::http::oauth2::v3::CookieConfig_SameSite:: CookieConfig_SameSite_DISABLED) { envoy::extensions::filters::http::oauth2::v3::OAuth2Config p; @@ -215,6 +222,10 @@ class OAuth2Test : public testing::TestWithParam { auto* oauth_nonce_config = cookie_configs->mutable_oauth_nonce_cookie_config(); oauth_nonce_config->set_same_site(nonce_samesite); + // Set value to disabled by default. + auto* code_verifier_config = cookie_configs->mutable_code_verifier_cookie_config(); + code_verifier_config->set_same_site(code_verifier_samesite); + MessageUtil::validate(p, ProtobufMessage::getStrictValidationVisitor()); // Create filter config. @@ -431,12 +442,15 @@ TEST_F(OAuth2Test, DefaultAuthScope) { Http::TestResponseHeaderMapImpl response_headers{ {Http::Headers::get().Status.get(), "302"}, {Http::Headers::get().SetCookie.get(), - "OauthNonce=" + TEST_STATE_CSRF_TOKEN + ";path=/;Max-Age=600;secure;HttpOnly"}, + "OauthNonce=" + TEST_CSRF_TOKEN + ";path=/;Max-Age=600;secure;HttpOnly"}, + {Http::Headers::get().SetCookie.get(), + "CodeVerifier=" + TEST_ENCRYPTED_CODE_VERIFIER + ";path=/;Max-Age=600;secure;HttpOnly"}, {Http::Headers::get().Location.get(), "https://auth.example.com/oauth/" "authorize/?client_id=" + - TEST_CLIENT_ID + + TEST_CLIENT_ID + "&code_challenge=" + TEST_CODE_CHALLENGE + + "&code_challenge_method=S256" + "&redirect_uri=https%3A%2F%2Ftraffic.example.com%2F_oauth" "&response_type=code" "&scope=" + @@ -493,11 +507,14 @@ TEST_F(OAuth2Test, PreservesQueryParametersInAuthorizationEndpoint) { Http::TestResponseHeaderMapImpl response_headers{ {Http::Headers::get().Status.get(), "302"}, {Http::Headers::get().SetCookie.get(), - "OauthNonce=" + TEST_STATE_CSRF_TOKEN + ";path=/;Max-Age=600;secure;HttpOnly"}, + "OauthNonce=" + TEST_CSRF_TOKEN + ";path=/;Max-Age=600;secure;HttpOnly"}, + {Http::Headers::get().SetCookie.get(), + "CodeVerifier=" + TEST_ENCRYPTED_CODE_VERIFIER + ";path=/;Max-Age=600;secure;HttpOnly"}, {Http::Headers::get().Location.get(), "https://auth.example.com/oauth/" "authorize/?client_id=" + - TEST_CLIENT_ID + + TEST_CLIENT_ID + "&code_challenge=" + TEST_CODE_CHALLENGE + + "&code_challenge_method=S256" + "&foo=bar" "&redirect_uri=https%3A%2F%2Ftraffic.example.com%2F_oauth" "&response_type=code" @@ -548,11 +565,14 @@ TEST_F(OAuth2Test, PreservesQueryParametersInAuthorizationEndpointWithUrlEncodin Http::TestResponseHeaderMapImpl response_headers{ {Http::Headers::get().Status.get(), "302"}, {Http::Headers::get().SetCookie.get(), - "OauthNonce=" + TEST_STATE_CSRF_TOKEN + ";path=/;Max-Age=600;secure;HttpOnly"}, + "OauthNonce=" + TEST_CSRF_TOKEN + ";path=/;Max-Age=600;secure;HttpOnly"}, + {Http::Headers::get().SetCookie.get(), + "CodeVerifier=" + TEST_ENCRYPTED_CODE_VERIFIER + ";path=/;Max-Age=600;secure;HttpOnly"}, {Http::Headers::get().Location.get(), "https://auth.example.com/oauth/" "authorize/?client_id=" + - TEST_CLIENT_ID + + TEST_CLIENT_ID + "&code_challenge=" + TEST_CODE_CHALLENGE + + "&code_challenge_method=S256" + "&foo=bar" "&redirect_uri=https%3A%2F%2Ftraffic.example.com%2F_oauth" "&response_type=code" @@ -590,6 +610,8 @@ TEST_F(OAuth2Test, RequestSignout) { "RefreshToken=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"}, {Http::Headers::get().SetCookie.get(), "OauthNonce=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"}, + {Http::Headers::get().SetCookie.get(), + "CodeVerifier=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"}, {Http::Headers::get().Location.get(), "https://traffic.example.com/"}, }; EXPECT_CALL(decoder_callbacks_, encodeHeaders_(HeaderMapEqualRef(&response_headers), true)); @@ -742,7 +764,9 @@ TEST_F(OAuth2Test, SetBearerToken) { Http::TestRequestHeaderMapImpl request_headers{ {Http::Headers::get().Path.get(), "/_oauth?code=123&state=" + TEST_ENCODED_STATE}, {Http::Headers::get().Cookie.get(), - "OauthNonce=" + TEST_STATE_CSRF_TOKEN + ";path=/;Max-Age=600;secure;HttpOnly"}, + "OauthNonce=" + TEST_CSRF_TOKEN + ";path=/;Max-Age=600;secure;HttpOnly"}, + {Http::Headers::get().Cookie.get(), + "CodeVerifier=" + TEST_ENCRYPTED_CODE_VERIFIER + ";path=/;Max-Age=600;secure;HttpOnly"}, {Http::Headers::get().Host.get(), "traffic.example.com"}, {Http::Headers::get().Scheme.get(), "https"}, {Http::Headers::get().Method.get(), Http::Headers::get().MethodValues.Get}, @@ -757,7 +781,7 @@ TEST_F(OAuth2Test, SetBearerToken) { EXPECT_CALL(*oauth_client_, asyncGetAccessToken("123", TEST_CLIENT_ID, "asdf_client_secret_fdsa", "https://traffic.example.com" + TEST_CALLBACK, - AuthType::UrlEncodedBody)); + TEST_CODE_VERIFIER, AuthType::UrlEncodedBody)); EXPECT_EQ(Http::FilterHeadersStatus::StopAllIterationAndBuffer, filter_->decodeHeaders(request_headers, false)); @@ -811,11 +835,14 @@ TEST_F(OAuth2Test, OAuthErrorNonOAuthHttpCallback) { Http::TestResponseHeaderMapImpl first_response_headers{ {Http::Headers::get().Status.get(), "302"}, {Http::Headers::get().SetCookie.get(), - "OauthNonce=" + TEST_STATE_CSRF_TOKEN + ";path=/;Max-Age=600;secure;HttpOnly"}, + "OauthNonce=" + TEST_CSRF_TOKEN + ";path=/;Max-Age=600;secure;HttpOnly"}, + {Http::Headers::get().SetCookie.get(), + "CodeVerifier=" + TEST_ENCRYPTED_CODE_VERIFIER + ";path=/;Max-Age=600;secure;HttpOnly"}, {Http::Headers::get().Location.get(), "https://auth.example.com/oauth/" "authorize/?client_id=" + - TEST_CLIENT_ID + + TEST_CLIENT_ID + "&code_challenge=" + TEST_CODE_CHALLENGE + + "&code_challenge_method=S256" + "&redirect_uri=https%3A%2F%2Ftraffic.example.com%2F_oauth" "&response_type=code" "&scope=" + @@ -840,7 +867,9 @@ TEST_F(OAuth2Test, OAuthErrorNonOAuthHttpCallback) { Http::TestRequestHeaderMapImpl second_request_headers{ {Http::Headers::get().Path.get(), "/_oauth?code=123&state=" + TEST_ENCODED_STATE}, {Http::Headers::get().Cookie.get(), - "OauthNonce=" + TEST_STATE_CSRF_TOKEN + ";path=/;Max-Age=600;secure;HttpOnly"}, + "OauthNonce=" + TEST_CSRF_TOKEN + ";path=/;Max-Age=600;secure;HttpOnly"}, + {Http::Headers::get().Cookie.get(), + "CodeVerifier=" + TEST_ENCRYPTED_CODE_VERIFIER + ";path=/;Max-Age=600;secure;HttpOnly"}, {Http::Headers::get().Host.get(), "traffic.example.com"}, {Http::Headers::get().Method.get(), Http::Headers::get().MethodValues.Get}, {Http::Headers::get().Scheme.get(), "https"}, @@ -852,7 +881,7 @@ TEST_F(OAuth2Test, OAuthErrorNonOAuthHttpCallback) { EXPECT_CALL(*oauth_client_, asyncGetAccessToken("123", TEST_CLIENT_ID, "asdf_client_secret_fdsa", "https://traffic.example.com" + TEST_CALLBACK, - AuthType::UrlEncodedBody)); + TEST_CODE_VERIFIER, AuthType::UrlEncodedBody)); // Invoke the callback logic. As a side effect, state_ will be populated. EXPECT_EQ(Http::FilterHeadersStatus::StopAllIterationAndBuffer, @@ -884,7 +913,7 @@ TEST_F(OAuth2Test, OAuthErrorNonOAuthHttpCallback) { EXPECT_CALL(*oauth_client_, asyncGetAccessToken("123", TEST_CLIENT_ID, "asdf_client_secret_fdsa", "https://traffic.example.com" + TEST_CALLBACK, - AuthType::UrlEncodedBody)); + TEST_CODE_VERIFIER, AuthType::UrlEncodedBody)); // Invoke the callback logic. As a side effect, state_ will be populated. EXPECT_EQ(Http::FilterHeadersStatus::StopAllIterationAndBuffer, @@ -933,7 +962,9 @@ TEST_F(OAuth2Test, OAuthCallbackStartsAuthentication) { Http::TestRequestHeaderMapImpl request_headers{ {Http::Headers::get().Path.get(), "/_oauth?code=123&state=" + TEST_ENCODED_STATE}, {Http::Headers::get().Cookie.get(), - "OauthNonce=" + TEST_STATE_CSRF_TOKEN + ";path=/;Max-Age=600;secure;HttpOnly"}, + "OauthNonce=" + TEST_CSRF_TOKEN + ";path=/;Max-Age=600;secure;HttpOnly"}, + {Http::Headers::get().Cookie.get(), + "CodeVerifier=" + TEST_ENCRYPTED_CODE_VERIFIER + ";path=/;Max-Age=600;secure;HttpOnly"}, {Http::Headers::get().Host.get(), "traffic.example.com"}, {Http::Headers::get().Scheme.get(), "https"}, {Http::Headers::get().Method.get(), Http::Headers::get().MethodValues.Get}, @@ -945,12 +976,67 @@ TEST_F(OAuth2Test, OAuthCallbackStartsAuthentication) { EXPECT_CALL(*oauth_client_, asyncGetAccessToken("123", TEST_CLIENT_ID, "asdf_client_secret_fdsa", "https://traffic.example.com" + TEST_CALLBACK, - AuthType::UrlEncodedBody)); + TEST_CODE_VERIFIER, AuthType::UrlEncodedBody)); EXPECT_EQ(Http::FilterHeadersStatus::StopAllIterationAndBuffer, filter_->decodeHeaders(request_headers, false)); } +/** + * Scenario: The OAuth filter receives a callback request from the OAuth server that has + * an invalid CodeVerifier cookie. + * + * Expected behavior: the filter should fail the request and return a 401 Unauthorized response. + */ +TEST_F(OAuth2Test, OAuthCallbackWithInvalidCodeVerifierCookie) { + static const std::string invalid_encrypted_code_verifier = "Fc1bBwAAAAAVzVsHAAAAABjf"; + Http::TestRequestHeaderMapImpl request_headers{ + {Http::Headers::get().Path.get(), "/_oauth?code=123&state=" + TEST_ENCODED_STATE}, + {Http::Headers::get().Cookie.get(), + "OauthNonce=" + TEST_CSRF_TOKEN + ";path=/;Max-Age=600;secure;HttpOnly"}, + {Http::Headers::get().Cookie.get(), + "CodeVerifier=" + invalid_encrypted_code_verifier + ";path=/;Max-Age=600;secure;HttpOnly"}, + {Http::Headers::get().Host.get(), "traffic.example.com"}, + {Http::Headers::get().Scheme.get(), "https"}, + {Http::Headers::get().Method.get(), Http::Headers::get().MethodValues.Get}, + }; + + // Deliberately fail the HMAC Validation check. + EXPECT_CALL(*validator_, setParams(_, _)); + EXPECT_CALL(*validator_, isValid()).WillOnce(Return(false)); + + EXPECT_CALL(decoder_callbacks_, sendLocalReply(Http::Code::Unauthorized, _, _, _, _)); + + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter_->decodeHeaders(request_headers, false)); +} + +/** + * Scenario: The OAuth filter receives a callback request from the OAuth server that lacks + * the CodeVerifier cookie. + * + * Expected behavior: the filter should fail the request and return a 401 Unauthorized response. + */ +TEST_F(OAuth2Test, OAuthCallbackWithoutCodeVerifierCookie) { + Http::TestRequestHeaderMapImpl request_headers{ + {Http::Headers::get().Path.get(), "/_oauth?code=123&state=" + TEST_ENCODED_STATE}, + {Http::Headers::get().Cookie.get(), + "OauthNonce=" + TEST_CSRF_TOKEN + ";path=/;Max-Age=600;secure;HttpOnly"}, + {Http::Headers::get().Host.get(), "traffic.example.com"}, + {Http::Headers::get().Scheme.get(), "https"}, + {Http::Headers::get().Method.get(), Http::Headers::get().MethodValues.Get}, + }; + + // Deliberately fail the HMAC Validation check. + EXPECT_CALL(*validator_, setParams(_, _)); + EXPECT_CALL(*validator_, isValid()).WillOnce(Return(false)); + + EXPECT_CALL(decoder_callbacks_, sendLocalReply(Http::Code::Unauthorized, _, _, _, _)); + + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter_->decodeHeaders(request_headers, false)); +} + /** * Scenario: The OAuth filter receives a callback request from the OAuth server that lacks a CSRF * token. This scenario simulates a CSRF attack where the original OAuth request was inserted to the @@ -965,7 +1051,7 @@ TEST_F(OAuth2Test, OAuthCallbackStartsAuthenticationNoCsrfToken) { Http::TestRequestHeaderMapImpl request_headers{ {Http::Headers::get().Path.get(), "/_oauth?code=123&state=" + state_without_csrf_token}, {Http::Headers::get().Cookie.get(), - "OauthNonce=" + TEST_STATE_CSRF_TOKEN + ";path=/;Max-Age=600;secure;HttpOnly"}, + "OauthNonce=" + TEST_CSRF_TOKEN + ";path=/;Max-Age=600;secure;HttpOnly"}, {Http::Headers::get().Host.get(), "traffic.example.com"}, {Http::Headers::get().Scheme.get(), "https"}, {Http::Headers::get().Method.get(), Http::Headers::get().MethodValues.Get}, @@ -983,20 +1069,55 @@ TEST_F(OAuth2Test, OAuthCallbackStartsAuthenticationNoCsrfToken) { /** * Scenario: The OAuth filter receives a callback request from the OAuth server that has an invalid - * CSRF token. This scenario simulates a CSRF attack where the original OAuth request was inserted - * to the user's browser by a malicious actor, and the user was tricked into clicking on the link. + * CSRF token (without Dot). This scenario simulates a CSRF attack where the original OAuth request + * was inserted to the user's browser by a malicious actor, and the user was tricked into clicking + * on the link. * * Expected behavior: the filter should fail the request and return a 401 Unauthorized response. */ -TEST_F(OAuth2Test, OAuthCallbackStartsAuthenticationInvalidCsrfTkoen) { - // {"url":"https://traffic.example.com/original_path?var1=1&var2=2","csrf_token":"0"} +TEST_F(OAuth2Test, OAuthCallbackStartsAuthenticationInvalidCsrfTokenWithoutDot) { + // {"url":"https://traffic.example.com/original_path?var1=1&var2=2","csrf_token":"${extracted}"} static const std::string state_with_invalid_csrf_token = "eyJ1cmwiOiJodHRwczovL3RyYWZmaWMuZXhhbXBsZS5jb20vb3JpZ2luYWxfcGF0aD92YXIxPTEmdmFyMj0yIiwiY3Ny" - "Zl90b2tlbiI6IjAifQ"; + "Zl90b2tlbiI6IjAwMDAwMDAwMDc1YmNkMTUifQ"; + static const std::string invalid_csrf_token_cookie = "00000000075bcd15"; Http::TestRequestHeaderMapImpl request_headers{ {Http::Headers::get().Path.get(), "/_oauth?code=123&state=" + state_with_invalid_csrf_token}, {Http::Headers::get().Cookie.get(), - "OauthNonce=" + TEST_STATE_CSRF_TOKEN + ";path=/;Max-Age=600;secure;HttpOnly"}, + "OauthNonce=" + invalid_csrf_token_cookie + ";path=/;Max-Age=600;secure;HttpOnly"}, + {Http::Headers::get().Host.get(), "traffic.example.com"}, + {Http::Headers::get().Scheme.get(), "https"}, + {Http::Headers::get().Method.get(), Http::Headers::get().MethodValues.Get}, + }; + + // Deliberately fail the HMAC Validation check. + EXPECT_CALL(*validator_, setParams(_, _)); + EXPECT_CALL(*validator_, isValid()).WillOnce(Return(false)); + + EXPECT_CALL(decoder_callbacks_, sendLocalReply(Http::Code::Unauthorized, _, _, _, _)); + + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter_->decodeHeaders(request_headers, false)); +} + +/** + * Scenario: The OAuth filter receives a callback request from the OAuth server that has an invalid + * CSRF token (hmac doesn't match). This scenario simulates a CSRF attack where the original OAuth + * request was inserted to the user's browser by a malicious actor, and the user was tricked into + * clicking on the link. + * + * Expected behavior: the filter should fail the request and return a 401 Unauthorized response. + */ +TEST_F(OAuth2Test, OAuthCallbackStartsAuthenticationInvalidCsrfTokenInvalidHmac) { + // {"url":"https://traffic.example.com/original_path?var1=1&var2=2","csrf_token":"${extracted}"} + static const std::string state_with_invalid_csrf_token = + "eyJ1cmwiOiJodHRwczovL3RyYWZmaWMuZXhhbXBsZS5jb20vb3JpZ2luYWxfcGF0aD92YXIxPTEmdmFyMj0yIiwiY3Ny" + "Zl90b2tlbiI6IjAwMDAwMDAwMDc1YmNkMTUuaW52YWxpZGhtYWMifQ"; + static const std::string invalid_csrf_token_cookie = "00000000075bcd15.invalidhmac"; + Http::TestRequestHeaderMapImpl request_headers{ + {Http::Headers::get().Path.get(), "/_oauth?code=123&state=" + state_with_invalid_csrf_token}, + {Http::Headers::get().Cookie.get(), + "OauthNonce=" + invalid_csrf_token_cookie + ";path=/;Max-Age=600;secure;HttpOnly"}, {Http::Headers::get().Host.get(), "traffic.example.com"}, {Http::Headers::get().Scheme.get(), "https"}, {Http::Headers::get().Method.get(), Http::Headers::get().MethodValues.Get}, @@ -1032,7 +1153,7 @@ TEST_F(OAuth2Test, OAuthCallbackStartsAuthenticationMalformedState) { {Http::Headers::get().Path.get(), "/_oauth?code=123&state=" + state_with_invalid_csrf_token_json}, {Http::Headers::get().Cookie.get(), - "OauthNonce=" + TEST_STATE_CSRF_TOKEN + ";path=/;Max-Age=600;secure;HttpOnly"}, + "OauthNonce=" + TEST_CSRF_TOKEN + ";path=/;Max-Age=600;secure;HttpOnly"}, {Http::Headers::get().Host.get(), "traffic.example.com"}, {Http::Headers::get().Scheme.get(), "https"}, {Http::Headers::get().Method.get(), Http::Headers::get().MethodValues.Get}, @@ -1134,22 +1255,23 @@ TEST_F(OAuth2Test, AjaxDoesNotRedirect) { // Validates the behavior of the cookie validator. TEST_F(OAuth2Test, CookieValidator) { expectValidCookies(CookieNames{"BearerToken", "OauthHMAC", "OauthExpires", "IdToken", - "RefreshToken", "OauthNonce"}, + "RefreshToken", "OauthNonce", "CodeVerifier"}, ""); } // Validates the behavior of the cookie validator with custom cookie names. TEST_F(OAuth2Test, CookieValidatorWithCustomNames) { expectValidCookies(CookieNames{"CustomBearerToken", "CustomOauthHMAC", "CustomOauthExpires", - "CustomIdToken", "CustomRefreshToken", "CustomOauthNonce"}, + "CustomIdToken", "CustomRefreshToken", "CustomOauthNonce", + "CustomCodeVerifier"}, ""); } // Validates the behavior of the cookie validator with custom cookie domain. TEST_F(OAuth2Test, CookieValidatorWithCookieDomain) { test_time_.setSystemTime(SystemTime(std::chrono::seconds(0))); - auto cookie_names = CookieNames{"BearerToken", "OauthHMAC", "OauthExpires", - "IdToken", "RefreshToken", "OauthNonce"}; + auto cookie_names = CookieNames{"BearerToken", "OauthHMAC", "OauthExpires", "IdToken", + "RefreshToken", "OauthNonce", "CodeVerifier"}; const auto expires_at_s = DateUtil::nowToSeconds(test_time_.timeSystem()) + 5; Http::TestRequestHeaderMapImpl request_headers{ @@ -1178,8 +1300,8 @@ TEST_F(OAuth2Test, CookieValidatorWithCookieDomain) { // Validates the behavior of the cookie validator when the combination of some fields could be same. TEST_F(OAuth2Test, CookieValidatorSame) { test_time_.setSystemTime(SystemTime(std::chrono::seconds(0))); - auto cookie_names = CookieNames{"BearerToken", "OauthHMAC", "OauthExpires", - "IdToken", "RefreshToken", "OauthNonce"}; + auto cookie_names = CookieNames{"BearerToken", "OauthHMAC", "OauthExpires", "IdToken", + "RefreshToken", "OauthNonce", "CodeVerifier"}; const auto expires_at_s = DateUtil::nowToSeconds(test_time_.timeSystem()) + 5; // Host name is `traffic.example.com:101` and the expire time is 5. @@ -1252,7 +1374,7 @@ TEST_F(OAuth2Test, CookieValidatorInvalidExpiresAt) { auto cookie_validator = std::make_shared( test_time_, CookieNames{"BearerToken", "OauthHMAC", "OauthExpires", "IdToken", "RefreshToken", - "OauthNonce"}, + "OauthNonce", "CodeVerifier"}, ""); cookie_validator->setParams(request_headers, "mock-secret"); @@ -1274,7 +1396,7 @@ TEST_F(OAuth2Test, CookieValidatorCanUpdateToken) { auto cookie_validator = std::make_shared( test_time_, CookieNames("BearerToken", "OauthHMAC", "OauthExpires", "IdToken", "RefreshToken", - "OauthNonce"), + "OauthNonce", "CodeVerifier"), ""); cookie_validator->setParams(request_headers, "mock-secret"); @@ -1298,7 +1420,7 @@ TEST_F(OAuth2Test, OAuthTestInvalidUrlInStateQueryParam) { "OauthHMAC=" "ZTRlMzU5N2Q4ZDIwZWE5ZTU5NTg3YTU3YTcxZTU0NDFkMzY1ZTc1NjMyODYyMj" "RlNjMxZTJmNTZkYzRmZTM0ZQ===="}, - {Http::Headers::get().Cookie.get(), "OauthNonce=" + TEST_STATE_CSRF_TOKEN}, + {Http::Headers::get().Cookie.get(), "OauthNonce=" + TEST_CSRF_TOKEN}, }; Http::TestRequestHeaderMapImpl expected_headers{ @@ -1325,7 +1447,7 @@ TEST_F(OAuth2Test, OAuthTestCallbackUrlInStateQueryParam) { // {"url":"https://traffic.example.com/_oauth","csrf_token":"${extracted}"} static const std::string state_with_callback_url = "eyJ1cmwiOiJodHRwczovL3RyYWZmaWMuZXhhbXBsZS5jb20vX29hdXRoIiwiY3NyZl90b2tlbiI6IjAwMDAwMDAwMDc1" - "YmNkMTUifQ"; + "YmNkMTUubmE2a3J1NHgxcEhnb2NTSWVVL21kdEhZbjU4R2gxYnF3ZVM0WFhvaXFWZz0ifSA"; Http::TestRequestHeaderMapImpl request_headers{ {Http::Headers::get().Host.get(), "traffic.example.com"}, @@ -1340,7 +1462,7 @@ TEST_F(OAuth2Test, OAuthTestCallbackUrlInStateQueryParam) { "OauthHMAC=" "ZTRlMzU5N2Q4ZDIwZWE5ZTU5NTg3YTU3YTcxZTU0NDFkMzY1ZTc1NjMyODYyMj" "RlNjMxZTJmNTZkYzRmZTM0ZQ===="}, - {Http::Headers::get().Cookie.get(), "OauthNonce=" + TEST_STATE_CSRF_TOKEN}, + {Http::Headers::get().Cookie.get(), "OauthNonce=" + TEST_CSRF_TOKEN}, }; Http::TestRequestHeaderMapImpl expected_response_headers{ @@ -1373,7 +1495,7 @@ TEST_F(OAuth2Test, OAuthTestCallbackUrlInStateQueryParam) { "OauthHMAC=" "ZTRlMzU5N2Q4ZDIwZWE5ZTU5NTg3YTU3YTcxZTU0NDFkMzY1ZTc1NjMyODYyMj" "RlNjMxZTJmNTZkYzRmZTM0ZQ===="}, - {Http::Headers::get().Cookie.get(), "OauthNonce=" + TEST_STATE_CSRF_TOKEN}, + {Http::Headers::get().Cookie.get(), "OauthNonce=" + TEST_CSRF_TOKEN}, {Http::CustomHeaders::get().Authorization.get(), "Bearer legit_token"}, }; @@ -1395,7 +1517,7 @@ TEST_F(OAuth2Test, OAuthTestUpdatePathAfterSuccess) { "OauthHMAC=" "ZTRlMzU5N2Q4ZDIwZWE5ZTU5NTg3YTU3YTcxZTU0NDFkMzY1ZTc1NjMyODYyMj" "RlNjMxZTJmNTZkYzRmZTM0ZQ===="}, - {Http::Headers::get().Cookie.get(), "OauthNonce=" + TEST_STATE_CSRF_TOKEN}, + {Http::Headers::get().Cookie.get(), "OauthNonce=" + TEST_CSRF_TOKEN}, }; Http::TestRequestHeaderMapImpl expected_response_headers{ @@ -1428,7 +1550,7 @@ TEST_F(OAuth2Test, OAuthTestUpdatePathAfterSuccess) { "OauthHMAC=" "ZTRlMzU5N2Q4ZDIwZWE5ZTU5NTg3YTU3YTcxZTU0NDFkMzY1ZTc1NjMyODYyMj" "RlNjMxZTJmNTZkYzRmZTM0ZQ===="}, - {Http::Headers::get().Cookie.get(), "OauthNonce=" + TEST_STATE_CSRF_TOKEN}, + {Http::Headers::get().Cookie.get(), "OauthNonce=" + TEST_CSRF_TOKEN}, {Http::CustomHeaders::get().Authorization.get(), "Bearer legit_token"}, }; @@ -1457,12 +1579,15 @@ TEST_F(OAuth2Test, OAuthTestFullFlowPostWithCookieDomain) { Http::TestResponseHeaderMapImpl first_response_headers{ {Http::Headers::get().Status.get(), "302"}, {Http::Headers::get().SetCookie.get(), - "OauthNonce=" + TEST_STATE_CSRF_TOKEN + + "OauthNonce=" + TEST_CSRF_TOKEN + ";domain=example.com;path=/;Max-Age=600;secure;HttpOnly"}, + {Http::Headers::get().SetCookie.get(), + "CodeVerifier=" + TEST_ENCRYPTED_CODE_VERIFIER + ";domain=example.com;path=/;Max-Age=600;secure;HttpOnly"}, {Http::Headers::get().Location.get(), "https://auth.example.com/oauth/" "authorize/?client_id=" + - TEST_CLIENT_ID + + TEST_CLIENT_ID + "&code_challenge=" + TEST_CODE_CHALLENGE + + "&code_challenge_method=S256" + "&redirect_uri=https%3A%2F%2Ftraffic.example.com%2F_oauth" "&response_type=code" "&scope=" + @@ -1486,7 +1611,9 @@ TEST_F(OAuth2Test, OAuthTestFullFlowPostWithCookieDomain) { // This represents the callback request from the authorization server. Http::TestRequestHeaderMapImpl second_request_headers{ {Http::Headers::get().Cookie.get(), - "OauthNonce=" + TEST_STATE_CSRF_TOKEN + + "OauthNonce=" + TEST_CSRF_TOKEN + ";domain=example.com;path=/;Max-Age=600;secure;HttpOnly"}, + {Http::Headers::get().Cookie.get(), + "CodeVerifier=" + TEST_ENCRYPTED_CODE_VERIFIER + ";domain=example.com;path=/;Max-Age=600;secure;HttpOnly"}, {Http::Headers::get().Path.get(), "/_oauth?code=123&state=" + TEST_ENCODED_STATE}, {Http::Headers::get().Host.get(), "traffic.example.com"}, @@ -1499,7 +1626,7 @@ TEST_F(OAuth2Test, OAuthTestFullFlowPostWithCookieDomain) { EXPECT_CALL(*oauth_client_, asyncGetAccessToken("123", TEST_CLIENT_ID, "asdf_client_secret_fdsa", "https://traffic.example.com" + TEST_CALLBACK, - AuthType::UrlEncodedBody)); + TEST_CODE_VERIFIER, AuthType::UrlEncodedBody)); // Invoke the callback logic. As a side effect, state_ will be populated. EXPECT_EQ(Http::FilterHeadersStatus::StopAllIterationAndBuffer, @@ -1563,11 +1690,14 @@ TEST_F(OAuth2Test, OAuthTestFullFlowPostWithSpecialCharactersForJson) { Http::TestResponseHeaderMapImpl first_response_headers{ {Http::Headers::get().Status.get(), "302"}, {Http::Headers::get().SetCookie.get(), - "OauthNonce=" + TEST_STATE_CSRF_TOKEN + ";path=/;Max-Age=600;secure;HttpOnly"}, + "OauthNonce=" + TEST_CSRF_TOKEN + ";path=/;Max-Age=600;secure;HttpOnly"}, + {Http::Headers::get().SetCookie.get(), + "CodeVerifier=" + TEST_ENCRYPTED_CODE_VERIFIER + ";path=/;Max-Age=600;secure;HttpOnly"}, {Http::Headers::get().Location.get(), "https://auth.example.com/oauth/" "authorize/?client_id=" + - TEST_CLIENT_ID + + TEST_CLIENT_ID + "&code_challenge=" + TEST_CODE_CHALLENGE + + "&code_challenge_method=S256" + "&redirect_uri=https%3A%2F%2Ftraffic.example.com%2F_oauth" "&response_type=code" "&scope=" + @@ -1593,7 +1723,9 @@ TEST_F(OAuth2Test, OAuthTestFullFlowPostWithSpecialCharactersForJson) { // This represents the callback request from the authorization server. Http::TestRequestHeaderMapImpl second_request_headers{ {Http::Headers::get().Cookie.get(), - "OauthNonce=" + TEST_STATE_CSRF_TOKEN + ";path=/;Max-Age=600;secure;HttpOnly"}, + "OauthNonce=" + TEST_CSRF_TOKEN + ";path=/;Max-Age=600;secure;HttpOnly"}, + {Http::Headers::get().Cookie.get(), + "CodeVerifier=" + TEST_ENCRYPTED_CODE_VERIFIER + ";path=/;Max-Age=600;secure;HttpOnly"}, {Http::Headers::get().Path.get(), "/_oauth?code=123&state=" + test_encoded_state_with_special_characters}, {Http::Headers::get().Host.get(), "traffic.example.com"}, @@ -1606,7 +1738,7 @@ TEST_F(OAuth2Test, OAuthTestFullFlowPostWithSpecialCharactersForJson) { EXPECT_CALL(*oauth_client_, asyncGetAccessToken("123", TEST_CLIENT_ID, "asdf_client_secret_fdsa", "https://traffic.example.com" + TEST_CALLBACK, - AuthType::UrlEncodedBody)); + TEST_CODE_VERIFIER, AuthType::UrlEncodedBody)); // Invoke the callback logic. As a side effect, state_ will be populated. EXPECT_EQ(Http::FilterHeadersStatus::StopAllIterationAndBuffer, @@ -2293,7 +2425,7 @@ TEST_F(OAuth2Test, CookieValidatorInTransition) { auto cookie_validator = std::make_shared( test_time_, CookieNames{"BearerToken", "OauthHMAC", "OauthExpires", "IdToken", "RefreshToken", - "OauthNonce"}, + "OauthNonce", "CodeVerifier"}, ""); cookie_validator->setParams(request_headers_base64only, "mock-secret"); EXPECT_TRUE(cookie_validator->hmacIsValid()); @@ -2339,11 +2471,14 @@ TEST_F(OAuth2Test, OAuthTestFullFlowWithUseRefreshToken) { Http::TestResponseHeaderMapImpl first_response_headers{ {Http::Headers::get().Status.get(), "302"}, {Http::Headers::get().SetCookie.get(), - "OauthNonce=" + TEST_STATE_CSRF_TOKEN + ";path=/;Max-Age=600;secure;HttpOnly"}, + "OauthNonce=" + TEST_CSRF_TOKEN + ";path=/;Max-Age=600;secure;HttpOnly"}, + {Http::Headers::get().SetCookie.get(), + "CodeVerifier=" + TEST_ENCRYPTED_CODE_VERIFIER + ";path=/;Max-Age=600;secure;HttpOnly"}, {Http::Headers::get().Location.get(), "https://auth.example.com/oauth/" "authorize/?client_id=" + - TEST_CLIENT_ID + + TEST_CLIENT_ID + "&code_challenge=" + TEST_CODE_CHALLENGE + + "&code_challenge_method=S256" + "&redirect_uri=https%3A%2F%2Ftraffic.example.com%2F_oauth" "&response_type=code" "&scope=" + @@ -2369,7 +2504,9 @@ TEST_F(OAuth2Test, OAuthTestFullFlowWithUseRefreshToken) { // This represents the callback request from the authorization server. Http::TestRequestHeaderMapImpl second_request_headers{ {Http::Headers::get().Cookie.get(), - "OauthNonce=" + TEST_STATE_CSRF_TOKEN + + "OauthNonce=" + TEST_CSRF_TOKEN + ";domain=example.com;path=/;Max-Age=600;secure;HttpOnly"}, + {Http::Headers::get().Cookie.get(), + "CodeVerifier=" + TEST_ENCRYPTED_CODE_VERIFIER + ";domain=example.com;path=/;Max-Age=600;secure;HttpOnly"}, {Http::Headers::get().Path.get(), "/_oauth?code=123&state=" + TEST_ENCODED_STATE}, {Http::Headers::get().Host.get(), "traffic.example.com"}, @@ -2383,7 +2520,7 @@ TEST_F(OAuth2Test, OAuthTestFullFlowWithUseRefreshToken) { EXPECT_CALL(*oauth_client_, asyncGetAccessToken("123", TEST_CLIENT_ID, "asdf_client_secret_fdsa", "https://traffic.example.com" + TEST_CALLBACK, - AuthType::UrlEncodedBody)); + TEST_CODE_VERIFIER, AuthType::UrlEncodedBody)); // Invoke the callback logic. As a side effect, state_ will be populated. EXPECT_EQ(Http::FilterHeadersStatus::StopAllIterationAndBuffer, @@ -2522,11 +2659,14 @@ TEST_F(OAuth2Test, OAuthTestRefreshAccessTokenFail) { Http::TestResponseHeaderMapImpl redirect_response_headers{ {Http::Headers::get().Status.get(), "302"}, {Http::Headers::get().SetCookie.get(), - "OauthNonce=" + TEST_STATE_CSRF_TOKEN + ";path=/;Max-Age=600;secure;HttpOnly"}, + "OauthNonce=" + TEST_CSRF_TOKEN + ";path=/;Max-Age=600;secure;HttpOnly"}, + {Http::Headers::get().SetCookie.get(), + "CodeVerifier=" + TEST_ENCRYPTED_CODE_VERIFIER + ";path=/;Max-Age=600;secure;HttpOnly"}, {Http::Headers::get().Location.get(), "https://auth.example.com/oauth/" "authorize/?client_id=" + - TEST_CLIENT_ID + + TEST_CLIENT_ID + "&code_challenge=" + TEST_CODE_CHALLENGE + + "&code_challenge_method=S256" + "&redirect_uri=https%3A%2F%2Ftraffic.example.com%2F_oauth" "&response_type=code" "&scope=" + @@ -2734,7 +2874,7 @@ TEST_F(OAuth2Test, AllCookiesStrictSameSite) { 0, false, false, false, false, false, SameSite::CookieConfig_SameSite_STRICT, SameSite::CookieConfig_SameSite_STRICT, SameSite::CookieConfig_SameSite_STRICT, SameSite::CookieConfig_SameSite_STRICT, SameSite::CookieConfig_SameSite_STRICT, - SameSite::CookieConfig_SameSite_STRICT)); + SameSite::CookieConfig_SameSite_STRICT, SameSite::CookieConfig_SameSite_STRICT)); oauthHMAC = "4TKyxPV/F7yyvr0XgJ2bkWFOc8t4IOFen1k29b84MAQ=;"; TestScopedRuntime scoped_runtime; test_time_.setSystemTime(SystemTime(std::chrono::seconds(1000))); @@ -2816,7 +2956,7 @@ TEST_F(OAuth2Test, AllCookiesLaxSameSite) { 0, false, false, false, false, false, SameSite::CookieConfig_SameSite_LAX, SameSite::CookieConfig_SameSite_LAX, SameSite::CookieConfig_SameSite_LAX, SameSite::CookieConfig_SameSite_LAX, SameSite::CookieConfig_SameSite_LAX, - SameSite::CookieConfig_SameSite_LAX)); + SameSite::CookieConfig_SameSite_LAX, SameSite::CookieConfig_SameSite_LAX)); oauthHMAC = "4TKyxPV/F7yyvr0XgJ2bkWFOc8t4IOFen1k29b84MAQ=;"; TestScopedRuntime scoped_runtime; test_time_.setSystemTime(SystemTime(std::chrono::seconds(1000))); @@ -2857,7 +2997,7 @@ TEST_F(OAuth2Test, MixedCookieSameSiteWithDisabled) { 0, false, false, false, false, false, SameSite::CookieConfig_SameSite_STRICT, SameSite::CookieConfig_SameSite_LAX, SameSite::CookieConfig_SameSite_DISABLED, SameSite::CookieConfig_SameSite_NONE, SameSite::CookieConfig_SameSite_STRICT, - SameSite::CookieConfig_SameSite_DISABLED)); + SameSite::CookieConfig_SameSite_DISABLED, SameSite::CookieConfig_SameSite_LAX)); oauthHMAC = "4TKyxPV/F7yyvr0XgJ2bkWFOc8t4IOFen1k29b84MAQ=;"; TestScopedRuntime scoped_runtime; test_time_.setSystemTime(SystemTime(std::chrono::seconds(1000))); @@ -2898,7 +3038,7 @@ TEST_F(OAuth2Test, MixedCookieSameSiteWithoutDisabled) { 0, false, false, false, false, false, SameSite::CookieConfig_SameSite_STRICT, SameSite::CookieConfig_SameSite_LAX, SameSite::CookieConfig_SameSite_NONE, SameSite::CookieConfig_SameSite_STRICT, SameSite::CookieConfig_SameSite_LAX, - SameSite::CookieConfig_SameSite_NONE)); + SameSite::CookieConfig_SameSite_NONE, SameSite::CookieConfig_SameSite_LAX)); oauthHMAC = "4TKyxPV/F7yyvr0XgJ2bkWFOc8t4IOFen1k29b84MAQ=;"; TestScopedRuntime scoped_runtime; test_time_.setSystemTime(SystemTime(std::chrono::seconds(1000))); @@ -2938,7 +3078,7 @@ TEST_F(OAuth2Test, CSRFSameSiteWithCookieDomain) { 0, false, false, true, false, false, SameSite::CookieConfig_SameSite_DISABLED, SameSite::CookieConfig_SameSite_DISABLED, SameSite::CookieConfig_SameSite_DISABLED, SameSite::CookieConfig_SameSite_DISABLED, SameSite::CookieConfig_SameSite_DISABLED, - SameSite::CookieConfig_SameSite_STRICT)); + SameSite::CookieConfig_SameSite_STRICT, SameSite::CookieConfig_SameSite_LAX)); // First construct the initial request to the oauth filter with URI parameters. Http::TestRequestHeaderMapImpl first_request_headers{ {Http::Headers::get().Path.get(), "/original_path?var1=1&var2=2"}, @@ -2951,12 +3091,16 @@ TEST_F(OAuth2Test, CSRFSameSiteWithCookieDomain) { Http::TestResponseHeaderMapImpl first_response_headers{ {Http::Headers::get().Status.get(), "302"}, {Http::Headers::get().SetCookie.get(), - "OauthNonce=" + TEST_STATE_CSRF_TOKEN + + "OauthNonce=" + TEST_CSRF_TOKEN + ";domain=example.com;path=/;Max-Age=600;secure;HttpOnly;SameSite=Strict"}, + {Http::Headers::get().SetCookie.get(), + "CodeVerifier=" + TEST_ENCRYPTED_CODE_VERIFIER + + ";domain=example.com;path=/;Max-Age=600;secure;HttpOnly"}, {Http::Headers::get().Location.get(), "https://auth.example.com/oauth/" "authorize/?client_id=" + - TEST_CLIENT_ID + + TEST_CLIENT_ID + "&code_challenge=" + TEST_CODE_CHALLENGE + + "&code_challenge_method=S256" + "&redirect_uri=https%3A%2F%2Ftraffic.example.com%2F_oauth" "&response_type=code" "&scope=" + diff --git a/test/extensions/filters/http/oauth2/oauth_integration_test.cc b/test/extensions/filters/http/oauth2/oauth_integration_test.cc index 15551a1297262..7be3c2c1aaba1 100644 --- a/test/extensions/filters/http/oauth2/oauth_integration_test.cc +++ b/test/extensions/filters/http/oauth2/oauth_integration_test.cc @@ -27,6 +27,10 @@ static const std::string TEST_STATE_CSRF_TOKEN_1 = static const std::string TEST_ENCODED_STATE_1 = "eyJ1cmwiOiJodHRwOi8vdHJhZmZpYy5leGFtcGxlLmNvbS9ub3QvX29hdXRoIiwiY3NyZl90b2tlbiI6IjhjMThiOGZjZj" "U3NWI1OTMuWnBrWE1ETkZpaW5rTDg3QW9TRE9OS3VsQnJ1T3BhSWlTQWQ3Q05rZ09Fbz0ifQ"; +static const std::string TEST_ENCRYPTED_CODE_VERIFIER = + "Fc1bBwAAAAAVzVsHAAAAACcWO_WnprqLTdaCdFE7rj83_Jej1OihEIfOcQJFRCQZirutZ-XL7LK2G2KgRnVCCA"; +static const std::string TEST_ENCRYPTED_CODE_VERIFIER_1 = + "Fc1bBwAAAAAVzVsHAAAAANRgXgBre6UErcWdPGZOl-o0px-SribGBqMNhaB6Smp-pjDSB20RXanapU6gVN4E1A"; class OauthIntegrationTest : public HttpIntegrationTest, public Grpc::GrpcClientIntegrationParamTest { public: @@ -333,7 +337,8 @@ name: oauth } void doAuthenticationFlow(absl::string_view token_secret, absl::string_view hmac_secret, - absl::string_view csrf_token, absl::string_view state) { + absl::string_view csrf_token, absl::string_view state, + absl::string_view code_verifier) { codec_client_ = makeHttpConnection(lookupPort("http")); Http::TestRequestHeaderMapImpl headers{ @@ -343,7 +348,8 @@ name: oauth {"x-forwarded-proto", "http"}, {":authority", "authority"}, {"authority", "Bearer token"}, - {"cookie", absl::StrCat(default_cookie_names_.oauth_nonce_, "=", csrf_token)}}; + {"cookie", absl::StrCat(default_cookie_names_.oauth_nonce_, "=", csrf_token)}, + {"cookie", absl::StrCat(default_cookie_names_.code_verifier_, "=", code_verifier)}}; auto encoder_decoder = codec_client_->startRequest(headers); request_encoder_ = &encoder_decoder.first; @@ -436,8 +442,8 @@ name: oauth cleanup(); } - const CookieNames default_cookie_names_{"BearerToken", "OauthHMAC", "OauthExpires", - "IdToken", "RefreshToken", "OauthNonce"}; + const CookieNames default_cookie_names_{"BearerToken", "OauthHMAC", "OauthExpires", "IdToken", + "RefreshToken", "OauthNonce", "CodeVerifier"}; envoy::config::listener::v3::Listener listener_config_; std::string listener_name_{"http"}; FakeHttpConnectionPtr lds_connection_; @@ -485,7 +491,8 @@ TEST_P(OauthIntegrationTest, AuthenticationFlow) { initialize(); // 1. Do one authentication flow. - doAuthenticationFlow("token_secret", "hmac_secret", TEST_STATE_CSRF_TOKEN, TEST_ENCODED_STATE); + doAuthenticationFlow("token_secret", "hmac_secret", TEST_STATE_CSRF_TOKEN, TEST_ENCODED_STATE, + TEST_ENCRYPTED_CODE_VERIFIER); // 2. Reload secrets. EXPECT_EQ(test_server_->counter("sds.token.update_success")->value(), 1); @@ -498,7 +505,7 @@ TEST_P(OauthIntegrationTest, AuthenticationFlow) { test_server_->waitForCounterEq("sds.hmac.update_success", 2, std::chrono::milliseconds(5000)); // 3. Do another one authentication flow. doAuthenticationFlow("token_secret_1", "hmac_secret_1", TEST_STATE_CSRF_TOKEN_1, - TEST_ENCODED_STATE_1); + TEST_ENCODED_STATE_1, TEST_ENCRYPTED_CODE_VERIFIER_1); } TEST_P(OauthIntegrationTest, RefreshTokenFlow) { @@ -596,7 +603,8 @@ TEST_P(OauthIntegrationTest, LoadListenerAfterServerIsInitialized) { test_server_->waitForCounterGe("listener_manager.lds.update_success", 2); test_server_->waitForGaugeEq("listener_manager.total_listeners_warming", 0); - doAuthenticationFlow("token_secret", "hmac_secret", TEST_STATE_CSRF_TOKEN, TEST_ENCODED_STATE); + doAuthenticationFlow("token_secret", "hmac_secret", TEST_STATE_CSRF_TOKEN, TEST_ENCODED_STATE, + TEST_ENCRYPTED_CODE_VERIFIER); if (lds_connection_ != nullptr) { AssertionResult result = lds_connection_->close(); RELEASE_ASSERT(result, result.message()); @@ -672,7 +680,8 @@ TEST_P(OauthIntegrationTestWithBasicAuth, AuthenticationFlow) { sendLdsResponse({MessageUtil::getYamlStringFromMessage(listener_config_)}, "initial"); }; initialize(); - doAuthenticationFlow("token_secret", "hmac_secret", TEST_STATE_CSRF_TOKEN, TEST_ENCODED_STATE); + doAuthenticationFlow("token_secret", "hmac_secret", TEST_STATE_CSRF_TOKEN, TEST_ENCODED_STATE, + TEST_ENCRYPTED_CODE_VERIFIER); } class OauthUseRefreshTokenDisabled : public OauthIntegrationTest { @@ -736,7 +745,8 @@ TEST_P(OauthUseRefreshTokenDisabled, FailRefreshTokenFlow) { initialize(); // 1. Do one authentication flow. - doAuthenticationFlow("token_secret", "hmac_secret", TEST_STATE_CSRF_TOKEN, TEST_ENCODED_STATE); + doAuthenticationFlow("token_secret", "hmac_secret", TEST_STATE_CSRF_TOKEN, TEST_ENCODED_STATE, + TEST_ENCRYPTED_CODE_VERIFIER); // 2. Reload secrets. EXPECT_EQ(test_server_->counter("sds.token.update_success")->value(), 1); diff --git a/test/extensions/filters/http/oauth2/oauth_test.cc b/test/extensions/filters/http/oauth2/oauth_test.cc index 9c5fa65fce10f..2de33bb063088 100644 --- a/test/extensions/filters/http/oauth2/oauth_test.cc +++ b/test/extensions/filters/http/oauth2/oauth_test.cc @@ -102,7 +102,7 @@ TEST_F(OAuth2ClientTest, RequestAccessTokenSuccess) { })); client_->setCallbacks(*mock_callbacks_); - client_->asyncGetAccessToken("a", "b", "c", "d"); + client_->asyncGetAccessToken("a", "b", "c", "d", "e"); EXPECT_EQ(1, callbacks_.size()); EXPECT_CALL(*mock_callbacks_, onGetAccessTokenSuccess("golden ticket", _, _, 1000s)); Http::MockAsyncClientRequest request(&cm_.thread_local_cluster_.async_client_); @@ -133,7 +133,7 @@ TEST_F(OAuth2ClientTest, RequestAccessTokenMissingExpiresIn) { })); client_->setCallbacks(*mock_callbacks_); - client_->asyncGetAccessToken("a", "b", "c", "d"); + client_->asyncGetAccessToken("a", "b", "c", "d", "e"); EXPECT_EQ(1, callbacks_.size()); EXPECT_CALL(*mock_callbacks_, sendUnauthorizedResponse()); Http::MockAsyncClientRequest request(&cm_.thread_local_cluster_.async_client_); @@ -169,7 +169,7 @@ TEST_F(OAuth2ClientTest, RequestAccessTokenDefaultExpiresIn) { uri.mutable_timeout()->set_seconds(1); client_ = std::make_shared(cm_, uri, absl::nullopt, 2000s); client_->setCallbacks(*mock_callbacks_); - client_->asyncGetAccessToken("a", "b", "c", "d"); + client_->asyncGetAccessToken("a", "b", "c", "d", "e"); EXPECT_EQ(1, callbacks_.size()); EXPECT_CALL(*mock_callbacks_, onGetAccessTokenSuccess("golden ticket", _, _, 2000s)); Http::MockAsyncClientRequest request(&cm_.thread_local_cluster_.async_client_); @@ -200,7 +200,7 @@ TEST_F(OAuth2ClientTest, RequestAccessTokenIncompleteResponse) { })); client_->setCallbacks(*mock_callbacks_); - client_->asyncGetAccessToken("a", "b", "c", "d"); + client_->asyncGetAccessToken("a", "b", "c", "d", "e"); EXPECT_EQ(1, callbacks_.size()); EXPECT_CALL(*mock_callbacks_, sendUnauthorizedResponse()); Http::MockAsyncClientRequest request(&cm_.thread_local_cluster_.async_client_); @@ -225,7 +225,7 @@ TEST_F(OAuth2ClientTest, RequestAccessTokenErrorResponse) { })); client_->setCallbacks(*mock_callbacks_); - client_->asyncGetAccessToken("a", "b", "c", "d"); + client_->asyncGetAccessToken("a", "b", "c", "d", "e"); EXPECT_EQ(1, callbacks_.size()); EXPECT_CALL(*mock_callbacks_, sendUnauthorizedResponse()); Http::MockAsyncClientRequest request(&cm_.thread_local_cluster_.async_client_); @@ -256,7 +256,7 @@ TEST_F(OAuth2ClientTest, RequestAccessTokenInvalidResponse) { })); client_->setCallbacks(*mock_callbacks_); - client_->asyncGetAccessToken("a", "b", "c", "d"); + client_->asyncGetAccessToken("a", "b", "c", "d", "e"); EXPECT_EQ(1, callbacks_.size()); EXPECT_CALL(*mock_callbacks_, sendUnauthorizedResponse()); Http::MockAsyncClientRequest request(&cm_.thread_local_cluster_.async_client_); @@ -274,7 +274,7 @@ TEST_F(OAuth2ClientTest, RequestAccessTokenNetworkError) { })); client_->setCallbacks(*mock_callbacks_); - client_->asyncGetAccessToken("a", "b", "c", "d"); + client_->asyncGetAccessToken("a", "b", "c", "d", "e"); EXPECT_EQ(1, callbacks_.size()); EXPECT_CALL(*mock_callbacks_, sendUnauthorizedResponse()); @@ -302,7 +302,7 @@ TEST_F(OAuth2ClientTest, RequestAccessTokenUnhealthyUpstream) { client_->setCallbacks(*mock_callbacks_); EXPECT_CALL(*mock_callbacks_, sendUnauthorizedResponse()); - client_->asyncGetAccessToken("a", "b", "c", "d"); + client_->asyncGetAccessToken("a", "b", "c", "d", "e"); } TEST_F(OAuth2ClientTest, RequestRefreshAccessTokenSuccess) { @@ -479,7 +479,7 @@ TEST_F(OAuth2ClientTest, NoCluster) { ON_CALL(cm_, getThreadLocalCluster("auth")).WillByDefault(Return(nullptr)); client_->setCallbacks(*mock_callbacks_); EXPECT_CALL(*mock_callbacks_, sendUnauthorizedResponse()); - client_->asyncGetAccessToken("a", "b", "c", "d"); + client_->asyncGetAccessToken("a", "b", "c", "d", "e"); EXPECT_EQ(0, callbacks_.size()); } @@ -523,7 +523,7 @@ TEST_F(OAuth2ClientTest, RequestAccessTokenRetryPolicy) { })); client_->setCallbacks(*mock_callbacks_); - client_->asyncGetAccessToken("a", "b", "c", "d"); + client_->asyncGetAccessToken("a", "b", "c", "d", "e"); } } // namespace Oauth2 diff --git a/tools/spelling/spelling_dictionary.txt b/tools/spelling/spelling_dictionary.txt index 023f1a5681261..a203fd5cf99bb 100644 --- a/tools/spelling/spelling_dictionary.txt +++ b/tools/spelling/spelling_dictionary.txt @@ -367,6 +367,7 @@ PEM PERF PGV PID +PKCE PKCS12 PKTINFO PNG @@ -645,6 +646,7 @@ chunked ci ciphersuite ciphersuites +ciphertext circllhist clientcert cloneable