From 92c59bd8a75d5f57571d0894e9a2243f909d52f0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 17 Jan 2026 06:57:53 +0000 Subject: [PATCH 1/5] Initial plan From 46289ce44b98b623517a0ed55dd0aac713b4fa31 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 17 Jan 2026 07:18:03 +0000 Subject: [PATCH 2/5] Migrate jwt_verify_lib code to Envoy and update consumers Co-authored-by: wbpcode <12389633+wbpcode@users.noreply.github.com> --- bazel/jwt_verify_lib.patch | 186 --- bazel/repositories.bzl | 8 - bazel/repository_locations.bzl | 15 - source/extensions/filters/http/common/BUILD | 2 +- .../filters/http/common/jwks_fetcher.cc | 8 +- .../filters/http/common/jwks_fetcher.h | 7 +- .../extensions/filters/http/common/jwt/BUILD | 49 + .../filters/http/common/jwt/check_audience.cc | 84 ++ .../filters/http/common/jwt/check_audience.h | 59 + .../filters/http/common/jwt/jwks.cc | 557 +++++++++ .../extensions/filters/http/common/jwt/jwks.h | 88 ++ .../extensions/filters/http/common/jwt/jwt.cc | 166 +++ .../extensions/filters/http/common/jwt/jwt.h | 113 ++ .../http/common/jwt/simple_lru_cache.h | 50 + .../http/common/jwt/simple_lru_cache_inl.h | 1099 +++++++++++++++++ .../filters/http/common/jwt/status.cc | 190 +++ .../filters/http/common/jwt/status.h | 266 ++++ .../filters/http/common/jwt/struct_utils.cc | 130 ++ .../filters/http/common/jwt/struct_utils.h | 62 + .../filters/http/common/jwt/verify.cc | 311 +++++ .../filters/http/common/jwt/verify.h | 97 ++ .../extensions/filters/http/gcp_authn/BUILD | 4 +- .../http/gcp_authn/gcp_authn_filter.cc | 6 +- .../filters/http/gcp_authn/token_cache.h | 18 +- .../extensions/filters/http/jwt_authn/BUILD | 10 +- .../filters/http/jwt_authn/authenticator.cc | 34 +- .../filters/http/jwt_authn/authenticator.h | 13 +- .../filters/http/jwt_authn/filter.cc | 12 +- .../filters/http/jwt_authn/filter.h | 2 +- .../filters/http/jwt_authn/filter_config.h | 2 +- .../filters/http/jwt_authn/filter_factory.cc | 9 +- .../http/jwt_authn/jwks_async_fetcher.cc | 2 +- .../http/jwt_authn/jwks_async_fetcher.h | 7 +- .../filters/http/jwt_authn/jwks_cache.cc | 16 +- .../filters/http/jwt_authn/jwks_cache.h | 13 +- .../filters/http/jwt_authn/jwt_cache.cc | 21 +- .../filters/http/jwt_authn/jwt_cache.h | 13 +- .../filters/http/jwt_authn/verifier.cc | 8 +- .../filters/http/jwt_authn/verifier.h | 2 +- source/extensions/filters/http/oauth2/BUILD | 2 +- .../extensions/filters/http/oauth2/filter.cc | 15 +- 41 files changed, 3440 insertions(+), 316 deletions(-) delete mode 100644 bazel/jwt_verify_lib.patch create mode 100644 source/extensions/filters/http/common/jwt/BUILD create mode 100644 source/extensions/filters/http/common/jwt/check_audience.cc create mode 100644 source/extensions/filters/http/common/jwt/check_audience.h create mode 100644 source/extensions/filters/http/common/jwt/jwks.cc create mode 100644 source/extensions/filters/http/common/jwt/jwks.h create mode 100644 source/extensions/filters/http/common/jwt/jwt.cc create mode 100644 source/extensions/filters/http/common/jwt/jwt.h create mode 100644 source/extensions/filters/http/common/jwt/simple_lru_cache.h create mode 100644 source/extensions/filters/http/common/jwt/simple_lru_cache_inl.h create mode 100644 source/extensions/filters/http/common/jwt/status.cc create mode 100644 source/extensions/filters/http/common/jwt/status.h create mode 100644 source/extensions/filters/http/common/jwt/struct_utils.cc create mode 100644 source/extensions/filters/http/common/jwt/struct_utils.h create mode 100644 source/extensions/filters/http/common/jwt/verify.cc create mode 100644 source/extensions/filters/http/common/jwt/verify.h diff --git a/bazel/jwt_verify_lib.patch b/bazel/jwt_verify_lib.patch deleted file mode 100644 index b16db530d6fb4..0000000000000 --- a/bazel/jwt_verify_lib.patch +++ /dev/null @@ -1,186 +0,0 @@ -diff --git a/BUILD b/BUILD -index 1234567..abcdefg 100644 ---- a/BUILD -+++ b/BUILD -@@ -22,11 +22,11 @@ cc_library( - "jwt_verify_lib/verify.h", - ], - deps = [ -- "//external:abseil_flat_hash_set", -- "//external:abseil_strings", -- "//external:abseil_time", -- "//external:protobuf", -- "//external:ssl", -+ "@com_google_absl//absl/container:flat_hash_set", -+ "@com_google_absl//absl/strings", -+ "@com_google_absl//absl/time", -+ "@com_google_protobuf//:protobuf", -+ "@envoy//bazel:boringssl", - ], - ) - -@@ -36,7 +36,7 @@ cc_library( - "simple_lru_cache/simple_lru_cache_inl.h", - ], - deps = [ -- "//external:abseil_flat_hash_map", -+ "@com_google_absl//absl/container:flat_hash_map", - ], - ) - -@@ -52,7 +52,7 @@ cc_test( - linkstatic = 1, - deps = [ - ":jwt_verify_lib", -- "//external:googletest_main", -+ "@com_google_googletest//:gtest_main", - ], - ) - -@@ -68,7 +68,7 @@ cc_test( - linkstatic = 1, - deps = [ - ":jwt_verify_lib", -- "//external:googletest_main", -+ "@com_google_googletest//:gtest_main", - ], - ) - -@@ -85,7 +85,7 @@ cc_test( - linkstatic = 1, - deps = [ - ":jwt_verify_lib", -- "//external:googletest_main", -+ "@com_google_googletest//:gtest_main", - ], - ) - -@@ -101,7 +101,7 @@ cc_test( - linkstatic = 1, - deps = [ - ":simple_lru_cache_lib", -- "//external:googletest_main", -+ "@com_google_googletest//:gtest_main", - ], - ) - -@@ -118,7 +118,7 @@ cc_test( - linkstatic = 1, - deps = [ - ":jwt_verify_lib", -- "//external:googletest_main", -+ "@com_google_googletest//:gtest_main", - ], - ) - -@@ -135,7 +135,7 @@ cc_test( - linkstatic = 1, - deps = [ - ":jwt_verify_lib", -- "//external:googletest_main", -+ "@com_google_googletest//:gtest_main", - ], - ) - -@@ -152,7 +152,7 @@ cc_test( - linkstatic = 1, - deps = [ - ":jwt_verify_lib", -- "//external:googletest_main", -+ "@com_google_googletest//:gtest_main", - ], - ) - -@@ -169,7 +169,7 @@ cc_test( - linkstatic = 1, - deps = [ - ":jwt_verify_lib", -- "//external:googletest_main", -+ "@com_google_googletest//:gtest_main", - ], - ) - -@@ -186,7 +186,7 @@ cc_test( - linkstatic = 1, - deps = [ - ":jwt_verify_lib", -- "//external:googletest_main", -+ "@com_google_googletest//:gtest_main", - ], - ) - -@@ -203,7 +203,7 @@ cc_test( - linkstatic = 1, - deps = [ - ":jwt_verify_lib", -- "//external:googletest_main", -+ "@com_google_googletest//:gtest_main", - ], - ) - -@@ -220,7 +220,7 @@ cc_test( - linkstatic = 1, - deps = [ - ":jwt_verify_lib", -- "//external:googletest_main", -+ "@com_google_googletest//:gtest_main", - ], - ) - -@@ -237,7 +237,7 @@ cc_test( - linkstatic = 1, - deps = [ - ":jwt_verify_lib", -- "//external:googletest_main", -+ "@com_google_googletest//:gtest_main", - ], - ) - -@@ -254,7 +254,7 @@ cc_test( - linkstatic = 1, - deps = [ - ":jwt_verify_lib", -- "//external:googletest_main", -+ "@com_google_googletest//:gtest_main", - ], - ) - -@@ -271,6 +271,6 @@ cc_test( - linkstatic = 1, - deps = [ - ":jwt_verify_lib", -- "//external:googletest_main", -+ "@com_google_googletest//:gtest_main", - ], - ) -diff --git a/test/fuzz/BUILD b/test/fuzz/BUILD -index 28247dd..e35142b 100644 ---- a/test/fuzz/BUILD -+++ b/test/fuzz/BUILD -@@ -1,5 +1,6 @@ - load("@rules_fuzzing//fuzzing:cc_defs.bzl", "cc_fuzz_test") --# load cc_proto_library -+load("@com_google_protobuf//bazel:cc_proto_library.bzl", "cc_proto_library") -+load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") - - licenses(["notice"]) - -@@ -27,7 +27,7 @@ cc_fuzz_test( - deps = [ - ":jwt_verify_lib_fuzz_input_cc_proto", - "//:jwt_verify_lib", -- "//external:libprotobuf_mutator", -+ "@com_github_google_libprotobuf_mutator//:libprotobuf_mutator", - ], - ) - -@@ -41,7 +41,7 @@ cc_test( - deps = [ - ":jwt_verify_lib_fuzz_input_cc_proto", - "//:jwt_verify_lib", -- "//external:abseil_strings", -- "//external:googletest_main", -+ "@com_google_absl//absl/strings", -+ "@com_google_googletest//:gtest_main", - ], - ) diff --git a/bazel/repositories.bzl b/bazel/repositories.bzl index d29b52b45b79b..3e68fddaa4060 100644 --- a/bazel/repositories.bzl +++ b/bazel/repositories.bzl @@ -153,7 +153,6 @@ def envoy_dependencies(skip_targets = []): _com_github_fmtlib_fmt() _com_github_gabime_spdlog() _com_github_google_benchmark() - _com_github_google_jwt_verify() _com_github_google_libprotobuf_mutator() _com_github_google_libsxg() _com_github_google_tcmalloc() @@ -800,13 +799,6 @@ def _emsdk(): patches = ["@envoy//bazel:emsdk.patch"], ) -def _com_github_google_jwt_verify(): - external_http_archive( - "com_github_google_jwt_verify", - patches = ["@envoy//bazel:jwt_verify_lib.patch"], - patch_args = ["-p1"], - ) - def _com_github_luajit_luajit(): external_http_archive( name = "com_github_luajit_luajit", diff --git a/bazel/repository_locations.bzl b/bazel/repository_locations.bzl index d4ff9375156a3..21c6d22b08262 100644 --- a/bazel/repository_locations.bzl +++ b/bazel/repository_locations.bzl @@ -891,21 +891,6 @@ REPOSITORY_LOCATIONS_SPEC = dict( license = "Boost", license_url = "https://github.com/msgpack/msgpack-c/blob/cpp-{version}/LICENSE_1_0.txt", ), - com_github_google_jwt_verify = dict( - project_name = "jwt_verify_lib", - project_desc = "JWT verification library for C++", - project_url = "https://github.com/google/jwt_verify_lib", - version = "b59e8075d4a4f975ba6f109e1916d6e60aeb5613", - sha256 = "637e4983506c4f26bbe2808ae4e1944e46cbb2277d34ff0b8a3b72bdac3c4b91", - strip_prefix = "jwt_verify_lib-{version}", - urls = ["https://github.com/google/jwt_verify_lib/archive/{version}.tar.gz"], - use_category = ["dataplane_ext"], - extensions = ["envoy.filters.http.jwt_authn", "envoy.filters.http.gcp_authn", "envoy.filters.http.oauth2"], - release_date = "2023-05-17", - cpe = "N/A", - license = "Apache-2.0", - license_url = "https://github.com/google/jwt_verify_lib/blob/{version}/LICENSE", - ), com_github_alibaba_hessian2_codec = dict( project_name = "hessian2-codec", project_desc = "hessian2-codec is a C++ library for hessian2 codec", diff --git a/source/extensions/filters/http/common/BUILD b/source/extensions/filters/http/common/BUILD index 73d4be20fbe93..ee487150183f7 100644 --- a/source/extensions/filters/http/common/BUILD +++ b/source/extensions/filters/http/common/BUILD @@ -34,7 +34,7 @@ envoy_cc_library( deps = [ "//envoy/upstream:cluster_manager_interface", "//source/common/http:utility_lib", - "@com_github_google_jwt_verify//:jwt_verify_lib", + "//source/extensions/filters/http/common/jwt:jwt_verify_lib", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", "@envoy_api//envoy/extensions/filters/http/jwt_authn/v3:pkg_cc_proto", ], diff --git a/source/extensions/filters/http/common/jwks_fetcher.cc b/source/extensions/filters/http/common/jwks_fetcher.cc index f50f27d037df2..42e7b305f02a3 100644 --- a/source/extensions/filters/http/common/jwks_fetcher.cc +++ b/source/extensions/filters/http/common/jwks_fetcher.cc @@ -7,8 +7,7 @@ #include "source/common/http/headers.h" #include "source/common/http/utility.h" #include "source/common/protobuf/utility.h" - -#include "jwt_verify_lib/status.h" +#include "source/extensions/filters/http/common/jwt/status.h" using envoy::extensions::filters::http::jwt_authn::v3::RemoteJwks; @@ -85,9 +84,8 @@ class JwksFetcherImpl : public JwksFetcher, ENVOY_LOG(debug, "{}: fetch pubkey [uri = {}]: success", __func__, uri); if (response->body().length() != 0) { const auto body = response->bodyAsString(); - auto jwks = - google::jwt_verify::Jwks::createFrom(body, google::jwt_verify::Jwks::Type::JWKS); - if (jwks->getStatus() == google::jwt_verify::Status::Ok) { + auto jwks = JwtVerify::Jwks::createFrom(body, JwtVerify::Jwks::Type::JWKS); + if (jwks->getStatus() == JwtVerify::Status::Ok) { ENVOY_LOG(debug, "{}: fetch pubkey [uri = {}]: succeeded", __func__, uri); receiver_->onJwksSuccess(std::move(jwks)); } else { diff --git a/source/extensions/filters/http/common/jwks_fetcher.h b/source/extensions/filters/http/common/jwks_fetcher.h index 456235688380c..349a905f688cd 100644 --- a/source/extensions/filters/http/common/jwks_fetcher.h +++ b/source/extensions/filters/http/common/jwks_fetcher.h @@ -5,13 +5,16 @@ #include "envoy/extensions/filters/http/jwt_authn/v3/config.pb.h" #include "envoy/upstream/cluster_manager.h" -#include "jwt_verify_lib/jwks.h" +#include "source/extensions/filters/http/common/jwt/jwks.h" namespace Envoy { namespace Extensions { namespace HttpFilters { namespace Common { +// Namespace alias for backward compatibility. +namespace JwtVerify = Common::JwtVerify; + class JwksFetcher; using JwksFetcherPtr = std::unique_ptr; /** @@ -37,7 +40,7 @@ class JwksFetcher { * of the returned JWKS object. * @param jwks the JWKS object retrieved. */ - virtual void onJwksSuccess(google::jwt_verify::JwksPtr&& jwks) PURE; + virtual void onJwksSuccess(JwtVerify::JwksPtr&& jwks) PURE; /* * Retrieval error callback. * * @param reason the failure reason. diff --git a/source/extensions/filters/http/common/jwt/BUILD b/source/extensions/filters/http/common/jwt/BUILD new file mode 100644 index 0000000000000..35a93fbcf6744 --- /dev/null +++ b/source/extensions/filters/http/common/jwt/BUILD @@ -0,0 +1,49 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +envoy_cc_library( + name = "jwt_verify_lib", + srcs = [ + "check_audience.cc", + "jwks.cc", + "jwt.cc", + "status.cc", + "struct_utils.cc", + "verify.cc", + ], + hdrs = [ + "check_audience.h", + "jwks.h", + "jwt.h", + "status.h", + "struct_utils.h", + "verify.h", + ], + visibility = ["//visibility:public"], + deps = [ + "//bazel:boringssl", + "@com_google_absl//absl/container:flat_hash_set", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/time", + "@com_google_protobuf//:protobuf", + ], +) + +envoy_cc_library( + name = "simple_lru_cache_lib", + hdrs = [ + "simple_lru_cache.h", + "simple_lru_cache_inl.h", + ], + visibility = ["//visibility:public"], + deps = [ + "@com_google_absl//absl/container:flat_hash_map", + ], +) diff --git a/source/extensions/filters/http/common/jwt/check_audience.cc b/source/extensions/filters/http/common/jwt/check_audience.cc new file mode 100644 index 0000000000000..751f3ca19a396 --- /dev/null +++ b/source/extensions/filters/http/common/jwt/check_audience.cc @@ -0,0 +1,84 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "source/extensions/filters/http/common/jwt/check_audience.h" + +#include "absl/strings/match.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Common { +namespace JwtVerify { +namespace { + +// HTTP Protocol scheme prefix in JWT aud claim. +constexpr absl::string_view HTTPSchemePrefix("http://"); + +// HTTPS Protocol scheme prefix in JWT aud claim. +constexpr absl::string_view HTTPSSchemePrefix("https://"); + +std::string sanitizeAudience(const std::string& aud) { + if (aud.empty()) { + return aud; + } + + size_t beg_pos = 0; + bool sanitized = false; + // Point beg to first character after protocol scheme prefix in audience. + if (absl::StartsWith(aud, HTTPSchemePrefix)) { + beg_pos = HTTPSchemePrefix.size(); + sanitized = true; + } else if (absl::StartsWith(aud, HTTPSSchemePrefix)) { + beg_pos = HTTPSSchemePrefix.size(); + sanitized = true; + } + + // Point end to trailing slash in aud. + size_t end_pos = aud.length(); + if (aud[end_pos - 1] == '/') { + --end_pos; + sanitized = true; + } + if (sanitized) { + return aud.substr(beg_pos, end_pos - beg_pos); + } + return aud; +} + +} // namespace + +CheckAudience::CheckAudience(const std::vector& config_audiences) { + for (const auto& aud : config_audiences) { + config_audiences_.insert(sanitizeAudience(aud)); + } +} + +bool CheckAudience::areAudiencesAllowed(const std::vector& jwt_audiences) const { + if (config_audiences_.empty()) { + return true; + } + for (const auto& aud : jwt_audiences) { + if (config_audiences_.find(sanitizeAudience(aud)) != config_audiences_.end()) { + return true; + } + } + return false; +} + +} // namespace JwtVerify +} // namespace Common +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/common/jwt/check_audience.h b/source/extensions/filters/http/common/jwt/check_audience.h new file mode 100644 index 0000000000000..eb6edca4e5288 --- /dev/null +++ b/source/extensions/filters/http/common/jwt/check_audience.h @@ -0,0 +1,59 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include +#include + +#include "source/extensions/filters/http/common/jwt/status.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Common { +namespace JwtVerify { + +/** + * RFC for JWT `aud `_ only + * specifies case sensitive comparison. But experiences showed that users + * easily add wrong scheme and tailing slash to cause mis-match. + * In this implemeation, scheme portion of URI and tailing slash is removed + * before comparison. + */ +class CheckAudience { +public: + // Construct the object with a list audiences from config. + CheckAudience(const std::vector& config_audiences); + + // Check any of jwt_audiences is matched with one of configurated ones. + bool areAudiencesAllowed(const std::vector& jwt_audiences) const; + + // check if config audiences is empty + bool empty() const { return config_audiences_.empty(); } + +private: + // configured audiences; + std::set config_audiences_; +}; + +using CheckAudiencePtr = std::unique_ptr; + +} // namespace JwtVerify +} // namespace Common +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/common/jwt/jwks.cc b/source/extensions/filters/http/common/jwt/jwks.cc new file mode 100644 index 0000000000000..b8b432f90caf7 --- /dev/null +++ b/source/extensions/filters/http/common/jwt/jwks.cc @@ -0,0 +1,557 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "source/extensions/filters/http/common/jwt/jwks.h" + +#include +#include + +#include "source/extensions/filters/http/common/jwt/struct_utils.h" + +#include "absl/strings/escaping.h" +#include "absl/strings/match.h" +#include "google/protobuf/struct.pb.h" +#include "google/protobuf/util/json_util.h" +#include "openssl/bio.h" +#include "openssl/bn.h" +#include "openssl/curve25519.h" +#include "openssl/ecdsa.h" +#include "openssl/evp.h" +#include "openssl/rsa.h" +#include "openssl/sha.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Common { +namespace JwtVerify { + +namespace { + +// The x509 certificate prefix string +const char kX509CertPrefix[] = "-----BEGIN CERTIFICATE-----\n"; +// The x509 certificate suffix string +const char kX509CertSuffix[] = "\n-----END CERTIFICATE-----\n"; + +// A convinence inline cast function. +inline const uint8_t* castToUChar(const std::string& str) { + return reinterpret_cast(str.c_str()); +} + +/** Class to create key object from string of public key, formatted in PEM + * or JWKs. + * If it fails, status_ holds the failure reason. + * + * Usage example: + * KeyGetter e; + * bssl::UniquePtr pkey = e.createEcKeyFromJwkEC(...); + */ +class KeyGetter : public WithStatus { +public: + bssl::UniquePtr createEvpPkeyFromPem(const std::string& pkey_pem) { + bssl::UniquePtr buf(BIO_new_mem_buf(pkey_pem.data(), pkey_pem.size())); + if (buf == nullptr) { + updateStatus(Status::JwksBioAllocError); + return nullptr; + } + bssl::UniquePtr key(PEM_read_bio_PUBKEY(buf.get(), nullptr, nullptr, nullptr)); + if (key == nullptr) { + updateStatus(Status::JwksPemBadBase64); + return nullptr; + } + return key; + } + + bssl::UniquePtr createEcKeyFromJwkEC(int nid, const std::string& x, const std::string& y) { + bssl::UniquePtr ec_key(EC_KEY_new_by_curve_name(nid)); + if (!ec_key) { + updateStatus(Status::JwksEcCreateKeyFail); + return nullptr; + } + bssl::UniquePtr bn_x = createBigNumFromBase64UrlString(x); + bssl::UniquePtr bn_y = createBigNumFromBase64UrlString(y); + if (!bn_x || !bn_y) { + // EC public key field x or y Base64 decode fail + updateStatus(Status::JwksEcXorYBadBase64); + return nullptr; + } + + if (EC_KEY_set_public_key_affine_coordinates(ec_key.get(), bn_x.get(), bn_y.get()) == 0) { + updateStatus(Status::JwksEcParseError); + return nullptr; + } + return ec_key; + } + + bssl::UniquePtr createRsaFromJwk(const std::string& n, const std::string& e) { + bssl::UniquePtr n_bn = createBigNumFromBase64UrlString(n); + bssl::UniquePtr e_bn = createBigNumFromBase64UrlString(e); + if (n_bn == nullptr || e_bn == nullptr) { + // RSA public key field is missing or has parse error. + updateStatus(Status::JwksRsaParseError); + return nullptr; + } + if (BN_cmp_word(e_bn.get(), 3) != 0 && BN_cmp_word(e_bn.get(), 65537) != 0) { + // non-standard key; reject it early. + updateStatus(Status::JwksRsaParseError); + return nullptr; + } + // When jwt_verify_lib's minimum supported BoringSSL revision is past + // https://boringssl-review.googlesource.com/c/boringssl/+/59386 (May 2023), + // replace all this with `RSA_new_public_key` instead. + bssl::UniquePtr rsa(RSA_new()); + if (rsa == nullptr || !RSA_set0_key(rsa.get(), n_bn.get(), e_bn.get(), /*d=*/nullptr)) { + // Allocation or programmer error. + updateStatus(Status::JwksRsaParseError); + return nullptr; + } + // `RSA_set0_key` takes ownership, but only on success. + n_bn.release(); + e_bn.release(); + if (!RSA_check_key(rsa.get())) { + // Not a valid RSA public key. + updateStatus(Status::JwksRsaParseError); + return nullptr; + } + return rsa; + } + + std::string createRawKeyFromJwkOKP(int /*nid*/, size_t keylen, const std::string& x) { + std::string x_decoded; + if (!absl::WebSafeBase64Unescape(x, &x_decoded)) { + updateStatus(Status::JwksOKPXBadBase64); + } else if (x_decoded.length() != keylen) { + updateStatus(Status::JwksOKPXWrongLength); + } + // For OKP the "x" value is the public key and can just be used as-is + return x_decoded; + } + +private: + bssl::UniquePtr createBigNumFromBase64UrlString(const std::string& s) { + std::string s_decoded; + if (!absl::WebSafeBase64Unescape(s, &s_decoded)) { + return nullptr; + } + return bssl::UniquePtr( + BN_bin2bn(castToUChar(s_decoded), s_decoded.length(), nullptr)); + }; +}; + +Status extractJwkFromJwkRSA(const ::google::protobuf::Struct& jwk_pb, Jwks::Pubkey* jwk) { + if (!jwk->alg_.empty() && (jwk->alg_.size() < 2 || (jwk->alg_.compare(0, 2, "RS") != 0 && + jwk->alg_.compare(0, 2, "PS") != 0))) { + return Status::JwksRSAKeyBadAlg; + } + + StructUtils jwk_getter(jwk_pb); + std::string n_str; + auto code = jwk_getter.GetString("n", &n_str); + if (code == StructUtils::MISSING) { + return Status::JwksRSAKeyMissingN; + } + if (code == StructUtils::WRONG_TYPE) { + return Status::JwksRSAKeyBadN; + } + + std::string e_str; + code = jwk_getter.GetString("e", &e_str); + if (code == StructUtils::MISSING) { + return Status::JwksRSAKeyMissingE; + } + if (code == StructUtils::WRONG_TYPE) { + return Status::JwksRSAKeyBadE; + } + + KeyGetter e_getter; + jwk->rsa_ = e_getter.createRsaFromJwk(n_str, e_str); + return e_getter.getStatus(); +} + +Status extractJwkFromJwkEC(const ::google::protobuf::Struct& jwk_pb, Jwks::Pubkey* jwk) { + if (!jwk->alg_.empty() && (jwk->alg_.size() < 2 || jwk->alg_.compare(0, 2, "ES") != 0)) { + return Status::JwksECKeyBadAlg; + } + + StructUtils jwk_getter(jwk_pb); + std::string crv_str; + auto code = jwk_getter.GetString("crv", &crv_str); + if (code == StructUtils::MISSING) { + crv_str = ""; + } + if (code == StructUtils::WRONG_TYPE) { + return Status::JwksECKeyBadCrv; + } + jwk->crv_ = crv_str; + + // If both alg and crv specified, make sure they match + if (!jwk->alg_.empty() && !jwk->crv_.empty()) { + if (!((jwk->alg_ == "ES256" && jwk->crv_ == "P-256") || + (jwk->alg_ == "ES384" && jwk->crv_ == "P-384") || + (jwk->alg_ == "ES512" && jwk->crv_ == "P-521"))) { + return Status::JwksECKeyAlgNotCompatibleWithCrv; + } + } + + // If neither alg or crv is set, assume P-256 + if (jwk->alg_.empty() && jwk->crv_.empty()) { + jwk->crv_ = "P-256"; + } + + int nid; + if (jwk->alg_ == "ES256" || jwk->crv_ == "P-256") { + nid = NID_X9_62_prime256v1; + jwk->crv_ = "P-256"; + } else if (jwk->alg_ == "ES384" || jwk->crv_ == "P-384") { + nid = NID_secp384r1; + jwk->crv_ = "P-384"; + } else if (jwk->alg_ == "ES512" || jwk->crv_ == "P-521") { + nid = NID_secp521r1; + jwk->crv_ = "P-521"; + } else { + return Status::JwksECKeyAlgOrCrvUnsupported; + } + + std::string x_str; + code = jwk_getter.GetString("x", &x_str); + if (code == StructUtils::MISSING) { + return Status::JwksECKeyMissingX; + } + if (code == StructUtils::WRONG_TYPE) { + return Status::JwksECKeyBadX; + } + + std::string y_str; + code = jwk_getter.GetString("y", &y_str); + if (code == StructUtils::MISSING) { + return Status::JwksECKeyMissingY; + } + if (code == StructUtils::WRONG_TYPE) { + return Status::JwksECKeyBadY; + } + + KeyGetter e_getter; + jwk->ec_key_ = e_getter.createEcKeyFromJwkEC(nid, x_str, y_str); + return e_getter.getStatus(); +} + +Status extractJwkFromJwkOct(const ::google::protobuf::Struct& jwk_pb, Jwks::Pubkey* jwk) { + if (!jwk->alg_.empty() && jwk->alg_ != "HS256" && jwk->alg_ != "HS384" && jwk->alg_ != "HS512") { + return Status::JwksHMACKeyBadAlg; + } + + StructUtils jwk_getter(jwk_pb); + std::string k_str; + auto code = jwk_getter.GetString("k", &k_str); + if (code == StructUtils::MISSING) { + return Status::JwksHMACKeyMissingK; + } + if (code == StructUtils::WRONG_TYPE) { + return Status::JwksHMACKeyBadK; + } + + std::string key; + if (!absl::WebSafeBase64Unescape(k_str, &key) || key.empty()) { + return Status::JwksOctBadBase64; + } + + jwk->hmac_key_ = key; + return Status::Ok; +} + +// The "OKP" key type is defined in https://tools.ietf.org/html/rfc8037 +Status extractJwkFromJwkOKP(const ::google::protobuf::Struct& jwk_pb, Jwks::Pubkey* jwk) { + // alg is not required, but if present it must be EdDSA + if (!jwk->alg_.empty() && jwk->alg_ != "EdDSA") { + return Status::JwksOKPKeyBadAlg; + } + + // crv is required per https://tools.ietf.org/html/rfc8037#section-2 + StructUtils jwk_getter(jwk_pb); + std::string crv_str; + auto code = jwk_getter.GetString("crv", &crv_str); + if (code == StructUtils::MISSING) { + return Status::JwksOKPKeyMissingCrv; + } + if (code == StructUtils::WRONG_TYPE) { + return Status::JwksOKPKeyBadCrv; + } + jwk->crv_ = crv_str; + + // Valid crv values: + // https://tools.ietf.org/html/rfc8037#section-3 + // https://www.iana.org/assignments/jose/jose.xhtml#web-key-elliptic-curve + // In addition to Ed25519 there are: + // X25519: Implemented in boringssl but not used for JWT and thus not + // supported here + // Ed448 and X448: Not implemented in boringssl + int nid; + size_t keylen; + if (jwk->crv_ == "Ed25519") { + nid = EVP_PKEY_ED25519; + keylen = ED25519_PUBLIC_KEY_LEN; + } else { + return Status::JwksOKPKeyCrvUnsupported; + } + + // x is required per https://tools.ietf.org/html/rfc8037#section-2 + std::string x_str; + code = jwk_getter.GetString("x", &x_str); + if (code == StructUtils::MISSING) { + return Status::JwksOKPKeyMissingX; + } + if (code == StructUtils::WRONG_TYPE) { + return Status::JwksOKPKeyBadX; + } + + KeyGetter e_getter; + jwk->okp_key_raw_ = e_getter.createRawKeyFromJwkOKP(nid, keylen, x_str); + return e_getter.getStatus(); +} + +Status extractJwk(const ::google::protobuf::Struct& jwk_pb, Jwks::Pubkey* jwk) { + StructUtils jwk_getter(jwk_pb); + // Check "kty" parameter, it should exist. + // https://tools.ietf.org/html/rfc7517#section-4.1 + auto code = jwk_getter.GetString("kty", &jwk->kty_); + if (code == StructUtils::MISSING) { + return Status::JwksMissingKty; + } + if (code == StructUtils::WRONG_TYPE) { + return Status::JwksBadKty; + } + + // "kid" and "alg" are optional, if they do not exist, set them to + // empty. https://tools.ietf.org/html/rfc7517#page-8 + jwk_getter.GetString("kid", &jwk->kid_); + jwk_getter.GetString("alg", &jwk->alg_); + + // Extract public key according to "kty" value. + // https://tools.ietf.org/html/rfc7518#section-6.1 + if (jwk->kty_ == "EC") { + return extractJwkFromJwkEC(jwk_pb, jwk); + } else if (jwk->kty_ == "RSA") { + return extractJwkFromJwkRSA(jwk_pb, jwk); + } else if (jwk->kty_ == "oct") { + return extractJwkFromJwkOct(jwk_pb, jwk); + } else if (jwk->kty_ == "OKP") { + return extractJwkFromJwkOKP(jwk_pb, jwk); + } + return Status::JwksNotImplementedKty; +} + +Status extractX509(const std::string& key, Jwks::Pubkey* jwk) { + jwk->bio_.reset(BIO_new(BIO_s_mem())); + if (BIO_write(jwk->bio_.get(), key.c_str(), key.length()) <= 0) { + return Status::JwksX509BioWriteError; + } + jwk->x509_.reset(PEM_read_bio_X509(jwk->bio_.get(), nullptr, nullptr, nullptr)); + if (jwk->x509_ == nullptr) { + return Status::JwksX509ParseError; + } + bssl::UniquePtr tmp_pkey(X509_get_pubkey(jwk->x509_.get())); + if (tmp_pkey == nullptr) { + return Status::JwksX509GetPubkeyError; + } + jwk->rsa_.reset(EVP_PKEY_get1_RSA(tmp_pkey.get())); + if (jwk->rsa_ == nullptr) { + return Status::JwksX509GetPubkeyError; + } + return Status::Ok; +} + +bool shouldCheckX509(const ::google::protobuf::Struct& jwks_pb) { + if (jwks_pb.fields().empty()) { + return false; + } + + for (const auto& kid : jwks_pb.fields()) { + if (kid.first.empty() || kid.second.kind_case() != google::protobuf::Value::kStringValue) { + return false; + } + const std::string& cert = kid.second.string_value(); + if (!absl::StartsWith(cert, kX509CertPrefix) || !absl::EndsWith(cert, kX509CertSuffix)) { + return false; + } + } + return true; +} + +Status createFromX509(const ::google::protobuf::Struct& jwks_pb, std::vector& keys) { + for (const auto& kid : jwks_pb.fields()) { + Jwks::PubkeyPtr key_ptr(new Jwks::Pubkey()); + Status status = extractX509(kid.second.string_value(), key_ptr.get()); + if (status != Status::Ok) { + return status; + } + + key_ptr->kid_ = kid.first; + key_ptr->kty_ = "RSA"; + keys.push_back(std::move(key_ptr)); + } + return Status::Ok; +} + +} // namespace + +Status Jwks::addKeyFromPem(const std::string& pkey, const std::string& kid, const std::string& alg) { + JwksPtr tmp = Jwks::createFromPem(pkey, kid, alg); + if (tmp->getStatus() != Status::Ok) { + return tmp->getStatus(); + } + keys_.insert(keys_.end(), std::make_move_iterator(tmp->keys_.begin()), + std::make_move_iterator(tmp->keys_.end())); + return Status::Ok; +} + +JwksPtr Jwks::createFrom(const std::string& pkey, Type type) { + JwksPtr keys(new Jwks()); + switch (type) { + case Type::JWKS: + keys->createFromJwksCore(pkey); + break; + case Type::PEM: + keys->createFromPemCore(pkey); + break; + } + return keys; +} + +JwksPtr Jwks::createFromPem(const std::string& pkey, const std::string& kid, + const std::string& alg) { + std::unique_ptr ret = Jwks::createFrom(pkey, Jwks::PEM); + if (ret->getStatus() != Status::Ok) { + return ret; + } + if (ret->keys_.size() != 1) { + ret->updateStatus(Status::JwksPemBadBase64); + return ret; + } + Pubkey* jwk = ret->keys_.at(0).get(); + jwk->kid_ = kid; + jwk->alg_ = alg; + + // If alg is a known EC algorithm, set the correct crv as well. + if (jwk->alg_ == "ES256") { + jwk->crv_ = "P-256"; + } + if (jwk->alg_ == "ES384") { + jwk->crv_ = "P-384"; + } + if (jwk->alg_ == "ES512") { + jwk->crv_ = "P-521"; + } + return ret; +} + +// pkey_pem must be a PEM-encoded PKCS #8 public key. +// This is the format that starts with -----BEGIN PUBLIC KEY-----. +void Jwks::createFromPemCore(const std::string& pkey_pem) { + keys_.clear(); + PubkeyPtr key_ptr(new Pubkey()); + KeyGetter e; + bssl::UniquePtr evp_pkey(e.createEvpPkeyFromPem(pkey_pem)); + updateStatus(e.getStatus()); + + if (evp_pkey == nullptr) { + assert(e.getStatus() != Status::Ok); + return; + } + assert(e.getStatus() == Status::Ok); + + switch (EVP_PKEY_id(evp_pkey.get())) { + case EVP_PKEY_RSA: + key_ptr->rsa_.reset(EVP_PKEY_get1_RSA(evp_pkey.get())); + key_ptr->kty_ = "RSA"; + break; + case EVP_PKEY_EC: + key_ptr->ec_key_.reset(EVP_PKEY_get1_EC_KEY(evp_pkey.get())); + key_ptr->kty_ = "EC"; + break; +#ifndef BORINGSSL_FIPS + case EVP_PKEY_ED25519: { + uint8_t raw_key[ED25519_PUBLIC_KEY_LEN]; + size_t out_len = ED25519_PUBLIC_KEY_LEN; + if (EVP_PKEY_get_raw_public_key(evp_pkey.get(), raw_key, &out_len) != 1 || + out_len != ED25519_PUBLIC_KEY_LEN) { + updateStatus(Status::JwksPemGetRawEd25519Error); + return; + } + key_ptr->okp_key_raw_ = std::string(reinterpret_cast(raw_key), out_len); + key_ptr->kty_ = "OKP"; + key_ptr->crv_ = "Ed25519"; + break; + } +#endif + default: + updateStatus(Status::JwksPemNotImplementedKty); + return; + } + + keys_.push_back(std::move(key_ptr)); +} + +void Jwks::createFromJwksCore(const std::string& jwks_json) { + keys_.clear(); + + ::google::protobuf::util::JsonParseOptions options; + ::google::protobuf::Struct jwks_pb; + const auto status = ::google::protobuf::util::JsonStringToMessage(jwks_json, &jwks_pb, options); + if (!status.ok()) { + updateStatus(Status::JwksParseError); + return; + } + + const auto& fields = jwks_pb.fields(); + const auto keys_it = fields.find("keys"); + if (keys_it == fields.end()) { + // X509 doesn't have "keys" field. + if (shouldCheckX509(jwks_pb)) { + updateStatus(createFromX509(jwks_pb, keys_)); + return; + } + updateStatus(Status::JwksNoKeys); + return; + } + if (keys_it->second.kind_case() != google::protobuf::Value::kListValue) { + updateStatus(Status::JwksBadKeys); + return; + } + + for (const auto& key_value : keys_it->second.list_value().values()) { + if (key_value.kind_case() != ::google::protobuf::Value::kStructValue) { + continue; + } + PubkeyPtr key_ptr(new Pubkey()); + Status extract_status = extractJwk(key_value.struct_value(), key_ptr.get()); + if (extract_status == Status::Ok) { + keys_.push_back(std::move(key_ptr)); + resetStatus(extract_status); + } else { + updateStatus(extract_status); + } + } + + if (keys_.empty()) { + updateStatus(Status::JwksNoValidKeys); + } else { + resetStatus(Status::Ok); + } +} + +} // namespace JwtVerify +} // namespace Common +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/common/jwt/jwks.h b/source/extensions/filters/http/common/jwt/jwks.h new file mode 100644 index 0000000000000..d3d118125976a --- /dev/null +++ b/source/extensions/filters/http/common/jwt/jwks.h @@ -0,0 +1,88 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include + +#include "source/extensions/filters/http/common/jwt/status.h" + +#include "openssl/ec.h" +#include "openssl/evp.h" +#include "openssl/pem.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Common { +namespace JwtVerify { + +/** + * Class to parse and a hold JSON Web Key Set. + * + * Usage example: + * JwksPtr keys = Jwks::createFrom(jwks_string, type); + * if (keys->getStatus() == Status::Ok) { ... } + */ +class Jwks : public WithStatus { +public: + // Format of public key. + enum Type { JWKS, PEM }; + + // Create from string + static std::unique_ptr createFrom(const std::string& pkey, Type type); + // Executes to createFrom with type=PEM and sets additional JWKS paramaters + // not specified within the PEM. + static std::unique_ptr createFromPem(const std::string& pkey, const std::string& kid, + const std::string& alg); + + // Adds a key to this keyset. + Status addKeyFromPem(const std::string& pkey, const std::string& kid, const std::string& alg); + + // Struct for JSON Web Key + struct Pubkey { + std::string hmac_key_; + std::string kid_; + std::string kty_; + std::string alg_; + std::string crv_; + bssl::UniquePtr rsa_; + bssl::UniquePtr ec_key_; + std::string okp_key_raw_; + bssl::UniquePtr bio_; + bssl::UniquePtr x509_; + }; + using PubkeyPtr = std::unique_ptr; + + // Access to list of Jwks + const std::vector& keys() const { return keys_; } + +private: + // Create Jwks + void createFromJwksCore(const std::string& pkey_jwks); + // Create PEM + void createFromPemCore(const std::string& pkey_pem); + + // List of Jwks + std::vector keys_; +}; + +using JwksPtr = std::unique_ptr; + +} // namespace JwtVerify +} // namespace Common +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/common/jwt/jwt.cc b/source/extensions/filters/http/common/jwt/jwt.cc new file mode 100644 index 0000000000000..54f8b74d222f9 --- /dev/null +++ b/source/extensions/filters/http/common/jwt/jwt.cc @@ -0,0 +1,166 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "source/extensions/filters/http/common/jwt/jwt.h" + +#include + +#include "source/extensions/filters/http/common/jwt/struct_utils.h" + +#include "absl/container/flat_hash_set.h" +#include "absl/strings/escaping.h" +#include "absl/strings/str_split.h" +#include "absl/time/clock.h" +#include "google/protobuf/util/json_util.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Common { +namespace JwtVerify { + +namespace { + +bool isImplemented(absl::string_view alg) { + static const absl::flat_hash_set implemented_algs = { + {"ES256"}, {"ES384"}, {"ES512"}, {"HS256"}, {"HS384"}, + {"HS512"}, {"RS256"}, {"RS384"}, {"RS512"}, {"PS256"}, + {"PS384"}, {"PS512"}, {"EdDSA"}, + }; + + return implemented_algs.find(alg) != implemented_algs.end(); +} + +} // namespace + +Jwt::Jwt(const Jwt& instance) { *this = instance; } + +Jwt& Jwt::operator=(const Jwt& rhs) { + parseFromString(rhs.jwt_); + return *this; +} + +Status Jwt::parseFromString(const std::string& jwt) { + // jwt must have exactly 2 dots with 3 sections. + jwt_ = jwt; + std::vector jwt_split = absl::StrSplit(jwt, '.', absl::SkipEmpty()); + if (jwt_split.size() != 3) { + return Status::JwtBadFormat; + } + + // Parse header json + header_str_base64url_ = std::string(jwt_split[0]); + if (!absl::WebSafeBase64Unescape(header_str_base64url_, &header_str_)) { + return Status::JwtHeaderParseErrorBadBase64; + } + + ::google::protobuf::util::JsonParseOptions options; + const auto header_status = + ::google::protobuf::util::JsonStringToMessage(header_str_, &header_pb_, options); + if (!header_status.ok()) { + return Status::JwtHeaderParseErrorBadJson; + } + + StructUtils header_getter(header_pb_); + // Header should contain "alg" and should be a string. + if (header_getter.GetString("alg", &alg_) != StructUtils::OK) { + return Status::JwtHeaderBadAlg; + } + + if (!isImplemented(alg_)) { + return Status::JwtHeaderNotImplementedAlg; + } + + // Header may contain "kid", should be a string if exists. + if (header_getter.GetString("kid", &kid_) == StructUtils::WRONG_TYPE) { + return Status::JwtHeaderBadKid; + } + + // Parse payload json + payload_str_base64url_ = std::string(jwt_split[1]); + if (!absl::WebSafeBase64Unescape(payload_str_base64url_, &payload_str_)) { + return Status::JwtPayloadParseErrorBadBase64; + } + + const auto payload_status = + ::google::protobuf::util::JsonStringToMessage(payload_str_, &payload_pb_, options); + if (!payload_status.ok()) { + return Status::JwtPayloadParseErrorBadJson; + } + + StructUtils payload_getter(payload_pb_); + if (payload_getter.GetString("iss", &iss_) == StructUtils::WRONG_TYPE) { + return Status::JwtPayloadParseErrorIssNotString; + } + if (payload_getter.GetString("sub", &sub_) == StructUtils::WRONG_TYPE) { + return Status::JwtPayloadParseErrorSubNotString; + } + + auto result = payload_getter.GetUInt64("iat", &iat_); + if (result == StructUtils::WRONG_TYPE) { + return Status::JwtPayloadParseErrorIatNotInteger; + } else if (result == StructUtils::OUT_OF_RANGE) { + return Status::JwtPayloadParseErrorIatOutOfRange; + } + + result = payload_getter.GetUInt64("nbf", &nbf_); + if (result == StructUtils::WRONG_TYPE) { + return Status::JwtPayloadParseErrorNbfNotInteger; + } else if (result == StructUtils::OUT_OF_RANGE) { + return Status::JwtPayloadParseErrorNbfOutOfRange; + } + + result = payload_getter.GetUInt64("exp", &exp_); + if (result == StructUtils::WRONG_TYPE) { + return Status::JwtPayloadParseErrorExpNotInteger; + } else if (result == StructUtils::OUT_OF_RANGE) { + return Status::JwtPayloadParseErrorExpOutOfRange; + } + + if (payload_getter.GetString("jti", &jti_) == StructUtils::WRONG_TYPE) { + return Status::JwtPayloadParseErrorJtiNotString; + } + + // "aud" can be either string array or string. + // GetStringList function will try to read as string, if fails, + // try to read as string array. + if (payload_getter.GetStringList("aud", &audiences_) == StructUtils::WRONG_TYPE) { + return Status::JwtPayloadParseErrorAudNotString; + } + + // Set up signature + if (!absl::WebSafeBase64Unescape(jwt_split[2], &signature_)) { + // Signature is a bad Base64url input. + return Status::JwtSignatureParseErrorBadBase64; + } + return Status::Ok; +} + +Status Jwt::verifyTimeConstraint(uint64_t now, uint64_t clock_skew) const { + // Check Jwt is active (nbf). + if (now + clock_skew < nbf_) { + return Status::JwtNotYetValid; + } + // Check JWT has not expired (exp). + if (exp_ && now > exp_ + clock_skew) { + return Status::JwtExpired; + } + return Status::Ok; +} + +} // namespace JwtVerify +} // namespace Common +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/common/jwt/jwt.h b/source/extensions/filters/http/common/jwt/jwt.h new file mode 100644 index 0000000000000..e4916090dc931 --- /dev/null +++ b/source/extensions/filters/http/common/jwt/jwt.h @@ -0,0 +1,113 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include + +#include "source/extensions/filters/http/common/jwt/status.h" + +#include "google/protobuf/struct.pb.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Common { +namespace JwtVerify { + +// Clock skew defaults to one minute. +constexpr uint64_t kClockSkewInSecond = 60; + +/** + * struct to hold a JWT data. + */ +struct Jwt { + // entire jwt + std::string jwt_; + + // header string + std::string header_str_; + // header base64_url encoded + std::string header_str_base64url_; + // header in Struct protobuf + ::google::protobuf::Struct header_pb_; + + // payload string + std::string payload_str_; + // payload base64_url encoded + std::string payload_str_base64url_; + // payload in Struct protobuf + ::google::protobuf::Struct payload_pb_; + // signature string + std::string signature_; + // alg + std::string alg_; + // kid + std::string kid_; + // iss + std::string iss_; + // audiences + std::vector audiences_; + // sub + std::string sub_; + // issued at + uint64_t iat_ = 0; + // not before + uint64_t nbf_ = 0; + // expiration + uint64_t exp_ = 0; + // JWT ID + std::string jti_; + + /** + * Standard constructor. + */ + Jwt() = default; + /** + * Copy constructor. The copy constructor is marked as explicit as the caller + * should understand the copy operation is non-trivial as a complete + * re-deserialization occurs. + * @param rhs the instance to copy. + */ + explicit Jwt(const Jwt& instance); + + /** + * Copy Jwt instance. + * @param rhs the instance to copy. + * @return this + */ + Jwt& operator=(const Jwt& rhs); + + /** + * Parse Jwt from string text + * @return the status. + */ + Status parseFromString(const std::string& jwt); + + /* + * Verify Jwt time constraint if specified + * esp: expiration time, nbf: not before time. + * @param now: is the current time in seconds since the unix epoch + * @param clock_skew: the the clock skew in second. + * @return the verification status. + */ + Status verifyTimeConstraint(uint64_t now, uint64_t clock_skew = kClockSkewInSecond) const; +}; + +} // namespace JwtVerify +} // namespace Common +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/common/jwt/simple_lru_cache.h b/source/extensions/filters/http/common/jwt/simple_lru_cache.h new file mode 100644 index 0000000000000..c54ad616518a3 --- /dev/null +++ b/source/extensions/filters/http/common/jwt/simple_lru_cache.h @@ -0,0 +1,50 @@ +/* Copyright 2016 Google Inc. All Rights Reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +==============================================================================*/ + +// For inclusion in .h files. The real class definition is in +// simple_lru_cache_inl.h. + +#pragma once + +#include + +#include "absl/container/flat_hash_map.h" // for hash<> + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Common { +namespace JwtVerify { +namespace SimpleLruCache { + +namespace internal { +template struct SimpleLRUHash : public std::hash {}; +} // namespace internal + +template , + typename EQ = std::equal_to> +class SimpleLRUCache; + +// Deleter is a functor that defines how to delete a Value*. That is, it +// contains a public method: +// operator() (Value* value) +// See example in the associated unittest. +template , typename EQ = std::equal_to> +class SimpleLRUCacheWithDeleter; + +} // namespace SimpleLruCache +} // namespace JwtVerify +} // namespace Common +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/common/jwt/simple_lru_cache_inl.h b/source/extensions/filters/http/common/jwt/simple_lru_cache_inl.h new file mode 100644 index 0000000000000..23e9a4d881a22 --- /dev/null +++ b/source/extensions/filters/http/common/jwt/simple_lru_cache_inl.h @@ -0,0 +1,1099 @@ +/* Copyright 2016 Google Inc. All Rights Reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +==============================================================================*/ + +// A generic LRU cache that maps from type Key to Value*. +// +// . Memory usage is fairly high: on a 64-bit architecture, a cache with +// 8-byte keys can use 108 bytes per element, not counting the +// size of the values. This overhead can be significant if many small +// elements are stored in the cache. +// +// . lookup returns a "Value*". Client should call "release" when done. +// +// . Override "removeElement" if you want to be notified when an +// element is being removed. The default implementation simply calls +// "delete" on the pointer. +// +// . Call clear() before destruction. +// +// . No internal locking is done: if the same cache will be shared +// by multiple threads, the caller should perform the required +// synchronization before invoking any operations on the cache. +// Note a reader lock is not sufficient as lookup() updates the pin count. +// +// . We provide support for setting a "max_idle_time". Entries +// are discarded when they have not been used for a time +// greater than the specified max idle time. If you do not +// call setMaxIdleSeconds(), entries never expire (they can +// only be removed to meet size constraints). +// +// . We also provide support for a strict age-based eviction policy +// instead of LRU. See setAgeBasedEviction(). + +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "source/extensions/filters/http/common/jwt/simple_lru_cache.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Common { +namespace JwtVerify { +namespace SimpleLruCache { + +#undef GOOGLE_DISALLOW_EVIL_CONSTRUCTORS +#define GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(TypeName) \ + TypeName(const TypeName&); \ + void operator=(const TypeName&) + +// Define number of microseconds for a second. +const int64_t kSecToUsec = 1000000; + +// Define a simple cycle timer interface to encapsulate timer related code. +// The concept is from CPU cycle. The cycle clock code from +// https://github.com/google/benchmark/src/cycleclock.h can be used. +// But that code only works for some platforms. To make code works for all +// platforms, SimpleCycleTimer class uses a fake CPU cycle each taking a +// microsecond. If needed, this timer class can be easily replaced by a +// real cycle_clock. +class SimpleCycleTimer { + public: + // Return the current cycle in microseconds. + static int64_t now() { + return std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + } + // Return number of cycles in a second. + static int64_t frequency() { return kSecToUsec; } + + private: + SimpleCycleTimer(); // no instances +}; + +// A constant iterator. a client of SimpleLRUCache should not create these +// objects directly, instead, create objects of type +// SimpleLRUCache::const_iterator. This is created inside of +// SimpleLRUCache::begin(),end(). Key and Value are the same as the template +// args to SimpleLRUCache Elem - the Value type for the internal hash_map that +// the SimpleLRUCache maintains H and EQ are the same as the template arguments +// for SimpleLRUCache +// +// NOTE: the iterator needs to keep a copy of end() for the Cache it is +// iterating over this is so SimpleLRUCacheConstIterator does not try to update +// its internal pair if its internal hash_map iterator is pointing +// to end see the implementation of operator++ for an example. +// +// NOTE: DO NOT SAVE POINTERS TO THE ITEM RETURNED BY THIS ITERATOR +// e.g. SimpleLRUCacheConstIterator it = something; do not say KeyToSave +// &something->first this will NOT work., as soon as you increment the iterator +// this will be gone. :( + +template +class SimpleLRUCacheConstIterator + : public std::iterator> { + public: + typedef typename MapType::const_iterator HashMapConstIterator; + // Allow parent template's types to be referenced without qualification. + typedef typename SimpleLRUCacheConstIterator::reference reference; + typedef typename SimpleLRUCacheConstIterator::pointer pointer; + + // This default constructed Iterator can only be assigned to or destroyed. + // All other operations give undefined behaviour. + SimpleLRUCacheConstIterator() {} + SimpleLRUCacheConstIterator(HashMapConstIterator it, + HashMapConstIterator end); + SimpleLRUCacheConstIterator& operator++(); + + reference operator*() { return external_view_; } + pointer operator->() { return &external_view_; } + + // For LRU mode, last_use_time() returns elements last use time. + // See getLastUseTime() description for more information. + int64_t last_use_time() const { return last_use_; } + + // For age-based mode, insertion_time() returns elements insertion time. + // See getInsertionTime() description for more information. + int64_t insertion_time() const { return last_use_; } + + friend bool operator==(const SimpleLRUCacheConstIterator& a, + const SimpleLRUCacheConstIterator& b) { + return a.it_ == b.it_; + } + + friend bool operator!=(const SimpleLRUCacheConstIterator& a, + const SimpleLRUCacheConstIterator& b) { + return !(a == b); + } + + private: + HashMapConstIterator it_; + HashMapConstIterator end_; + std::pair external_view_; + int64_t last_use_; +}; + +// Each entry uses the following structure +template +struct SimpleLRUCacheElem { + Key key; // The key + Value* value; // The stored value + int pin; // Number of outstanding releases + size_t units; // Number of units for this value + SimpleLRUCacheElem* next = nullptr; // Next entry in LRU chain + SimpleLRUCacheElem* prev = nullptr; // Prev entry in LRU chain + int64_t last_use_; // Timestamp of last use (in LRU mode) + // or creation (in age-based mode) + + SimpleLRUCacheElem(const Key& k, Value* v, int p, size_t u, int64_t last_use) + : key(k), value(v), pin(p), units(u), last_use_(last_use) {} + + bool isLinked() const { + // If we are in the LRU then next and prev should be non-NULL. Otherwise + // both should be properly initialized to nullptr. + assert(static_cast(next == nullptr) == + static_cast(prev == nullptr)); + return next != nullptr; + } + + void unlink() { + if (!isLinked()) return; + prev->next = next; + next->prev = prev; + prev = nullptr; + next = nullptr; + } + + void link(SimpleLRUCacheElem* head) { + next = head->next; + prev = head; + next->prev = this; // i.e. head->next->prev = this; + prev->next = this; // i.e. head->next = this; + } + static const int64_t kNeverUsed = -1; +}; + +template +const int64_t SimpleLRUCacheElem::kNeverUsed; + +// A simple class passed into various cache methods to change the +// behavior for that single call. +class SimpleLRUCacheOptions { + public: + SimpleLRUCacheOptions() : update_eviction_order_(true) {} + + // If false neither the last modified time (for based age eviction) nor + // the element ordering (for LRU eviction) will be updated. + // This value must be the same for both lookup and release. + // The default is true. + bool update_eviction_order() const { return update_eviction_order_; } + void set_update_eviction_order(bool v) { update_eviction_order_ = v; } + + private: + bool update_eviction_order_; +}; + +// The MapType's value_type must be pair +template +class SimpleLRUCacheBase { + public: + // class ScopedLookup + // If you have some code that looks like this: + // val = c->Lookup(key); + // if (val) { + // if (something) { + // c->Release(key, val); + // return; + // } + // if (something else) { + // c->Release(key, val); + // return; + // } + // Then ScopedLookup will make the code simpler. It automatically + // releases the value when the instance goes out of scope. + // Example: + // ScopedLookup lookup(c, key); + // if (lookup.Found()) { + // ... + // + // NOTE: Be extremely careful when using ScopedLookup with Mutexes. This + // code is safe since the lock will be released after the ScopedLookup is + // destroyed. + // MutexLock l(&mu_); + // ScopedLookup lookup(....); + // + // This is NOT safe since the lock is released before the ScopedLookup is + // destroyed, and consequently the value will be unpinned without the lock + // being held. + // mu_.Lock(); + // ScopedLookup lookup(....); + // ... + // mu_.Unlock(); + class ScopedLookup { + public: + ScopedLookup(SimpleLRUCacheBase* cache, const Key& key) + : cache_(cache), + key_(key), + value_(cache_->lookupWithOptions(key_, options_)) {} + + ScopedLookup(SimpleLRUCacheBase* cache, const Key& key, + const SimpleLRUCacheOptions& options) + : cache_(cache), + key_(key), + options_(options), + value_(cache_->lookupWithOptions(key_, options_)) {} + + ~ScopedLookup() { + if (value_ != nullptr) cache_->releaseWithOptions(key_, value_, options_); + } + const Key& key() const { return key_; } + Value* value() const { return value_; } + bool found() const { return value_ != nullptr; } + const SimpleLRUCacheOptions& options() const { return options_; } + + private: + SimpleLRUCacheBase* const cache_; + const Key key_; + const SimpleLRUCacheOptions options_; + Value* const value_; + + GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(ScopedLookup); + }; + + // Create a cache that will hold up to the specified number of units. + // Usually the units will be byte sizes, but in some caches different + // units may be used. For instance, we may want each open file to + // be one unit in an open-file cache. + // + // By default, the max_idle_time is infinity; i.e. entries will + // stick around in the cache regardless of how old they are. + explicit SimpleLRUCacheBase(int64_t total_units); + + // Release all resources. Cache must have been "clear"ed. This + // requirement is imposed because "clear()" will call + // "removeElement" for each element in the cache. The destructor + // cannot do that because it runs after any subclass destructor. + virtual ~SimpleLRUCacheBase() { + assert(table_.size() == 0); + assert(defer_.size() == 0); + } + + // Change the maximum size of the cache to the specified number of units. + // If necessary, entries will be evicted to comply with the new size. + void setMaxSize(int64_t total_units) { + max_units_ = total_units; + garbageCollect(); + } + + // Change the max idle time to the specified number of seconds. + // If "seconds" is a negative number, it sets the max idle time + // to infinity. + void setMaxIdleSeconds(double seconds) { + setTimeout(seconds, true /* lru */); + } + + // Stop using the LRU eviction policy and instead expire anything + // that has been in the cache for more than the specified number + // of seconds. + // If "seconds" is a negative number, entries don't expire but if + // we need to make room the oldest entries will be removed first. + // You can't set both a max idle time and age-based eviction. + void setAgeBasedEviction(double seconds) { + setTimeout(seconds, false /* lru */); + } + + // If cache contains an entry for "k", return a pointer to it. + // Else return nullptr. + // + // If a value is returned, the caller must call "release" when it no + // longer needs that value. This functionality is useful to prevent + // the value from being evicted from the cache until it is no longer + // being used. + Value* lookup(const Key& k) { + return lookupWithOptions(k, SimpleLRUCacheOptions()); + } + + // Same as "lookup(Key)" but allows for additional options. See + // the SimpleLRUCacheOptions object for more information. + Value* lookupWithOptions(const Key& k, const SimpleLRUCacheOptions& options); + + // Removes the pinning done by an earlier "lookup". After this call, + // the caller should no longer depend on the value sticking around. + // + // If there are no more pins on this entry, it may be deleted if + // either it has been "remove"d, or the cache is overfull. + // In this case "removeElement" will be called. + void release(const Key& k, Value* value) { + releaseWithOptions(k, value, SimpleLRUCacheOptions()); + } + + // Same as "release(Key, Value)" but allows for additional options. See + // the SimpleLRUCacheOptions object for more information. Take care + // that the SimpleLRUCacheOptions object passed into this method is + // compatible with SimpleLRUCacheOptions object passed into lookup. + // If they are incompatible it can put the cache into some unexpected + // states. Better yet, just use a ScopedLookup which takes care of this + // for you. + void releaseWithOptions(const Key& k, Value* value, + const SimpleLRUCacheOptions& options); + + // Insert the specified "k,value" pair in the cache. Remembers that + // the value occupies "units" units. For "InsertPinned", the newly + // inserted value will be pinned in the cache: the caller should + // call "release" when it wants to remove the pin. + // + // Any old entry for "k" is "remove"d. + // + // If the insertion causes the cache to become overfull, unpinned + // entries will be deleted in an LRU order to make room. + // "removeElement" will be called for each such entry. + void insert(const Key& k, Value* value, size_t units) { + insertPinned(k, value, units); + release(k, value); + } + void insertPinned(const Key& k, Value* value, size_t units); + + // Change the reported size of an object. + void updateSize(const Key& k, const Value* value, size_t units); + + // return true iff pair is still in use + // (i.e., either in the table or the deferred list) + // Note, if (value == nullptr), only key is used for matching + bool stillInUse(const Key& k) const { return stillInUse(k, nullptr); } + bool stillInUse(const Key& k, const Value* value) const; + + // Remove any entry corresponding to "k" from the cache. Note that + // if the entry is pinned because of an earlier lookup or + // insertPinned operation, the entry will disappear from further + // lookups, but will not actually be deleted until all of the pins + // are released. + // + // "removeElement" will be called if an entry is actually removed. + void remove(const Key& k); + + // Removes all entries from the cache. The pinned entries will + // disappear from further Lookups, but will not actually be deleted + // until all of the pins are released. This is different from clear() + // because clear() cleans up everything and requires that all Values are + // unpinned. + // + // "remove" will be called for each cache entry. + void removeAll(); + + // Remove all unpinned entries from the cache. + // "removeElement" will be called for each such entry. + void removeUnpinned(); + + // Remove all entries from the cache. It is an error to call this + // operation if any entry in the cache is currently pinned. + // + // "removeElement" will be called for all removed entries. + void clear(); + + // Remove all entries which have exceeded their max idle time or age + // set using setMaxIdleSeconds() or setAgeBasedEviction() respectively. + void removeExpiredEntries() { + if (max_idle_ >= 0) discardIdle(max_idle_); + } + + // Return current size of cache + int64_t size() const { return units_; } + + // Return number of entries in the cache. This value may differ + // from size() if some of the elements have a cost != 1. + int64_t entries() const { return table_.size(); } + + // Return size of deferred deletions + int64_t deferredSize() const; + + // Return number of deferred deletions + int64_t deferredEntries() const; + + // Return size of entries that are pinned but not deferred + int64_t pinnedSize() const { return pinned_units_; } + + // Return maximum size of cache + int64_t maxSize() const { return max_units_; } + + // Return the age (in microseconds) of the least recently used element in + // the cache. If the cache is empty, zero (0) is returned. + int64_t ageOfLRUItemInMicroseconds() const; + + // In LRU mode, this is the time of last use in cycles. Last use is defined + // as time of last release(), insert() or insertPinned() methods. + // + // The timer is not updated on lookup(), so getLastUseTime() will + // still return time of previous access until release(). + // + // Returns -1 if key was not found, CycleClock cycle count otherwise. + // REQUIRES: LRU mode + int64_t getLastUseTime(const Key& k) const; + + // For age-based mode, this is the time of element insertion in cycles, + // set by insert() and insertPinned() methods. + // Returns -1 if key was not found, CycleClock cycle count otherwise. + // REQUIRES: age-based mode + int64_t getInsertionTime(const Key& k) const; + + // Invokes 'debugIterator' on each element in the cache. The + // 'pin_count' argument supplied will be the pending reference count + // for the element. The 'is_deferred' argument will be true for + // elements that have been removed but whose removal is deferred. + // The supplied value for "output" will be passed to the debugIterator. + void debugOutput(std::string* output) const; + + // Return a std::string that summarizes the contents of the cache. + // + // The output of this function is not suitable for parsing by borgmon. For an + // example of exporting the summary information in a borgmon mapped-value + // format, see GFS_CS_BufferCache::ExportSummaryAsMap in + // file/gfs/chunkserver/gfs_chunkserver.{cc,h} + std::string summary() const { + std::stringstream ss; + ss << pinnedSize() << "/" << deferredSize() << "/" << size() << " p/d/a"; + return ss.str(); + } + + // STL style const_iterator support + typedef SimpleLRUCacheConstIterator const_iterator; + friend class SimpleLRUCacheConstIterator; + const_iterator begin() const { + return const_iterator(table_.begin(), table_.end()); + } + const_iterator end() const { + return const_iterator(table_.end(), table_.end()); + } + + // Invokes the 'resize' operation on the underlying map with the given + // size hint. The exact meaning of this operation and its availability + // depends on the supplied MapType. + void resizeTable(typename MapType::size_type size_hint) { + table_.resize(size_hint); + } + + protected: + // Override this operation if you want to control how a value is + // cleaned up. For example, if the value is a "File", you may want + // to "close" it instead of "delete"ing it. + // + // Not actually implemented here because often value's destructor is + // protected, and the derived SimpleLRUCache is declared a friend, + // so we implement it in the derived SimpleLRUCache. + virtual void removeElement(Value* value) = 0; + + virtual void debugIterator(const Value* value, int pin_count, + int64_t last_timestamp, bool is_deferred, + std::string* output) const { + std::stringstream ss; + ss << "ox" << std::hex << value << std::dec << ": pin: " << pin_count; + ss << ", is_deferred: " << is_deferred; + ss << ", last_use: " << last_timestamp << std::endl; + *output += ss.str(); + } + + // Override this operation if you want to evict cache entries + // based on parameters other than the total units stored. + // For example, if the cache stores open sstables, where the cost + // is the size in bytes of the open sstable, you may want to evict + // entries from the cache not only before the max size in bytes + // is reached but also before reaching the limit of open file + // descriptors. Thus, you may want to override this function in a + // subclass and return true if either size() is too large or + // entries() is too large. + virtual bool isOverfull() const { return units_ > max_units_; } + + private: + typedef SimpleLRUCacheElem Elem; + typedef MapType Table; + typedef typename Table::iterator TableIterator; + typedef typename Table::const_iterator TableConstIterator; + typedef MapType DeferredTable; + typedef typename DeferredTable::iterator DeferredTableIterator; + typedef typename DeferredTable::const_iterator DeferredTableConstIterator; + + Table table_; // Main table + // Pinned entries awaiting to be released before they can be discarded. + // This is a key -> list mapping (multiple deferred entries for the same key) + // The machinery used to maintain main LRU list is reused here, though this + // list is not necessarily LRU and we don't care about the order of elements. + DeferredTable defer_; + int64_t units_; // Combined units of all elements + int64_t max_units_; // Max allowed units + int64_t pinned_units_; // Combined units of all pinned elements + Elem head_; // Dummy head of LRU list (next is mru elem) + int64_t max_idle_; // Maximum number of idle cycles + bool lru_; // LRU or age-based eviction? + + // Representation invariants: + // . LRU list is circular doubly-linked list + // . Each live "Elem" is either in "table_" or "defer_" + // . LRU list contains elements in "table_" that can be removed to free space + // . Each "Elem" in "defer_" has a non-zero pin count + + void discard(Elem* e) { + assert(e->pin == 0); + units_ -= e->units; + removeElement(e->value); + delete e; + } + + // Count the number and total size of the elements in the deferred table. + void countDeferredEntries(int64_t* num_entries, int64_t* total_size) const; + + // Currently in deferred table? + // Note, if (value == nullptr), only key is used for matching. + bool inDeferredTable(const Key& k, const Value* value) const; + + void garbageCollect(); // Discard to meet space constraints + void discardIdle(int64_t max_idle); // Discard to meet idle-time constraints + + void setTimeout(double seconds, bool lru); + + bool isOverfullInternal() const { + return ((units_ > max_units_) || isOverfull()); + } + void remove(Elem* e); + + public: + static const size_t kElemSize = sizeof(Elem); + + private: + GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(SimpleLRUCacheBase); +}; + +template +SimpleLRUCacheBase::SimpleLRUCacheBase( + int64_t total_units) + : head_(Key(), nullptr, 0, 0, Elem::kNeverUsed) { + units_ = 0; + pinned_units_ = 0; + max_units_ = total_units; + head_.next = &head_; + head_.prev = &head_; + max_idle_ = -1; // Stands for "no expiration" + lru_ = true; // default to LRU, not age-based +} + +template +void SimpleLRUCacheBase::setTimeout(double seconds, + bool lru) { + if (seconds < 0 || std::isinf(seconds)) { + // Treat as no expiration based on idle time + lru_ = lru; + max_idle_ = -1; + } else if (max_idle_ >= 0 && lru != lru_) { + // LOG(DFATAL) << "Can't setMaxIdleSeconds() and setAgeBasedEviction()"; + // In production we'll just ignore the second call + assert(0); + } else { + lru_ = lru; + + // Convert to cycles ourselves in order to perform all calculations in + // floating point so that we avoid integer overflow. + // NOTE: The largest representable int64_t cannot be represented exactly as + // a + // double, so the cast results in a slightly larger value which cannot be + // converted back to an int64_t. The next smallest double is representable + // as + // an int64_t, however, so if we make sure that `timeout_cycles` is strictly + // smaller than the result of the cast, we know that casting + // `timeout_cycles` to int64_t will not overflow. + // NOTE 2: If you modify the computation here, make sure to update the + // getBoundaryTimeout() method in the test as well. + const double timeout_cycles = seconds * SimpleCycleTimer::frequency(); + if (timeout_cycles >= std::numeric_limits::max()) { + // The value is outside the range of int64_t, so "round" down to something + // that can be represented. + max_idle_ = std::numeric_limits::max(); + } else { + max_idle_ = static_cast(timeout_cycles); + } + discardIdle(max_idle_); + } +} + +template +void SimpleLRUCacheBase::removeAll() { + // For each element: call "Remove" + for (TableIterator iter = table_.begin(); iter != table_.end(); ++iter) { + remove(iter->second); + } + table_.clear(); +} + +template +void SimpleLRUCacheBase::removeUnpinned() { + for (Elem* e = head_.next; e != &head_;) { + Elem* next = e->next; + if (e->pin == 0) remove(e->key); + e = next; + } +} + +template +void SimpleLRUCacheBase::clear() { + // For each element: call "removeElement" and delete it + for (TableConstIterator iter = table_.begin(); iter != table_.end();) { + Elem* e = iter->second; + // Pre-increment the iterator to avoid possible + // accesses to deleted memory in cases where the + // key is a pointer to the memory that is freed by + // Discard. + ++iter; + discard(e); + } + // Pinned entries cannot be Discarded and defer_ contains nothing but pinned + // entries. Therefore, it must be already be empty at this point. + assert(defer_.empty()); + // Get back into pristine state + table_.clear(); + head_.next = &head_; + head_.prev = &head_; + units_ = 0; + pinned_units_ = 0; +} + +template +Value* SimpleLRUCacheBase::lookupWithOptions( + const Key& k, const SimpleLRUCacheOptions& options) { + removeExpiredEntries(); + + TableIterator iter = table_.find(k); + if (iter != table_.end()) { + // We set last_use_ upon Release, not during lookup. + Elem* e = iter->second; + if (e->pin == 0) { + pinned_units_ += e->units; + // We are pinning this entry, take it off the LRU list if we are in LRU + // mode. In strict age-based mode entries stay on the list while pinned. + if (lru_ && options.update_eviction_order()) e->unlink(); + } + e->pin++; + return e->value; + } + return nullptr; +} + +template +void SimpleLRUCacheBase::releaseWithOptions( + const Key& k, Value* value, const SimpleLRUCacheOptions& options) { + { // First check to see if this is a deferred value + DeferredTableIterator iter = defer_.find(k); + if (iter != defer_.end()) { + const Elem* const head = iter->second; + // Go from oldest to newest, assuming that oldest entries get released + // first. This may or may not be true and makes no semantic difference. + Elem* e = head->prev; + while (e != head && e->value != value) { + e = e->prev; + } + if (e->value == value) { + // Found in deferred list: release it + assert(e->pin > 0); + e->pin--; + if (e->pin == 0) { + if (e == head) { + // When changing the head, remove the head item and re-insert the + // second item on the list (if there are any left). Do not re-use + // the key from the first item. + // Even though the two keys compare equal, the lifetimes may be + // different (such as a key of Std::StringPiece). + defer_.erase(iter); + if (e->prev != e) { + defer_[e->prev->key] = e->prev; + } + } + e->unlink(); + discard(e); + } + return; + } + } + } + { // Not deferred; so look in hash table + TableIterator iter = table_.find(k); + assert(iter != table_.end()); + Elem* e = iter->second; + assert(e->value == value); + assert(e->pin > 0); + if (lru_ && options.update_eviction_order()) { + e->last_use_ = SimpleCycleTimer::now(); + } + e->pin--; + + if (e->pin == 0) { + if (lru_ && options.update_eviction_order()) e->link(&head_); + pinned_units_ -= e->units; + if (isOverfullInternal()) { + // This element is no longer needed, and we are full. So kick it out. + remove(k); + } + } + } +} + +template +void SimpleLRUCacheBase::insertPinned(const Key& k, + Value* value, + size_t units) { + // Get rid of older entry (if any) from table + remove(k); + + // Make new element + Elem* e = new Elem(k, value, 1, units, SimpleCycleTimer::now()); + + // Adjust table, total units fields. + units_ += units; + pinned_units_ += units; + table_[k] = e; + + // If we are in the strict age-based eviction mode, the entry goes on the LRU + // list now and is never removed. In the LRU mode, the list will only contain + // unpinned entries. + if (!lru_) e->link(&head_); + garbageCollect(); +} + +template +void SimpleLRUCacheBase::updateSize(const Key& k, + const Value* value, + size_t units) { + TableIterator table_iter = table_.find(k); + if ((table_iter != table_.end()) && + ((value == nullptr) || (value == table_iter->second->value))) { + Elem* e = table_iter->second; + units_ -= e->units; + if (e->pin > 0) { + pinned_units_ -= e->units; + } + e->units = units; + units_ += e->units; + if (e->pin > 0) { + pinned_units_ += e->units; + } + } else { + const DeferredTableIterator iter = defer_.find(k); + if (iter != defer_.end()) { + const Elem* const head = iter->second; + Elem* e = iter->second; + do { + if (e->value == value || value == nullptr) { + units_ -= e->units; + e->units = units; + units_ += e->units; + } + e = e->prev; + } while (e != head); + } + } + garbageCollect(); +} + +template +bool SimpleLRUCacheBase::stillInUse( + const Key& k, const Value* value) const { + TableConstIterator iter = table_.find(k); + if ((iter != table_.end()) && + ((value == nullptr) || (value == iter->second->value))) { + return true; + } else { + return inDeferredTable(k, value); + } +} + +template +bool SimpleLRUCacheBase::inDeferredTable( + const Key& k, const Value* value) const { + const DeferredTableConstIterator iter = defer_.find(k); + if (iter != defer_.end()) { + const Elem* const head = iter->second; + const Elem* e = head; + do { + if (e->value == value || value == nullptr) return true; + e = e->prev; + } while (e != head); + } + return false; +} + +template +void SimpleLRUCacheBase::remove(const Key& k) { + TableIterator iter = table_.find(k); + if (iter != table_.end()) { + Elem* e = iter->second; + table_.erase(iter); + remove(e); + } +} + +template +void SimpleLRUCacheBase::remove(Elem* e) { + // Unlink e whether it is in the LRU or the deferred list. It is safe to call + // unlink() if it is not in either list. + e->unlink(); + if (e->pin > 0) { + pinned_units_ -= e->units; + + // Now add it to the deferred table. + DeferredTableIterator iter = defer_.find(e->key); + if (iter == defer_.end()) { + // Inserting a new key, the element becomes the head of the list. + e->prev = e->next = e; + defer_[e->key] = e; + } else { + // There is already a deferred list for this key, attach the element to it + Elem* head = iter->second; + e->link(head); + } + } else { + discard(e); + } +} + +template +void SimpleLRUCacheBase::garbageCollect() { + Elem* e = head_.prev; + while (isOverfullInternal() && (e != &head_)) { + Elem* prev = e->prev; + if (e->pin == 0) { + // Erase from hash-table + TableIterator iter = table_.find(e->key); + assert(iter != table_.end()); + assert(iter->second == e); + table_.erase(iter); + e->unlink(); + discard(e); + } + e = prev; + } +} + +// Not using cycle. Instead using second from time() +static const int kAcceptableClockSynchronizationDriftCycles = 1; + +template +void SimpleLRUCacheBase::discardIdle( + int64_t max_idle) { + if (max_idle < 0) return; + + Elem* e = head_.prev; + const int64_t threshold = SimpleCycleTimer::now() - max_idle; +#ifndef NDEBUG + int64_t last = 0; +#endif + while ((e != &head_) && (e->last_use_ < threshold)) { + // Sanity check: LRU list should be sorted by last_use_. We could + // check the entire list, but that gives quadratic behavior. + // + // TSCs on different cores of multi-core machines sometime get slightly out + // of sync; compensate for this by allowing clock to go backwards by up to + // kAcceptableClockSynchronizationDriftCycles CPU cycles. + // + // A kernel bug (http://b/issue?id=777807) sometimes causes TSCs to become + // widely unsynchronized, in which case this CHECK will fail. As a + // temporary work-around, running + // + // $ sudo bash + // # echo performance>/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor + // # /etc/init.d/cpufrequtils restart + // + // fixes the problem. +#ifndef NDEBUG + assert(last <= e->last_use_ + kAcceptableClockSynchronizationDriftCycles); + last = e->last_use_; +#endif + + Elem* prev = e->prev; + // There are no pinned elements on the list in the LRU mode, and in the + // age-based mode we push them out of the main table regardless of pinning. + assert(e->pin == 0 || !lru_); + remove(e->key); + e = prev; + } +} + +template +void SimpleLRUCacheBase::countDeferredEntries( + int64_t* num_entries, int64_t* total_size) const { + *num_entries = *total_size = 0; + for (DeferredTableConstIterator iter = defer_.begin(); iter != defer_.end(); + ++iter) { + const Elem* const head = iter->second; + const Elem* e = head; + do { + (*num_entries)++; + *total_size += e->units; + e = e->prev; + } while (e != head); + } +} + +template +int64_t SimpleLRUCacheBase::deferredSize() const { + int64_t entries, size; + countDeferredEntries(&entries, &size); + return size; +} + +template +int64_t SimpleLRUCacheBase::deferredEntries() const { + int64_t entries, size; + countDeferredEntries(&entries, &size); + return entries; +} + +template +int64_t SimpleLRUCacheBase::ageOfLRUItemInMicroseconds() const { + if (head_.prev == &head_) return 0; + return kSecToUsec * (SimpleCycleTimer::now() - head_.prev->last_use_) / + SimpleCycleTimer::frequency(); +} + +template +int64_t SimpleLRUCacheBase::getLastUseTime( + const Key& k) const { + // getLastUseTime works only in LRU mode + assert(lru_); + TableConstIterator iter = table_.find(k); + if (iter == table_.end()) return -1; + const Elem* e = iter->second; + return e->last_use_; +} + +template +int64_t SimpleLRUCacheBase::getInsertionTime( + const Key& k) const { + // getInsertionTime works only in age-based mode + assert(!lru_); + TableConstIterator iter = table_.find(k); + if (iter == table_.end()) return -1; + const Elem* e = iter->second; + return e->last_use_; +} + +template +void SimpleLRUCacheBase::debugOutput( + std::string* output) const { + std::stringstream ss; + ss << "SimpleLRUCache of " << table_.size(); + ss << " elements plus " << deferredEntries(); + ss << " deferred elements (" << size(); + ss << " units, " << maxSize() << " max units)"; + *output += ss.str(); + for (TableConstIterator iter = table_.begin(); iter != table_.end(); ++iter) { + const Elem* e = iter->second; + debugIterator(e->value, e->pin, e->last_use_, false, output); + } + *output += "Deferred elements\n"; + for (DeferredTableConstIterator iter = defer_.begin(); iter != defer_.end(); + ++iter) { + const Elem* const head = iter->second; + const Elem* e = head; + do { + debugIterator(e->value, e->pin, e->last_use_, true, output); + e = e->prev; + } while (e != head); + } +} + +// construct an iterator be sure to save a copy of end() as well, so we don't +// update external_view_ in that case. this is b/c if it_ == end(), calling +// it_->first segfaults. we could do this by making sure a specific field in +// it_ is not nullptr but that relies on the internal implementation of it_, so +// we pass in end() instead +template +SimpleLRUCacheConstIterator::SimpleLRUCacheConstIterator( + HashMapConstIterator it, HashMapConstIterator end) + : it_(it), end_(end) { + if (it_ != end_) { + external_view_.first = it_->first; + external_view_.second = it_->second->value; + last_use_ = it_->second->last_use_; + } +} + +template +auto SimpleLRUCacheConstIterator::operator++() + -> SimpleLRUCacheConstIterator& { + it_++; + if (it_ != end_) { + external_view_.first = it_->first; + external_view_.second = it_->second->value; + last_use_ = it_->second->last_use_; + } + return *this; +} + +template +class SimpleLRUCache + : public SimpleLRUCacheBase< + Key, Value, + absl::flat_hash_map*, H, EQ>, + EQ> { + public: + explicit SimpleLRUCache(int64_t total_units) + : SimpleLRUCacheBase< + Key, Value, + absl::flat_hash_map*, H, EQ>, + EQ>(total_units) {} + + protected: + virtual void removeElement(Value* value) { delete value; } + + private: + GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(SimpleLRUCache); +}; + +template +class SimpleLRUCacheWithDeleter + : public SimpleLRUCacheBase< + Key, Value, + absl::flat_hash_map*, H, EQ>, + EQ> { + typedef absl::flat_hash_map*, H, EQ> + HashMap; + typedef SimpleLRUCacheBase Base; + + public: + explicit SimpleLRUCacheWithDeleter(int64_t total_units) + : Base(total_units), deleter_() {} + + SimpleLRUCacheWithDeleter(int64_t total_units, Deleter deleter) + : Base(total_units), deleter_(deleter) {} + + protected: + virtual void removeElement(Value* value) { deleter_(value); } + + private: + Deleter deleter_; + GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(SimpleLRUCacheWithDeleter); +}; + +} // namespace SimpleLruCache +} // namespace JwtVerify +} // namespace Common +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/common/jwt/status.cc b/source/extensions/filters/http/common/jwt/status.cc new file mode 100644 index 0000000000000..b679054f2f322 --- /dev/null +++ b/source/extensions/filters/http/common/jwt/status.cc @@ -0,0 +1,190 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "source/extensions/filters/http/common/jwt/status.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Common { +namespace JwtVerify { + +std::string getStatusString(Status status) { + switch (status) { + case Status::Ok: + return "OK"; + case Status::JwtMissed: + return "Jwt is missing"; + case Status::JwtNotYetValid: + return "Jwt not yet valid"; + case Status::JwtExpired: + return "Jwt is expired"; + case Status::JwtBadFormat: + return "Jwt is not in the form of Header.Payload.Signature with two dots " + "and 3 sections"; + case Status::JwtHeaderParseErrorBadBase64: + return "Jwt header is an invalid Base64url encoded"; + case Status::JwtHeaderParseErrorBadJson: + return "Jwt header is an invalid JSON"; + case Status::JwtHeaderBadAlg: + return "Jwt header [alg] field is required and must be a string"; + case Status::JwtHeaderNotImplementedAlg: + return "Jwt header [alg] is not supported"; + case Status::JwtHeaderBadKid: + return "Jwt header [kid] field is not a string"; + case Status::JwtPayloadParseErrorBadBase64: + return "Jwt payload is an invalid Base64url encoded"; + case Status::JwtEd25519SignatureWrongLength: + return "Jwt ED25519 signature is wrong length"; + case Status::JwtPayloadParseErrorBadJson: + return "Jwt payload is an invalid JSON"; + case Status::JwtPayloadParseErrorIssNotString: + return "Jwt payload [iss] field is not a string"; + case Status::JwtPayloadParseErrorSubNotString: + return "Jwt payload [sub] field is not a string"; + case Status::JwtPayloadParseErrorIatNotInteger: + return "Jwt payload [iat] field is not an integer"; + case Status::JwtPayloadParseErrorIatOutOfRange: + return "Jwt payload [iat] field is not a positive 64 bit integer"; + case Status::JwtPayloadParseErrorNbfNotInteger: + return "Jwt payload [nbf] field is not an integer"; + case Status::JwtPayloadParseErrorNbfOutOfRange: + return "Jwt payload [nbf] field is not a positive 64 bit integer"; + case Status::JwtPayloadParseErrorExpNotInteger: + return "Jwt payload [exp] field is not an integer"; + case Status::JwtPayloadParseErrorExpOutOfRange: + return "Jwt payload [exp] field is not a positive 64 bit integer"; + case Status::JwtPayloadParseErrorJtiNotString: + return "Jwt payload [jti] field is not a string"; + case Status::JwtPayloadParseErrorAudNotString: + return "Jwt payload [aud] field is not a string or string list"; + case Status::JwtSignatureParseErrorBadBase64: + return "Jwt signature is an invalid Base64url encoded"; + case Status::JwtUnknownIssuer: + return "Jwt issuer is not configured"; + case Status::JwtAudienceNotAllowed: + return "Audiences in Jwt are not allowed"; + case Status::JwtVerificationFail: + return "Jwt verification fails"; + case Status::JwtMultipleTokens: + return "Found multiple Jwt tokens"; + + case Status::JwksParseError: + return "Jwks is an invalid JSON"; + case Status::JwksNoKeys: + return "Jwks does not have [keys] field"; + case Status::JwksBadKeys: + return "[keys] in Jwks is not an array"; + case Status::JwksNoValidKeys: + return "Jwks doesn't have any valid public key"; + case Status::JwksKidAlgMismatch: + return "Jwks doesn't have key to match kid or alg from Jwt"; + case Status::JwksRsaParseError: + return "Jwks RSA [n] or [e] field is missing or has a parse error"; + case Status::JwksEcCreateKeyFail: + return "Jwks EC create key fail"; + case Status::JwksEcXorYBadBase64: + return "Jwks EC [x] or [y] field is an invalid Base64."; + case Status::JwksEcParseError: + return "Jwks EC [x] and [y] fields have a parse error."; + case Status::JwksOctBadBase64: + return "Jwks Oct key is an invalid Base64"; + case Status::JwksOKPXBadBase64: + return "Jwks OKP [x] field is an invalid Base64."; + case Status::JwksOKPXWrongLength: + return "Jwks OKP [x] field is wrong length."; + case Status::JwksFetchFail: + return "Jwks remote fetch is failed"; + + case Status::JwksMissingKty: + return "[kty] is missing in [keys]"; + case Status::JwksBadKty: + return "[kty] is bad in [keys]"; + case Status::JwksNotImplementedKty: + return "[kty] is not supported in [keys]"; + + case Status::JwksRSAKeyBadAlg: + return "[alg] is not started with [RS] or [PS] for an RSA key"; + case Status::JwksRSAKeyMissingN: + return "[n] field is missing for a RSA key"; + case Status::JwksRSAKeyBadN: + return "[n] field is not string for a RSA key"; + case Status::JwksRSAKeyMissingE: + return "[e] field is missing for a RSA key"; + case Status::JwksRSAKeyBadE: + return "[e] field is not string for a RSA key"; + + case Status::JwksECKeyBadAlg: + return "[alg] is not started with [ES] for an EC key"; + case Status::JwksECKeyBadCrv: + return "[crv] field is not string for an EC key"; + case Status::JwksECKeyAlgOrCrvUnsupported: + return "[crv] or [alg] field is not supported for an EC key"; + case Status::JwksECKeyAlgNotCompatibleWithCrv: + return "[crv] field specified is not compatible with [alg] for an EC key"; + case Status::JwksECKeyMissingX: + return "[x] field is missing for an EC key"; + case Status::JwksECKeyBadX: + return "[x] field is not string for an EC key"; + case Status::JwksECKeyMissingY: + return "[y] field is missing for an EC key"; + case Status::JwksECKeyBadY: + return "[y] field is not string for an EC key"; + + case Status::JwksHMACKeyBadAlg: + return "[alg] does not start with [HS] for an HMAC key"; + case Status::JwksHMACKeyMissingK: + return "[k] field is missing for an HMAC key"; + case Status::JwksHMACKeyBadK: + return "[k] field is not string for an HMAC key"; + + case Status::JwksOKPKeyBadAlg: + return "[alg] is not [EdDSA] for an OKP key"; + case Status::JwksOKPKeyMissingCrv: + return "[crv] field is missing for an OKP key"; + case Status::JwksOKPKeyBadCrv: + return "[crv] field is not string for an OKP key"; + case Status::JwksOKPKeyCrvUnsupported: + return "[crv] field is not supported for an OKP key"; + case Status::JwksOKPKeyMissingX: + return "[x] field is missing for an OKP key"; + case Status::JwksOKPKeyBadX: + return "[x] field is not string for an OKP key"; + + case Status::JwksX509BioWriteError: + return "X509 parse pubkey internal fails: memory allocation"; + case Status::JwksX509ParseError: + return "X509 parse pubkey fails"; + case Status::JwksX509GetPubkeyError: + return "X509 parse pubkey internal fails: get pubkey"; + + case Status::JwksPemNotImplementedKty: + return "PEM Key type is not supported"; + case Status::JwksPemBadBase64: + return "PEM pubkey parse fails"; + case Status::JwksPemGetRawEd25519Error: + return "PEM failed to get raw ED25519 key"; + + case Status::JwksBioAllocError: + return "Failed to create BIO due to memory allocation failure"; + }; + // Return empty string though switch-case is exhaustive. See issues/91. + return ""; +} + +} // namespace JwtVerify +} // namespace Common +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/common/jwt/status.h b/source/extensions/filters/http/common/jwt/status.h new file mode 100644 index 0000000000000..884a8664fb244 --- /dev/null +++ b/source/extensions/filters/http/common/jwt/status.h @@ -0,0 +1,266 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Common { +namespace JwtVerify { + +/** + * Define the Jwt verification error status. + */ +enum class Status { + Ok = 0, + + // Jwt errors: + + // Jwt missing. + JwtMissed, + + // Jwt not valid yet. + JwtNotYetValid, + + // Jwt expired. + JwtExpired, + + // JWT is not in the form of Header.Payload.Signature + JwtBadFormat, + + // Jwt header is an invalid Base64url encoded. + JwtHeaderParseErrorBadBase64, + + // Jwt header is an invalid JSON. + JwtHeaderParseErrorBadJson, + + // "alg" in the header is not a string. + JwtHeaderBadAlg, + + // Value of "alg" in the header is invalid. + JwtHeaderNotImplementedAlg, + + // "kid" in the header is not a string. + JwtHeaderBadKid, + + // Jwt payload is an invalid Base64url encoded. + JwtPayloadParseErrorBadBase64, + + // Jwt payload is an invalid JSON. + JwtPayloadParseErrorBadJson, + + // Jwt payload field [iss] must be string. + JwtPayloadParseErrorIssNotString, + + // Jwt payload field [sub] must be string. + JwtPayloadParseErrorSubNotString, + + // Jwt payload field [iat] must be integer. + JwtPayloadParseErrorIatNotInteger, + + // Jwt payload field [iat] must be within a 64 bit positive integer range. + JwtPayloadParseErrorIatOutOfRange, + + // Jwt payload field [nbf] must be integer. + JwtPayloadParseErrorNbfNotInteger, + + // Jwt payload field [nbf] must be within a 64 bit positive integer range. + JwtPayloadParseErrorNbfOutOfRange, + + // Jwt payload field [exp] must be integer. + JwtPayloadParseErrorExpNotInteger, + + // Jwt payload field [exp] must be within a 64 bit positive integer range. + JwtPayloadParseErrorExpOutOfRange, + + // Jwt payload field [jti] must be string. + JwtPayloadParseErrorJtiNotString, + + // Jwt payload field [aud] must be string or string list. + JwtPayloadParseErrorAudNotString, + + // Jwt signature is an invalid Base64url input. + JwtSignatureParseErrorBadBase64, + + // Jwt ED25519 signature is wrong length + JwtEd25519SignatureWrongLength, + + // Issuer is not configured. + JwtUnknownIssuer, + + // Audience is not allowed. + JwtAudienceNotAllowed, + + // Jwt verification fails. + JwtVerificationFail, + + // Found multiple Jwt tokens. + JwtMultipleTokens, + + // Jwks errors + + // Jwks is an invalid JSON. + JwksParseError, + + // Jwks does not have "keys". + JwksNoKeys, + + // "keys" in Jwks is not an array. + JwksBadKeys, + + // Jwks doesn't have any valid public key. + JwksNoValidKeys, + + // Jwks doesn't have key to match kid or alg from Jwt. + JwksKidAlgMismatch, + + // "n" or "e" field of a Jwk RSA is missing or has a parse error. + JwksRsaParseError, + + // Failed to create a EC_KEY object. + JwksEcCreateKeyFail, + + // "x" or "y" field is an invalid Base64 + JwksEcXorYBadBase64, + + // "x" or "y" field of a Jwk EC is missing or has a parse error. + JwksEcParseError, + + // Jwks Oct key is an invalid Base64. + JwksOctBadBase64, + + // "x" field is invalid Base64 + JwksOKPXBadBase64, + // "x" field is wrong length + JwksOKPXWrongLength, + + // Failed to fetch public key + JwksFetchFail, + + // "kty" is missing in "keys". + JwksMissingKty, + // "kty" is not string type in "keys". + JwksBadKty, + // "kty" is not supported in "keys". + JwksNotImplementedKty, + + // "alg" is not started with "RS" for a RSA key + JwksRSAKeyBadAlg, + // "n" field is missing for a RSA key + JwksRSAKeyMissingN, + // "n" field is not string for a RSA key + JwksRSAKeyBadN, + // "e" field is missing for a RSA key + JwksRSAKeyMissingE, + // "e" field is not string for a RSA key + JwksRSAKeyBadE, + + // "alg" is not "ES256", "ES384" or "ES512" for an EC key + JwksECKeyBadAlg, + // "crv" field is not string for an EC key + JwksECKeyBadCrv, + // "crv" or "alg" is not supported for an EC key + JwksECKeyAlgOrCrvUnsupported, + // "crv" is not compatible with "alg" for an EC key + JwksECKeyAlgNotCompatibleWithCrv, + // "x" field is missing for an EC key + JwksECKeyMissingX, + // "x" field is not string for an EC key + JwksECKeyBadX, + // "y" field is missing for an EC key + JwksECKeyMissingY, + // "y" field is not string for an EC key + JwksECKeyBadY, + + // "alg" is not "HS256", "HS384" or "HS512" for an HMAC key + JwksHMACKeyBadAlg, + // "k" field is missing for an HMAC key + JwksHMACKeyMissingK, + // "k" field is not string for an HMAC key + JwksHMACKeyBadK, + + // "alg" is not "EdDSA" for an OKP key + JwksOKPKeyBadAlg, + // "crv" field is missing for an OKP key + JwksOKPKeyMissingCrv, + // "crv" field is not string for an OKP key + JwksOKPKeyBadCrv, + // "crv" is not supported for an OKP key + JwksOKPKeyCrvUnsupported, + // "x" field is missing for an OKP key + JwksOKPKeyMissingX, + // "x" field is not string for an OKP key + JwksOKPKeyBadX, + + // X509 BIO_Write function fails + JwksX509BioWriteError, + // X509 parse pubkey fails + JwksX509ParseError, + // X509 get pubkey fails + JwksX509GetPubkeyError, + + // Key type is not supported. + JwksPemNotImplementedKty, + // Unable to parse public key + JwksPemBadBase64, + // Failed to get raw ED25519 key from PEM + JwksPemGetRawEd25519Error, + + // Failed to create BIO + JwksBioAllocError, +}; + +/** + * Convert enum status to string. + * @param status is the enum status. + * @return the string status. + */ +std::string getStatusString(Status status); + +/** + * Base class to keep the status that represents "OK" or the first failure. + */ +class WithStatus { +public: + WithStatus() : status_(Status::Ok) {} + + /** + * Get the current status. + * @return the enum status. + */ + Status getStatus() const { return status_; } + +protected: + void updateStatus(Status status) { + // Only keep the first failure + if (status_ == Status::Ok) { + status_ = status; + } + } + + void resetStatus(Status status) { status_ = status; } + +private: + // The internal status. + Status status_; +}; + +} // namespace JwtVerify +} // namespace Common +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/common/jwt/struct_utils.cc b/source/extensions/filters/http/common/jwt/struct_utils.cc new file mode 100644 index 0000000000000..ec6cfd536177d --- /dev/null +++ b/source/extensions/filters/http/common/jwt/struct_utils.cc @@ -0,0 +1,130 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "source/extensions/filters/http/common/jwt/struct_utils.h" + +#include "absl/strings/str_split.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Common { +namespace JwtVerify { + +StructUtils::StructUtils(const ::google::protobuf::Struct& struct_pb) : struct_pb_(struct_pb) {} + +StructUtils::FindResult StructUtils::GetString(const std::string& name, std::string* str_value) { + const ::google::protobuf::Value* found; + FindResult result = GetValue(name, found); + if (result != OK) { + return result; + } + if (found->kind_case() != google::protobuf::Value::kStringValue) { + return WRONG_TYPE; + } + *str_value = found->string_value(); + return OK; +} + +StructUtils::FindResult StructUtils::GetDouble(const std::string& name, double* double_value) { + const ::google::protobuf::Value* found; + FindResult result = GetValue(name, found); + if (result != OK) { + return result; + } + if (found->kind_case() != google::protobuf::Value::kNumberValue) { + return WRONG_TYPE; + } + *double_value = found->number_value(); + return OK; +} + +StructUtils::FindResult StructUtils::GetUInt64(const std::string& name, uint64_t* int_value) { + double double_value; + FindResult result = GetDouble(name, &double_value); + if (result != OK) { + return result; + } + if (double_value < 0 || + double_value >= static_cast(std::numeric_limits::max())) { + return OUT_OF_RANGE; + } + *int_value = static_cast(double_value); + return OK; +} + +StructUtils::FindResult StructUtils::GetBoolean(const std::string& name, bool* bool_value) { + const ::google::protobuf::Value* found; + FindResult result = GetValue(name, found); + if (result != OK) { + return result; + } + if (found->kind_case() != google::protobuf::Value::kBoolValue) { + return WRONG_TYPE; + } + *bool_value = found->bool_value(); + return OK; +} + +StructUtils::FindResult StructUtils::GetStringList(const std::string& name, + std::vector* list) { + const ::google::protobuf::Value* found; + FindResult result = GetValue(name, found); + if (result != OK) { + return result; + } + if (found->kind_case() == google::protobuf::Value::kStringValue) { + list->push_back(found->string_value()); + return OK; + } + if (found->kind_case() == google::protobuf::Value::kListValue) { + for (const auto& v : found->list_value().values()) { + if (v.kind_case() != google::protobuf::Value::kStringValue) { + return WRONG_TYPE; + } + list->push_back(v.string_value()); + } + return OK; + } + return WRONG_TYPE; +} + +StructUtils::FindResult StructUtils::GetValue(const std::string& nested_names, + const google::protobuf::Value*& found) { + const std::vector name_vector = absl::StrSplit(nested_names, '.'); + + const google::protobuf::Struct* current_struct = &struct_pb_; + for (size_t i = 0; i < name_vector.size(); ++i) { + const auto& fields = current_struct->fields(); + const auto it = fields.find(std::string(name_vector[i])); + if (it == fields.end()) { + return MISSING; + } + if (i == name_vector.size() - 1) { + found = &it->second; + return OK; + } + if (it->second.kind_case() != google::protobuf::Value::kStructValue) { + return WRONG_TYPE; + } + current_struct = &it->second.struct_value(); + } + return MISSING; +} + +} // namespace JwtVerify +} // namespace Common +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/common/jwt/struct_utils.h b/source/extensions/filters/http/common/jwt/struct_utils.h new file mode 100644 index 0000000000000..057858370e6a9 --- /dev/null +++ b/source/extensions/filters/http/common/jwt/struct_utils.h @@ -0,0 +1,62 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "google/protobuf/struct.pb.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Common { +namespace JwtVerify { + +class StructUtils { +public: + StructUtils(const ::google::protobuf::Struct& struct_pb); + + enum FindResult { + OK = 0, + MISSING, + WRONG_TYPE, + OUT_OF_RANGE, + }; + + FindResult GetString(const std::string& name, std::string* str_value); + + // Return error if the JSON value is not within a positive 64 bit integer + // range. The decimals in the JSON value are dropped. + FindResult GetUInt64(const std::string& name, uint64_t* int_value); + + FindResult GetDouble(const std::string& name, double* double_value); + + FindResult GetBoolean(const std::string& name, bool* bool_value); + + // Get string or list of string, designed to get "aud" field + // "aud" can be either string array or string. + // Try as string array, read it as empty array if doesn't exist. + FindResult GetStringList(const std::string& name, std::vector* list); + + // Find the value with nested names. + FindResult GetValue(const std::string& nested_names, const google::protobuf::Value*& found); + +private: + const ::google::protobuf::Struct& struct_pb_; +}; + +} // namespace JwtVerify +} // namespace Common +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/common/jwt/verify.cc b/source/extensions/filters/http/common/jwt/verify.cc new file mode 100644 index 0000000000000..1a5d4f5d2d85a --- /dev/null +++ b/source/extensions/filters/http/common/jwt/verify.cc @@ -0,0 +1,311 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "source/extensions/filters/http/common/jwt/verify.h" + +#include "source/extensions/filters/http/common/jwt/check_audience.h" + +#include "absl/strings/string_view.h" +#include "absl/time/clock.h" +#include "openssl/bn.h" +#include "openssl/curve25519.h" +#include "openssl/ecdsa.h" +#include "openssl/err.h" +#include "openssl/evp.h" +#include "openssl/hmac.h" +#include "openssl/mem.h" +#include "openssl/rsa.h" +#include "openssl/sha.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Common { +namespace JwtVerify { +namespace { + +// A convenience inline cast function. +inline const uint8_t* castToUChar(const absl::string_view& str) { + return reinterpret_cast(str.data()); +} + +bool verifySignatureRSA(RSA* key, const EVP_MD* md, const uint8_t* signature, size_t signature_len, + const uint8_t* signed_data, size_t signed_data_len) { + if (key == nullptr || md == nullptr || signature == nullptr || signed_data == nullptr) { + return false; + } + bssl::UniquePtr evp_pkey(EVP_PKEY_new()); + if (EVP_PKEY_set1_RSA(evp_pkey.get(), key) != 1) { + return false; + } + + bssl::UniquePtr md_ctx(EVP_MD_CTX_create()); + if (EVP_DigestVerifyInit(md_ctx.get(), nullptr, md, nullptr, evp_pkey.get()) == 1) { + if (EVP_DigestVerifyUpdate(md_ctx.get(), signed_data, signed_data_len) == 1) { + if (EVP_DigestVerifyFinal(md_ctx.get(), signature, signature_len) == 1) { + return true; + } + } + } + ERR_clear_error(); + return false; +} + +bool verifySignatureRSA(RSA* key, const EVP_MD* md, absl::string_view signature, + absl::string_view signed_data) { + return verifySignatureRSA(key, md, castToUChar(signature), signature.length(), + castToUChar(signed_data), signed_data.length()); +} + +bool verifySignatureRSAPSS(RSA* key, const EVP_MD* md, const uint8_t* signature, + size_t signature_len, const uint8_t* signed_data, + size_t signed_data_len) { + if (key == nullptr || md == nullptr || signature == nullptr || signed_data == nullptr) { + return false; + } + bssl::UniquePtr evp_pkey(EVP_PKEY_new()); + if (EVP_PKEY_set1_RSA(evp_pkey.get(), key) != 1) { + return false; + } + + bssl::UniquePtr md_ctx(EVP_MD_CTX_create()); + // pctx is owned by md_ctx, no need to free it separately. + EVP_PKEY_CTX* pctx; + if (EVP_DigestVerifyInit(md_ctx.get(), &pctx, md, nullptr, evp_pkey.get()) == 1 && + EVP_PKEY_CTX_set_rsa_padding(pctx, RSA_PKCS1_PSS_PADDING) == 1 && + EVP_PKEY_CTX_set_rsa_mgf1_md(pctx, md) == 1 && + EVP_DigestVerify(md_ctx.get(), signature, signature_len, signed_data, signed_data_len) == 1) { + return true; + } + + ERR_clear_error(); + return false; +} + +bool verifySignatureRSAPSS(RSA* key, const EVP_MD* md, absl::string_view signature, + absl::string_view signed_data) { + return verifySignatureRSAPSS(key, md, castToUChar(signature), signature.length(), + castToUChar(signed_data), signed_data.length()); +} + +bool verifySignatureEC(EC_KEY* key, const EVP_MD* md, const uint8_t* signature, + size_t signature_len, const uint8_t* signed_data, size_t signed_data_len) { + if (key == nullptr || md == nullptr || signature == nullptr || signed_data == nullptr) { + return false; + } + bssl::UniquePtr md_ctx(EVP_MD_CTX_create()); + std::vector digest(EVP_MAX_MD_SIZE); + unsigned int digest_len = 0; + + if (EVP_DigestInit(md_ctx.get(), md) == 0) { + return false; + } + + if (EVP_DigestUpdate(md_ctx.get(), signed_data, signed_data_len) == 0) { + return false; + } + + if (EVP_DigestFinal(md_ctx.get(), digest.data(), &digest_len) == 0) { + return false; + } + + bssl::UniquePtr ecdsa_sig(ECDSA_SIG_new()); + if (!ecdsa_sig) { + return false; + } + + if (BN_bin2bn(signature, signature_len / 2, ecdsa_sig->r) == nullptr || + BN_bin2bn(signature + (signature_len / 2), signature_len / 2, ecdsa_sig->s) == nullptr) { + return false; + } + + if (ECDSA_do_verify(digest.data(), digest_len, ecdsa_sig.get(), key) == 1) { + return true; + } + + ERR_clear_error(); + return false; +} + +bool verifySignatureEC(EC_KEY* key, const EVP_MD* md, absl::string_view signature, + absl::string_view signed_data) { + return verifySignatureEC(key, md, castToUChar(signature), signature.length(), + castToUChar(signed_data), signed_data.length()); +} + +bool verifySignatureOct(const uint8_t* key, size_t key_len, const EVP_MD* md, + const uint8_t* signature, size_t signature_len, const uint8_t* signed_data, + size_t signed_data_len) { + if (key == nullptr || md == nullptr || signature == nullptr || signed_data == nullptr) { + return false; + } + + std::vector out(EVP_MAX_MD_SIZE); + unsigned int out_len = 0; + if (HMAC(md, key, key_len, signed_data, signed_data_len, out.data(), &out_len) == nullptr) { + ERR_clear_error(); + return false; + } + + if (out_len != signature_len) { + return false; + } + + if (CRYPTO_memcmp(out.data(), signature, signature_len) == 0) { + return true; + } + + ERR_clear_error(); + return false; +} + +bool verifySignatureOct(absl::string_view key, const EVP_MD* md, absl::string_view signature, + absl::string_view signed_data) { + return verifySignatureOct(castToUChar(key), key.length(), md, castToUChar(signature), + signature.length(), castToUChar(signed_data), signed_data.length()); +} + +Status verifySignatureEd25519(absl::string_view key, absl::string_view signature, + absl::string_view signed_data) { + if (signature.length() != ED25519_SIGNATURE_LEN) { + return Status::JwtEd25519SignatureWrongLength; + } + + if (ED25519_verify(castToUChar(signed_data), signed_data.length(), castToUChar(signature), + castToUChar(key.data())) == 1) { + return Status::Ok; + } + + ERR_clear_error(); + return Status::JwtVerificationFail; +} + +} // namespace + +Status verifyJwtWithoutTimeChecking(const Jwt& jwt, const Jwks& jwks) { + // Verify signature + std::string signed_data = jwt.header_str_base64url_ + '.' + jwt.payload_str_base64url_; + bool kid_alg_matched = false; + for (const auto& jwk : jwks.keys()) { + // If kid is specified in JWT, JWK with the same kid is used for + // verification. + // If kid is not specified in JWT, try all JWK. + if (!jwt.kid_.empty() && !jwk->kid_.empty() && jwk->kid_ != jwt.kid_) { + continue; + } + + // The same alg must be used. + if (!jwk->alg_.empty() && jwk->alg_ != jwt.alg_) { + continue; + } + kid_alg_matched = true; + + if (jwk->kty_ == "EC") { + const EVP_MD* md; + if (jwt.alg_ == "ES384") { + md = EVP_sha384(); + } else if (jwt.alg_ == "ES512") { + md = EVP_sha512(); + } else { + // default to SHA256 + md = EVP_sha256(); + } + + if (verifySignatureEC(jwk->ec_key_.get(), md, jwt.signature_, signed_data)) { + // Verification succeeded. + return Status::Ok; + } + } else if (jwk->kty_ == "RSA") { + const EVP_MD* md; + if (jwt.alg_ == "RS384" || jwt.alg_ == "PS384") { + md = EVP_sha384(); + } else if (jwt.alg_ == "RS512" || jwt.alg_ == "PS512") { + md = EVP_sha512(); + } else { + // default to SHA256 + md = EVP_sha256(); + } + + if (jwt.alg_.compare(0, 2, "RS") == 0) { + if (verifySignatureRSA(jwk->rsa_.get(), md, jwt.signature_, signed_data)) { + // Verification succeeded. + return Status::Ok; + } + } else if (jwt.alg_.compare(0, 2, "PS") == 0) { + if (verifySignatureRSAPSS(jwk->rsa_.get(), md, jwt.signature_, signed_data)) { + // Verification succeeded. + return Status::Ok; + } + } + } else if (jwk->kty_ == "oct") { + const EVP_MD* md; + if (jwt.alg_ == "HS384") { + md = EVP_sha384(); + } else if (jwt.alg_ == "HS512") { + md = EVP_sha512(); + } else { + // default to SHA256 + md = EVP_sha256(); + } + + if (verifySignatureOct(jwk->hmac_key_, md, jwt.signature_, signed_data)) { + // Verification succeeded. + return Status::Ok; + } + } else if (jwk->kty_ == "OKP" && jwk->crv_ == "Ed25519") { + Status status = verifySignatureEd25519(jwk->okp_key_raw_, jwt.signature_, signed_data); + // For verification failures keep going and try the rest of the keys in + // the JWKS. Otherwise status is either OK or an error with the JWT and we + // can return immediately. + if (status == Status::Ok || status == Status::JwtEd25519SignatureWrongLength) { + return status; + } + } + } + + // Verification failed. + return kid_alg_matched ? Status::JwtVerificationFail : Status::JwksKidAlgMismatch; +} + +Status verifyJwt(const Jwt& jwt, const Jwks& jwks) { + return verifyJwt(jwt, jwks, absl::ToUnixSeconds(absl::Now())); +} + +Status verifyJwt(const Jwt& jwt, const Jwks& jwks, uint64_t now, uint64_t clock_skew) { + Status time_status = jwt.verifyTimeConstraint(now, clock_skew); + if (time_status != Status::Ok) { + return time_status; + } + + return verifyJwtWithoutTimeChecking(jwt, jwks); +} + +Status verifyJwt(const Jwt& jwt, const Jwks& jwks, const std::vector& audiences) { + return verifyJwt(jwt, jwks, audiences, absl::ToUnixSeconds(absl::Now())); +} + +Status verifyJwt(const Jwt& jwt, const Jwks& jwks, const std::vector& audiences, + uint64_t now) { + CheckAudience checker(audiences); + if (!checker.areAudiencesAllowed(jwt.audiences_)) { + return Status::JwtAudienceNotAllowed; + } + return verifyJwt(jwt, jwks, now); +} + +} // namespace JwtVerify +} // namespace Common +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/common/jwt/verify.h b/source/extensions/filters/http/common/jwt/verify.h new file mode 100644 index 0000000000000..a42e378795bf9 --- /dev/null +++ b/source/extensions/filters/http/common/jwt/verify.h @@ -0,0 +1,97 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "source/extensions/filters/http/common/jwt/jwks.h" +#include "source/extensions/filters/http/common/jwt/jwt.h" +#include "source/extensions/filters/http/common/jwt/status.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Common { +namespace JwtVerify { + +/** + * This function verifies JWT signature is valid. + * If verification failed, returns the failure reason. + * Note this method does not verify the "aud" claim. + * @param jwt is Jwt object + * @param jwks is Jwks object + * @return the verification status + */ +Status verifyJwtWithoutTimeChecking(const Jwt& jwt, const Jwks& jwks); + +/** + * This function verifies JWT signature is valid and that it has not expired + * checking the "exp" and "nbf" claims against the system's current wall clock. + * If verification failed, returns the failure reason. + * Note this method does not verify the "aud" claim. + * @param jwt is Jwt object + * @param jwks is Jwks object + * @return the verification status + */ +Status verifyJwt(const Jwt& jwt, const Jwks& jwks); + +/** + * This function verifies JWT signature is valid and that it has not expired + * checking the "exp" and "nbf" claims against the provided time. If + * verification failed, returns the failure reason. Note this method does not + * verify the "aud" claim. + * @param jwt is Jwt object + * @param jwks is Jwks object + * @param now is the number of seconds since the unix epoch + * @param clock_skew is the clock skew in second + * @return the verification status + */ +Status verifyJwt(const Jwt& jwt, const Jwks& jwks, uint64_t now, + uint64_t clock_skew = kClockSkewInSecond); + +/** + * This function verifies JWT signature is valid, that it has not expired + * checking the "exp" and "nbf" claims against the system's current wall clock + * as well as validating that one of the entries in the audience list appears + * as a member in the "aud" claim of the specified JWT. If the supplied + * audience list is empty, no verification of the JWT's "aud" field is + * performed. If verification failed, returns the failure reason. + * @param jwt is Jwt object + * @param jwks is Jwks object + * @param audiences a list of audience by which to check against + * @return the verification status + */ +Status verifyJwt(const Jwt& jwt, const Jwks& jwks, const std::vector& audiences); + +/** + * This function verifies JWT signature is valid, that it has not expired + * checking the "exp" and "nbf" claims against the provided time + * as well as validating that one of the entries in the audience list appears + * as a member in the "aud" claim of the specified JWT. If the supplied + * audience list is empty, no verification of the JWT's "aud" field is + * performed. + * If verification failed, + * returns the failure reason. + * @param jwt is Jwt object + * @param jwks is Jwks object + * @param audiences a list of audience by which to check against. + * @return the verification status + */ +Status verifyJwt(const Jwt& jwt, const Jwks& jwks, const std::vector& audiences, + uint64_t now); + +} // namespace JwtVerify +} // namespace Common +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/gcp_authn/BUILD b/source/extensions/filters/http/gcp_authn/BUILD index c6e0ba93debcd..7986a8e22025d 100644 --- a/source/extensions/filters/http/gcp_authn/BUILD +++ b/source/extensions/filters/http/gcp_authn/BUILD @@ -51,8 +51,8 @@ envoy_cc_library( "//source/common/http:utility_lib", "//source/extensions/filters/http/common:factory_base_lib", "//source/extensions/filters/http/common:pass_through_filter_lib", - "@com_github_google_jwt_verify//:jwt_verify_lib", - "@com_github_google_jwt_verify//:simple_lru_cache_lib", + "//source/extensions/filters/http/common/jwt:jwt_verify_lib", + "//source/extensions/filters/http/common/jwt:simple_lru_cache_lib", "@envoy_api//envoy/extensions/filters/http/gcp_authn/v3:pkg_cc_proto", ], ) diff --git a/source/extensions/filters/http/gcp_authn/gcp_authn_filter.cc b/source/extensions/filters/http/gcp_authn/gcp_authn_filter.cc index c88702c442070..2d35a14ed7fa3 100644 --- a/source/extensions/filters/http/gcp_authn/gcp_authn_filter.cc +++ b/source/extensions/filters/http/gcp_authn/gcp_authn_filter.cc @@ -28,7 +28,7 @@ void addTokenToRequest(Http::RequestHeaderMap& hdrs, absl::string_view token_str } // namespace using ::Envoy::Router::RouteConstSharedPtr; -using ::google::jwt_verify::Status; +using JwtVerify::Status; using Http::FilterHeadersStatus; // TODO(tyxia) Handle the duplicated outstanding requests. @@ -109,8 +109,8 @@ void GcpAuthnFilter::onComplete(const Http::ResponseMessage* response) { ENVOY_LOG(debug, "No request header to be modified."); } // Decode the tokens. - std::unique_ptr<::google::jwt_verify::Jwt> jwt = - std::make_unique<::google::jwt_verify::Jwt>(); + std::unique_ptr jwt = + std::make_unique(); Status status = jwt->parseFromString(token_str); if (status == Status::Ok) { if (jwt_token_cache_ != nullptr) { diff --git a/source/extensions/filters/http/gcp_authn/token_cache.h b/source/extensions/filters/http/gcp_authn/token_cache.h index c0628e8f15884..fd4ee11428bbd 100644 --- a/source/extensions/filters/http/gcp_authn/token_cache.h +++ b/source/extensions/filters/http/gcp_authn/token_cache.h @@ -6,19 +6,21 @@ #include "envoy/extensions/filters/http/gcp_authn/v3/gcp_authn.pb.validate.h" #include "source/extensions/filters/http/common/factory_base.h" - -#include "jwt_verify_lib/jwt.h" -#include "jwt_verify_lib/verify.h" -#include "simple_lru_cache/simple_lru_cache_inl.h" +#include "source/extensions/filters/http/common/jwt/jwt.h" +#include "source/extensions/filters/http/common/jwt/simple_lru_cache_inl.h" +#include "source/extensions/filters/http/common/jwt/verify.h" namespace Envoy { namespace Extensions { namespace HttpFilters { namespace GcpAuthn { +// Namespace alias for backward compatibility. +namespace JwtVerify = Common::JwtVerify; + template -using LRUCache = ::google::simple_lru_cache::SimpleLRUCache; -using JwtToken = ::google::jwt_verify::Jwt; +using LRUCache = JwtVerify::SimpleLruCache::SimpleLRUCache; +using JwtToken = JwtVerify::Jwt; template class TokenCacheImpl : public Logger::Loggable { public: @@ -94,8 +96,8 @@ TokenType* TokenCacheImpl::validateTokenAndReturn(const std::string& // token producer here, we should instead include the clock skew as the part of the `now` time // up front to account for the clock skew on the consumer side where the token will be consumed. if (found_token->verifyTimeConstraint( - DateUtil::nowToSeconds(time_source_) + ::google::jwt_verify::kClockSkewInSecond, - /*clock_skew=*/0) == ::google::jwt_verify::Status::JwtExpired) { + DateUtil::nowToSeconds(time_source_) + JwtVerify::kClockSkewInSecond, + /*clock_skew=*/0) == JwtVerify::Status::JwtExpired) { // Remove the expired entry. lru_cache_.remove(key); } else { diff --git a/source/extensions/filters/http/jwt_authn/BUILD b/source/extensions/filters/http/jwt_authn/BUILD index 1961dde062aca..0b0fbd027e326 100644 --- a/source/extensions/filters/http/jwt_authn/BUILD +++ b/source/extensions/filters/http/jwt_authn/BUILD @@ -42,7 +42,7 @@ envoy_cc_library( "//source/common/protobuf:utility_lib", "//source/common/tracing:http_tracer_lib", "//source/extensions/filters/http/common:jwks_fetcher_lib", - "@com_github_google_jwt_verify//:jwt_verify_lib", + "//source/extensions/filters/http/common/jwt:jwt_verify_lib", "@envoy_api//envoy/extensions/filters/http/jwt_authn/v3:pkg_cc_proto", ], ) @@ -56,7 +56,7 @@ envoy_cc_library( ":jwt_cache_lib", "//source/common/config:datasource_lib", "//source/common/router:retry_policy_lib", - "@com_github_google_jwt_verify//:jwt_verify_lib", + "//source/extensions/filters/http/common/jwt:jwt_verify_lib", "@envoy_api//envoy/extensions/filters/http/jwt_authn/v3:pkg_cc_proto", ], ) @@ -85,7 +85,7 @@ envoy_cc_library( ":matchers_lib", "//envoy/http:filter_interface", "//source/common/http:headers_lib", - "@com_github_google_jwt_verify//:jwt_verify_lib", + "//source/extensions/filters/http/common/jwt:jwt_verify_lib", ], ) @@ -148,8 +148,8 @@ envoy_cc_library( hdrs = ["jwt_cache.h"], deps = [ "//source/common/protobuf:utility_lib", - "@com_github_google_jwt_verify//:jwt_verify_lib", - "@com_github_google_jwt_verify//:simple_lru_cache_lib", + "//source/extensions/filters/http/common/jwt:jwt_verify_lib", + "//source/extensions/filters/http/common/jwt:simple_lru_cache_lib", "@envoy_api//envoy/extensions/filters/http/jwt_authn/v3:pkg_cc_proto", ], ) diff --git a/source/extensions/filters/http/jwt_authn/authenticator.cc b/source/extensions/filters/http/jwt_authn/authenticator.cc index e3501743dde15..4e0060e12b359 100644 --- a/source/extensions/filters/http/jwt_authn/authenticator.cc +++ b/source/extensions/filters/http/jwt_authn/authenticator.cc @@ -11,16 +11,16 @@ #include "source/common/json/json_loader.h" #include "source/common/protobuf/protobuf.h" #include "source/common/tracing/http_tracer_impl.h" +#include "source/extensions/filters/http/common/jwt/jwt.h" +#include "source/extensions/filters/http/common/jwt/struct_utils.h" +#include "source/extensions/filters/http/common/jwt/verify.h" #include "absl/strings/str_split.h" #include "absl/time/time.h" -#include "jwt_verify_lib/jwt.h" -#include "jwt_verify_lib/struct_utils.h" -#include "jwt_verify_lib/verify.h" -using ::google::jwt_verify::CheckAudience; -using ::google::jwt_verify::Status; -using ::google::jwt_verify::StructUtils; +using JwtVerify::CheckAudience; +using JwtVerify::Status; +using JwtVerify::StructUtils; namespace Envoy { namespace Extensions { @@ -57,7 +57,7 @@ class AuthenticatorImpl : public Logger::Loggable, provider_(provider), is_allow_failed_(allow_failed), is_allow_missing_(allow_missing), time_source_(time_source) {} // Following functions are for JwksFetcher::JwksReceiver interface - void onJwksSuccess(google::jwt_verify::JwksPtr&& jwks) override; + void onJwksSuccess(JwtVerify::JwksPtr&& jwks) override; void onJwksError(Failure reason) override; // Following functions are for Authenticator interface. void verify(Http::RequestHeaderMap& headers, Tracing::Span& parent_span, @@ -107,7 +107,7 @@ class AuthenticatorImpl : public Logger::Loggable, std::vector tokens_; JwtLocationConstPtr curr_token_; // The JWT object. - std::unique_ptr<::google::jwt_verify::Jwt> owned_jwt_; + std::unique_ptr<::JwtVerify::Jwt> owned_jwt_; // The JWKS data object JwksCache::JwksData* jwks_data_{}; // The HTTP request headers @@ -129,7 +129,7 @@ class AuthenticatorImpl : public Logger::Loggable, const bool is_allow_failed_; const bool is_allow_missing_; TimeSource& time_source_; - ::google::jwt_verify::Jwt* jwt_{}; + ::JwtVerify::Jwt* jwt_{}; }; std::string AuthenticatorImpl::name() const { @@ -190,7 +190,7 @@ void AuthenticatorImpl::startVerify() { if (!use_jwt_cache) { ENVOY_LOG(debug, "{}: Parse Jwt {}", name(), curr_token_->token()); - owned_jwt_ = std::make_unique<::google::jwt_verify::Jwt>(); + owned_jwt_ = std::make_unique<::JwtVerify::Jwt>(); status = owned_jwt_->parseFromString(curr_token_->token()); jwt_ = owned_jwt_.get(); @@ -223,7 +223,7 @@ void AuthenticatorImpl::startVerify() { } // Default is 60 seconds - uint64_t clock_skew_seconds = ::google::jwt_verify::kClockSkewInSecond; + uint64_t clock_skew_seconds = ::JwtVerify::kClockSkewInSecond; if (jwks_data_->getJwtProvider().clock_skew_seconds() > 0) { clock_skew_seconds = jwks_data_->getJwtProvider().clock_skew_seconds(); } @@ -304,7 +304,7 @@ void AuthenticatorImpl::startVerify() { doneWithStatus(Status::JwksNoValidKeys); } -void AuthenticatorImpl::onJwksSuccess(google::jwt_verify::JwksPtr&& jwks) { +void AuthenticatorImpl::onJwksSuccess(JwtVerify::JwksPtr&& jwks) { jwks_cache_.stats().jwks_fetch_success_.inc(); const Status status = jwks_data_->setRemoteJwks(std::move(jwks))->getStatus(); if (status != Status::Ok) { @@ -328,7 +328,7 @@ void AuthenticatorImpl::onDestroy() { // Verify with a specific public key. void AuthenticatorImpl::verifyKey() { const Status status = - ::google::jwt_verify::verifyJwtWithoutTimeChecking(*jwt_, *jwks_data_->getJwksObj()); + ::JwtVerify::verifyJwtWithoutTimeChecking(*jwt_, *jwks_data_->getJwksObj()); if (status != Status::Ok) { doneWithStatus(status); @@ -452,11 +452,11 @@ void AuthenticatorImpl::setPayloadMetadata(const Protobuf::Struct& jwt_payload) void AuthenticatorImpl::doneWithStatus(const Status& status) { ENVOY_LOG(debug, "{}: JWT verification completed with: {}", name(), - ::google::jwt_verify::getStatusString(status)); + ::JwtVerify::getStatusString(status)); if (Status::Ok != status) { // Forward the failed status to dynamic metadata - ENVOY_LOG(debug, "status is: {}", ::google::jwt_verify::getStatusString(status)); + ENVOY_LOG(debug, "status is: {}", ::JwtVerify::getStatusString(status)); std::string failed_status_in_metadata; @@ -472,9 +472,9 @@ void AuthenticatorImpl::doneWithStatus(const Status& status) { Protobuf::Struct failed_status; auto& failed_status_fields = *failed_status.mutable_fields(); failed_status_fields["code"].set_number_value(enumToInt(status)); - failed_status_fields["message"].set_string_value(google::jwt_verify::getStatusString(status)); + failed_status_fields["message"].set_string_value(JwtVerify::getStatusString(status)); ENVOY_LOG(debug, "Code: {} Message: {}", enumToInt(status), - google::jwt_verify::getStatusString(status)); + JwtVerify::getStatusString(status)); set_extracted_jwt_data_cb_(failed_status_in_metadata, failed_status); } } diff --git a/source/extensions/filters/http/jwt_authn/authenticator.h b/source/extensions/filters/http/jwt_authn/authenticator.h index 45b89d770987f..3c5f85189e6fa 100644 --- a/source/extensions/filters/http/jwt_authn/authenticator.h +++ b/source/extensions/filters/http/jwt_authn/authenticator.h @@ -6,18 +6,21 @@ #include "source/extensions/filters/http/jwt_authn/jwks_cache.h" #include "source/extensions/filters/http/jwt_authn/jwt_cache.h" -#include "jwt_verify_lib/check_audience.h" -#include "jwt_verify_lib/status.h" +#include "source/extensions/filters/http/common/jwt/check_audience.h" +#include "source/extensions/filters/http/common/jwt/status.h" namespace Envoy { namespace Extensions { namespace HttpFilters { namespace JwtAuthn { +// Namespace alias for backward compatibility and readability. +namespace JwtVerify = Common::JwtVerify; + class Authenticator; using AuthenticatorPtr = std::unique_ptr; -using AuthenticatorCallback = std::function; +using AuthenticatorCallback = std::function; using SetExtractedJwtDataCallback = std::function; @@ -42,7 +45,7 @@ class Authenticator { virtual void onDestroy() PURE; // Authenticator factory function. - static AuthenticatorPtr create(const ::google::jwt_verify::CheckAudience* check_audience, + static AuthenticatorPtr create(const JwtVerify::CheckAudience* check_audience, const absl::optional& provider, bool allow_failed, bool allow_missing, JwksCache& jwks_cache, Upstream::ClusterManager& cluster_manager, @@ -58,7 +61,7 @@ class AuthFactory { virtual ~AuthFactory() = default; // Factory method for creating authenticator, and populate it with provider config. - virtual AuthenticatorPtr create(const ::google::jwt_verify::CheckAudience* check_audience, + virtual AuthenticatorPtr create(const JwtVerify::CheckAudience* check_audience, const absl::optional& provider, bool allow_failed, bool allow_missing) const PURE; }; diff --git a/source/extensions/filters/http/jwt_authn/filter.cc b/source/extensions/filters/http/jwt_authn/filter.cc index cddd91f74119c..fda9f2f4353e2 100644 --- a/source/extensions/filters/http/jwt_authn/filter.cc +++ b/source/extensions/filters/http/jwt_authn/filter.cc @@ -2,11 +2,11 @@ #include "source/common/http/headers.h" #include "source/common/http/utility.h" +#include "source/extensions/filters/http/common/jwt/status.h" #include "absl/strings/str_split.h" -#include "jwt_verify_lib/status.h" -using ::google::jwt_verify::Status; +using JwtVerify::Status; namespace Envoy { namespace Extensions { @@ -112,7 +112,7 @@ void Filter::clearRouteCache() { decoder_callbacks_->downstreamCallbacks()->clea void Filter::onComplete(const Status& status) { ENVOY_STREAM_LOG(debug, "Jwt authentication completed with: {}", *decoder_callbacks_, - ::google::jwt_verify::getStatusString(status)); + ::JwtVerify::getStatusString(status)); // This stream has been reset, abort the callback. if (state_ == Responded) { return; @@ -127,11 +127,11 @@ void Filter::onComplete(const Status& status) { if (config_.get()->stripFailureResponse()) { decoder_callbacks_->sendLocalReply( code, "", nullptr, absl::nullopt, - generateRcDetails(::google::jwt_verify::getStatusString(status))); + generateRcDetails(::JwtVerify::getStatusString(status))); return; } decoder_callbacks_->sendLocalReply( - code, ::google::jwt_verify::getStatusString(status), + code, ::JwtVerify::getStatusString(status), [uri = this->original_uri_, status](Http::ResponseHeaderMap& headers) { std::string value = absl::StrCat("Bearer realm=\"", uri, "\""); if (status != Status::JwtMissed) { @@ -139,7 +139,7 @@ void Filter::onComplete(const Status& status) { } headers.setCopy(Http::Headers::get().WWWAuthenticate, value); }, - absl::nullopt, generateRcDetails(::google::jwt_verify::getStatusString(status))); + absl::nullopt, generateRcDetails(::JwtVerify::getStatusString(status))); return; } stats_.allowed_.inc(); diff --git a/source/extensions/filters/http/jwt_authn/filter.h b/source/extensions/filters/http/jwt_authn/filter.h index ef2a6ac574ed1..acaaa20e6e397 100644 --- a/source/extensions/filters/http/jwt_authn/filter.h +++ b/source/extensions/filters/http/jwt_authn/filter.h @@ -35,7 +35,7 @@ class Filter : public Http::StreamDecoderFilter, void setExtractedData(const Protobuf::Struct& extracted_data) override; void clearRouteCache() override; // It will be called when its verify() call is completed. - void onComplete(const ::google::jwt_verify::Status& status) override; + void onComplete(const JwtVerify::Status& status) override; // The callback function. Http::StreamDecoderFilterCallbacks* decoder_callbacks_; diff --git a/source/extensions/filters/http/jwt_authn/filter_config.h b/source/extensions/filters/http/jwt_authn/filter_config.h index 0b4c557cf56b0..1ce5f5f97dde9 100644 --- a/source/extensions/filters/http/jwt_authn/filter_config.h +++ b/source/extensions/filters/http/jwt_authn/filter_config.h @@ -108,7 +108,7 @@ class FilterConfigImpl : public Logger::Loggable, findPerRouteVerifier(const PerRouteFilterConfig& per_route) const override; // methods for AuthFactory interface. Factory method to help create authenticators. - AuthenticatorPtr create(const ::google::jwt_verify::CheckAudience* check_audience, + AuthenticatorPtr create(const JwtVerify::CheckAudience* check_audience, const absl::optional& provider, bool allow_failed, bool allow_missing) const override { return Authenticator::create(check_audience, provider, allow_failed, allow_missing, diff --git a/source/extensions/filters/http/jwt_authn/filter_factory.cc b/source/extensions/filters/http/jwt_authn/filter_factory.cc index 2033a31cab635..05655dce154d0 100644 --- a/source/extensions/filters/http/jwt_authn/filter_factory.cc +++ b/source/extensions/filters/http/jwt_authn/filter_factory.cc @@ -5,13 +5,12 @@ #include "envoy/registry/registry.h" #include "source/common/config/datasource.h" +#include "source/extensions/filters/http/common/jwt/jwks.h" #include "source/extensions/filters/http/jwt_authn/filter.h" -#include "jwt_verify_lib/jwks.h" - using envoy::extensions::filters::http::jwt_authn::v3::JwtAuthentication; -using ::google::jwt_verify::Jwks; -using ::google::jwt_verify::Status; +using JwtVerify::Jwks; +using JwtVerify::Status; namespace Envoy { namespace Extensions { @@ -31,7 +30,7 @@ void validateJwtConfig(const JwtAuthentication& proto_config, Api::Api& api) { if (jwks_obj->getStatus() != Status::Ok) { throw EnvoyException( fmt::format("Provider '{}' in jwt_authn config has invalid local jwks: {}", name, - ::google::jwt_verify::getStatusString(jwks_obj->getStatus()))); + ::JwtVerify::getStatusString(jwks_obj->getStatus()))); } } } diff --git a/source/extensions/filters/http/jwt_authn/jwks_async_fetcher.cc b/source/extensions/filters/http/jwt_authn/jwks_async_fetcher.cc index 95ca054ab3c42..c33cc735fc74f 100644 --- a/source/extensions/filters/http/jwt_authn/jwks_async_fetcher.cc +++ b/source/extensions/filters/http/jwt_authn/jwks_async_fetcher.cc @@ -97,7 +97,7 @@ void JwksAsyncFetcher::handleFetchDone() { } } -void JwksAsyncFetcher::onJwksSuccess(google::jwt_verify::JwksPtr&& jwks) { +void JwksAsyncFetcher::onJwksSuccess(JwtVerify::JwksPtr&& jwks) { done_fn_(std::move(jwks)); handleFetchDone(); refetch_timer_->enableTimer(good_refetch_duration_); diff --git a/source/extensions/filters/http/jwt_authn/jwks_async_fetcher.h b/source/extensions/filters/http/jwt_authn/jwks_async_fetcher.h index aeadf85332881..e5117c05561ca 100644 --- a/source/extensions/filters/http/jwt_authn/jwks_async_fetcher.h +++ b/source/extensions/filters/http/jwt_authn/jwks_async_fetcher.h @@ -15,6 +15,9 @@ namespace Extensions { namespace HttpFilters { namespace JwtAuthn { +// Namespace alias for backward compatibility. +namespace JwtVerify = Common::JwtVerify; + /** * CreateJwksFetcherCb is a callback interface for creating a JwksFetcher instance. */ @@ -24,7 +27,7 @@ using CreateJwksFetcherCb = std::function; +using JwksDoneFetched = std::function; // This class handles fetching Jwks asynchronously. // It will be no-op if async_fetch is not enabled. @@ -50,7 +53,7 @@ class JwksAsyncFetcher : public Logger::Loggable, void handleFetchDone(); // Override the functions from Common::JwksFetcher::JwksReceiver - void onJwksSuccess(google::jwt_verify::JwksPtr&& jwks) override; + void onJwksSuccess(JwtVerify::JwksPtr&& jwks) override; void onJwksError(Failure reason) override; // the remote Jwks config diff --git a/source/extensions/filters/http/jwt_authn/jwks_cache.cc b/source/extensions/filters/http/jwt_authn/jwks_cache.cc index be646452b5f59..5486a2afd3282 100644 --- a/source/extensions/filters/http/jwt_authn/jwks_cache.cc +++ b/source/extensions/filters/http/jwt_authn/jwks_cache.cc @@ -13,17 +13,17 @@ #include "source/common/http/utility.h" #include "source/common/protobuf/message_validator_impl.h" #include "source/common/router/retry_policy_impl.h" +#include "source/extensions/filters/http/common/jwt/check_audience.h" #include "absl/container/node_hash_map.h" #include "absl/strings/string_view.h" #include "absl/time/time.h" #include "absl/types/optional.h" -#include "jwt_verify_lib/check_audience.h" using envoy::extensions::filters::http::jwt_authn::v3::JwtAuthentication; using envoy::extensions::filters::http::jwt_authn::v3::JwtProvider; -using ::google::jwt_verify::Jwks; -using ::google::jwt_verify::Status; +using JwtVerify::Jwks; +using JwtVerify::Status; namespace Envoy { namespace Extensions { @@ -41,7 +41,7 @@ class JwksDataImpl : public JwksCache::JwksData, public Logger::Loggable(audiences); + audiences_ = std::make_unique<::JwtVerify::CheckAudience>(audiences); if (jwt_provider_.has_subjects()) { sub_matcher_.emplace(jwt_provider_.subjects(), context.serverFactoryContext()); @@ -69,7 +69,7 @@ class JwksDataImpl : public JwksCache::JwksData, public Logger::LoggablegetStatus() != Status::Ok) { ENVOY_LOG(warn, "Invalid inline jwks for issuer: {}, jwks: {}", jwt_provider_.issuer(), inline_jwks); @@ -113,7 +113,7 @@ class JwksDataImpl : public JwksCache::JwksData, public Logger::Loggable( jwt_provider_.remote_jwks(), retry_policy_, context, fetcher_cb, stats, - [this](google::jwt_verify::JwksPtr&& jwks) { setJwksToAllThreads(std::move(jwks)); }); + [this](JwtVerify::JwksPtr&& jwks) { setJwksToAllThreads(std::move(jwks)); }); } } @@ -158,7 +158,7 @@ class JwksDataImpl : public JwksCache::JwksData, public Logger::Loggable= tls_->expire_; } - const ::google::jwt_verify::Jwks* setRemoteJwks(JwksConstPtr&& jwks) override { + const ::JwtVerify::Jwks* setRemoteJwks(JwksConstPtr&& jwks) override { // convert unique_ptr to shared_ptr JwksConstSharedPtr shared_jwks = std::move(jwks); tls_->jwks_ = shared_jwks; @@ -198,7 +198,7 @@ class JwksDataImpl : public JwksCache::JwksData, public Logger::Loggable; -using JwksConstPtr = std::unique_ptr; -using JwksConstSharedPtr = std::shared_ptr; +using JwksConstPtr = std::unique_ptr; +using JwksConstSharedPtr = std::shared_ptr; /** * Interface to access all configured Jwt rules and their cached Jwks objects. @@ -70,13 +73,13 @@ class JwksCache { virtual const Router::RetryPolicyConstSharedPtr& retryPolicy() const PURE; // Get the Jwks object. - virtual const ::google::jwt_verify::Jwks* getJwksObj() const PURE; + virtual const JwtVerify::Jwks* getJwksObj() const PURE; // Return true if jwks object is expired. virtual bool isExpired() const PURE; // Set a remote Jwks. - virtual const ::google::jwt_verify::Jwks* setRemoteJwks(JwksConstPtr&& jwks) PURE; + virtual const JwtVerify::Jwks* setRemoteJwks(JwksConstPtr&& jwks) PURE; // Get Token Cache. virtual JwtCache& getJwtCache() PURE; diff --git a/source/extensions/filters/http/jwt_authn/jwt_cache.cc b/source/extensions/filters/http/jwt_authn/jwt_cache.cc index 5e7f73795dbc5..6d2122e911bc9 100644 --- a/source/extensions/filters/http/jwt_authn/jwt_cache.cc +++ b/source/extensions/filters/http/jwt_authn/jwt_cache.cc @@ -3,10 +3,9 @@ #include #include "source/common/common/assert.h" +#include "source/extensions/filters/http/common/jwt/simple_lru_cache_inl.h" -#include "simple_lru_cache/simple_lru_cache_inl.h" - -using ::google::simple_lru_cache::SimpleLRUCache; +using JwtVerify::SimpleLruCache::SimpleLRUCache; namespace Envoy { namespace Extensions { @@ -27,8 +26,7 @@ class JwtCacheImpl : public JwtCache { // if cache_size is 0, it is not specified in the config, use default auto cache_size = config.jwt_cache_size() == 0 ? kJwtCacheDefaultSize : config.jwt_cache_size(); - jwt_lru_cache_ = - std::make_unique>(cache_size); + jwt_lru_cache_ = std::make_unique>(cache_size); max_jwt_size_for_cache_ = config.jwt_max_token_size() == 0 ? kMaxJwtSizeForCache : config.jwt_max_token_size(); } @@ -40,17 +38,16 @@ class JwtCacheImpl : public JwtCache { } } - ::google::jwt_verify::Jwt* lookup(const std::string& token) override { + JwtVerify::Jwt* lookup(const std::string& token) override { if (!jwt_lru_cache_) { return nullptr; } - SimpleLRUCache::ScopedLookup lookup( - jwt_lru_cache_.get(), token); + SimpleLRUCache::ScopedLookup lookup(jwt_lru_cache_.get(), token); if (lookup.found()) { - ::google::jwt_verify::Jwt* const found_jwt = lookup.value(); + JwtVerify::Jwt* const found_jwt = lookup.value(); ASSERT(found_jwt != nullptr); if (found_jwt->verifyTimeConstraint(DateUtil::nowToSeconds(time_source_)) != - ::google::jwt_verify::Status::JwtExpired) { + JwtVerify::Status::JwtExpired) { return found_jwt; } else { jwt_lru_cache_->remove(token); @@ -59,7 +56,7 @@ class JwtCacheImpl : public JwtCache { return nullptr; } - void insert(const std::string& token, std::unique_ptr<::google::jwt_verify::Jwt>&& jwt) override { + void insert(const std::string& token, std::unique_ptr&& jwt) override { if (!jwt_lru_cache_ || token.size() > std::numeric_limits::max()) { return; } @@ -70,7 +67,7 @@ class JwtCacheImpl : public JwtCache { } private: - std::unique_ptr> jwt_lru_cache_; + std::unique_ptr> jwt_lru_cache_; TimeSource& time_source_; uint32_t max_jwt_size_for_cache_; }; diff --git a/source/extensions/filters/http/jwt_authn/jwt_cache.h b/source/extensions/filters/http/jwt_authn/jwt_cache.h index 2fa6d2dad896d..a8cde950dd963 100644 --- a/source/extensions/filters/http/jwt_authn/jwt_cache.h +++ b/source/extensions/filters/http/jwt_authn/jwt_cache.h @@ -6,9 +6,8 @@ #include "envoy/extensions/filters/http/jwt_authn/v3/config.pb.h" #include "source/common/common/utility.h" - -#include "jwt_verify_lib/jwt.h" -#include "jwt_verify_lib/verify.h" +#include "source/extensions/filters/http/common/jwt/jwt.h" +#include "source/extensions/filters/http/common/jwt/verify.h" using envoy::extensions::filters::http::jwt_authn::v3::JwtCacheConfig; @@ -17,6 +16,9 @@ namespace Extensions { namespace HttpFilters { namespace JwtAuthn { +// Namespace alias for backward compatibility. +namespace JwtVerify = Common::JwtVerify; + // Cache key is the JWT string, value is parsed JWT struct. class JwtCache; @@ -28,12 +30,11 @@ class JwtCache { // Lookup a JWT in the cache, if found return the pointer to its parsed jwt struct. // If no found, return nullptr. - virtual ::google::jwt_verify::Jwt* lookup(const std::string& token) PURE; + virtual JwtVerify::Jwt* lookup(const std::string& token) PURE; // Insert a JWT and its parsed JWT struct to the cache. // The function will take over the ownership of jwt object. - virtual void insert(const std::string& token, - std::unique_ptr<::google::jwt_verify::Jwt>&& jwt) PURE; + virtual void insert(const std::string& token, std::unique_ptr&& jwt) PURE; // JwtCache factory function. static JwtCachePtr create(bool enable_cache, const JwtCacheConfig& config, diff --git a/source/extensions/filters/http/jwt_authn/verifier.cc b/source/extensions/filters/http/jwt_authn/verifier.cc index 205e3ac8c6617..32eecb231f0da 100644 --- a/source/extensions/filters/http/jwt_authn/verifier.cc +++ b/source/extensions/filters/http/jwt_authn/verifier.cc @@ -2,14 +2,14 @@ #include "envoy/extensions/filters/http/jwt_authn/v3/config.pb.h" -#include "jwt_verify_lib/check_audience.h" +#include "source/extensions/filters/http/common/jwt/check_audience.h" using envoy::extensions::filters::http::jwt_authn::v3::JwtProvider; using envoy::extensions::filters::http::jwt_authn::v3::JwtRequirement; using envoy::extensions::filters::http::jwt_authn::v3::JwtRequirementAndList; using envoy::extensions::filters::http::jwt_authn::v3::JwtRequirementOrList; -using ::google::jwt_verify::CheckAudience; -using ::google::jwt_verify::Status; +using JwtVerify::CheckAudience; +using JwtVerify::Status; namespace Envoy { namespace Extensions { @@ -153,7 +153,7 @@ class ProviderAndAudienceVerifierImpl : public ProviderVerifierImpl { const CheckAudience* getAudienceChecker() const override { return check_audience_.get(); } // Check audience object - ::google::jwt_verify::CheckAudiencePtr check_audience_; + ::JwtVerify::CheckAudiencePtr check_audience_; }; // Allow missing or failed verifier diff --git a/source/extensions/filters/http/jwt_authn/verifier.h b/source/extensions/filters/http/jwt_authn/verifier.h index b235d3a9319de..c5c24c81265d9 100644 --- a/source/extensions/filters/http/jwt_authn/verifier.h +++ b/source/extensions/filters/http/jwt_authn/verifier.h @@ -44,7 +44,7 @@ class Verifier { * * @param status the status of the request. */ - virtual void onComplete(const ::google::jwt_verify::Status& status) PURE; + virtual void onComplete(const JwtVerify::Status& status) PURE; }; // Context object to hold data needed for verifier. diff --git a/source/extensions/filters/http/oauth2/BUILD b/source/extensions/filters/http/oauth2/BUILD index d2fd760b057e3..e558e8b58a8c1 100644 --- a/source/extensions/filters/http/oauth2/BUILD +++ b/source/extensions/filters/http/oauth2/BUILD @@ -59,7 +59,7 @@ envoy_cc_library( "//source/common/router:retry_policy_lib", "//source/common/secret:secret_provider_impl_lib", "//source/extensions/filters/http/common:pass_through_filter_lib", - "@com_github_google_jwt_verify//:jwt_verify_lib", + "//source/extensions/filters/http/common/jwt:jwt_verify_lib", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", "@envoy_api//envoy/extensions/filters/http/oauth2/v3:pkg_cc_proto", ], diff --git a/source/extensions/filters/http/oauth2/filter.cc b/source/extensions/filters/http/oauth2/filter.cc index 44333bbf734c5..e91d0de4a69ad 100644 --- a/source/extensions/filters/http/oauth2/filter.cc +++ b/source/extensions/filters/http/oauth2/filter.cc @@ -21,14 +21,14 @@ #include "source/common/protobuf/utility.h" #include "source/common/router/retry_policy_impl.h" #include "source/common/runtime/runtime_features.h" +#include "source/extensions/filters/http/common/jwt/jwt.h" +#include "source/extensions/filters/http/common/jwt/status.h" #include "absl/strings/escaping.h" #include "absl/strings/match.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_join.h" #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; @@ -38,6 +38,9 @@ namespace Extensions { namespace HttpFilters { namespace Oauth2 { +// Namespace alias for backward compatibility. +namespace JwtVerify = Common::JwtVerify; + namespace { Http::RegisterCustomInlineHeader authorization_handle(Http::CustomHeaders::get().Authorization); @@ -1118,8 +1121,8 @@ std::string OAuth2Filter::getExpiresTimeForRefreshToken(const std::string& refresh_token, const std::chrono::seconds& expires_in) const { if (config_->useRefreshToken()) { - ::google::jwt_verify::Jwt jwt; - if (jwt.parseFromString(refresh_token) == ::google::jwt_verify::Status::Ok && jwt.exp_ != 0) { + JwtVerify::Jwt jwt; + if (jwt.parseFromString(refresh_token) == JwtVerify::Status::Ok && jwt.exp_ != 0) { const std::chrono::seconds expiration_from_jwt = std::chrono::seconds{jwt.exp_}; const std::chrono::seconds now = std::chrono::time_point_cast(time_source_.systemTime()) @@ -1149,8 +1152,8 @@ OAuth2Filter::getExpiresTimeForRefreshToken(const std::string& refresh_token, std::string OAuth2Filter::getExpiresTimeForIdToken(const std::string& id_token, const std::chrono::seconds& expires_in) const { if (!id_token.empty()) { - ::google::jwt_verify::Jwt jwt; - if (jwt.parseFromString(id_token) == ::google::jwt_verify::Status::Ok && jwt.exp_ != 0) { + JwtVerify::Jwt jwt; + if (jwt.parseFromString(id_token) == JwtVerify::Status::Ok && jwt.exp_ != 0) { const std::chrono::seconds expiration_from_jwt = std::chrono::seconds{jwt.exp_}; const std::chrono::seconds now = std::chrono::time_point_cast(time_source_.systemTime()) From 641b93c713eef19f3e7a6e038f95e10bd874bbdd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 17 Jan 2026 07:20:04 +0000 Subject: [PATCH 3/5] Fix inconsistent namespace qualifier in jwks_cache.cc Co-authored-by: wbpcode <12389633+wbpcode@users.noreply.github.com> --- docs/NAMESPACE_MIGRATION_DIFF.md | 148 ++++++++++++++++++ .../filters/http/jwt_authn/jwks_cache.cc | 8 +- 2 files changed, 152 insertions(+), 4 deletions(-) create mode 100644 docs/NAMESPACE_MIGRATION_DIFF.md diff --git a/docs/NAMESPACE_MIGRATION_DIFF.md b/docs/NAMESPACE_MIGRATION_DIFF.md new file mode 100644 index 0000000000000..f4f73cc3e4127 --- /dev/null +++ b/docs/NAMESPACE_MIGRATION_DIFF.md @@ -0,0 +1,148 @@ +# JWT Verify Library Migration - Namespace Changes + +This document describes the namespace changes made when migrating the code from `github.com/google/jwt_verify_lib` to the Envoy internal implementation. + +## Summary + +The JWT verification library was migrated from an external dependency to internal Envoy code. The main change is the namespace update: + +| Original Namespace | New Envoy Namespace | +|-------------------|---------------------| +| `google::jwt_verify` | `Envoy::Extensions::HttpFilters::Common::JwtVerify` | +| `google::simple_lru_cache` | `Envoy::Extensions::HttpFilters::Common::JwtVerify::SimpleLruCache` | + +## Include Path Changes + +| Original Include | New Include | +|-----------------|-------------| +| `jwt_verify_lib/status.h` | `source/extensions/filters/http/common/jwt/status.h` | +| `jwt_verify_lib/jwt.h` | `source/extensions/filters/http/common/jwt/jwt.h` | +| `jwt_verify_lib/jwks.h` | `source/extensions/filters/http/common/jwt/jwks.h` | +| `jwt_verify_lib/verify.h` | `source/extensions/filters/http/common/jwt/verify.h` | +| `jwt_verify_lib/check_audience.h` | `source/extensions/filters/http/common/jwt/check_audience.h` | +| `jwt_verify_lib/struct_utils.h` | `source/extensions/filters/http/common/jwt/struct_utils.h` | +| `simple_lru_cache/simple_lru_cache.h` | `source/extensions/filters/http/common/jwt/simple_lru_cache.h` | +| `simple_lru_cache/simple_lru_cache_inl.h` | `source/extensions/filters/http/common/jwt/simple_lru_cache_inl.h` | + +## Namespace Declaration Changes + +### Original (google/jwt_verify_lib) + +```cpp +namespace google { +namespace jwt_verify { +// ... code ... +} // namespace jwt_verify +} // namespace google +``` + +### New (Envoy internal) + +```cpp +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Common { +namespace JwtVerify { +// ... code ... +} // namespace JwtVerify +} // namespace Common +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy +``` + +### SimpleLruCache Namespace Change + +#### Original + +```cpp +namespace google { +namespace simple_lru_cache { +// ... code ... +} // namespace simple_lru_cache +} // namespace google +``` + +#### New + +```cpp +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Common { +namespace JwtVerify { +namespace SimpleLruCache { +// ... code ... +} // namespace SimpleLruCache +} // namespace JwtVerify +} // namespace Common +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy +``` + +## Usage in Consumer Code + +Consumers can use namespace aliases for backward compatibility: + +```cpp +// In consumer headers/source files: +namespace JwtVerify = Envoy::Extensions::HttpFilters::Common::JwtVerify; + +// Then use: +JwtVerify::Status status; +JwtVerify::Jwt jwt; +JwtVerify::Jwks jwks; +JwtVerify::SimpleLruCache::SimpleLRUCache cache; +``` + +## Files Migrated + +The following files were migrated from `github.com/google/jwt_verify_lib`: + +### Source Files +- `src/status.cc` -> `source/extensions/filters/http/common/jwt/status.cc` +- `src/jwt.cc` -> `source/extensions/filters/http/common/jwt/jwt.cc` +- `src/jwks.cc` -> `source/extensions/filters/http/common/jwt/jwks.cc` +- `src/verify.cc` -> `source/extensions/filters/http/common/jwt/verify.cc` +- `src/check_audience.cc` -> `source/extensions/filters/http/common/jwt/check_audience.cc` +- `src/struct_utils.cc` -> `source/extensions/filters/http/common/jwt/struct_utils.cc` + +### Header Files +- `jwt_verify_lib/status.h` -> `source/extensions/filters/http/common/jwt/status.h` +- `jwt_verify_lib/jwt.h` -> `source/extensions/filters/http/common/jwt/jwt.h` +- `jwt_verify_lib/jwks.h` -> `source/extensions/filters/http/common/jwt/jwks.h` +- `jwt_verify_lib/verify.h` -> `source/extensions/filters/http/common/jwt/verify.h` +- `jwt_verify_lib/check_audience.h` -> `source/extensions/filters/http/common/jwt/check_audience.h` +- `jwt_verify_lib/struct_utils.h` -> `source/extensions/filters/http/common/jwt/struct_utils.h` +- `simple_lru_cache/simple_lru_cache.h` -> `source/extensions/filters/http/common/jwt/simple_lru_cache.h` +- `simple_lru_cache/simple_lru_cache_inl.h` -> `source/extensions/filters/http/common/jwt/simple_lru_cache_inl.h` + +## Bazel Dependency Changes + +### Removed from `bazel/repository_locations.bzl`: +- `com_github_google_jwt_verify` entry + +### Removed from `bazel/repositories.bzl`: +- `_com_github_google_jwt_verify()` function and its call + +### Removed Patch File: +- `bazel/jwt_verify_lib.patch` + +### New Bazel Targets: +- `//source/extensions/filters/http/common/jwt:jwt_verify_lib` +- `//source/extensions/filters/http/common/jwt:simple_lru_cache_lib` + +### Consumer BUILD file changes: +Replace: +```bazel +"@com_github_google_jwt_verify//:jwt_verify_lib", +"@com_github_google_jwt_verify//:simple_lru_cache_lib", +``` + +With: +```bazel +"//source/extensions/filters/http/common/jwt:jwt_verify_lib", +"//source/extensions/filters/http/common/jwt:simple_lru_cache_lib", +``` diff --git a/source/extensions/filters/http/jwt_authn/jwks_cache.cc b/source/extensions/filters/http/jwt_authn/jwks_cache.cc index 5486a2afd3282..94d888add8934 100644 --- a/source/extensions/filters/http/jwt_authn/jwks_cache.cc +++ b/source/extensions/filters/http/jwt_authn/jwks_cache.cc @@ -41,7 +41,7 @@ class JwksDataImpl : public JwksCache::JwksData, public Logger::Loggable(audiences); + audiences_ = std::make_unique(audiences); if (jwt_provider_.has_subjects()) { sub_matcher_.emplace(jwt_provider_.subjects(), context.serverFactoryContext()); @@ -69,7 +69,7 @@ class JwksDataImpl : public JwksCache::JwksData, public Logger::LoggablegetStatus() != Status::Ok) { ENVOY_LOG(warn, "Invalid inline jwks for issuer: {}, jwks: {}", jwt_provider_.issuer(), inline_jwks); @@ -158,7 +158,7 @@ class JwksDataImpl : public JwksCache::JwksData, public Logger::Loggable= tls_->expire_; } - const ::JwtVerify::Jwks* setRemoteJwks(JwksConstPtr&& jwks) override { + const JwtVerify::Jwks* setRemoteJwks(JwksConstPtr&& jwks) override { // convert unique_ptr to shared_ptr JwksConstSharedPtr shared_jwks = std::move(jwks); tls_->jwks_ = shared_jwks; @@ -198,7 +198,7 @@ class JwksDataImpl : public JwksCache::JwksData, public Logger::Loggable Date: Sat, 17 Jan 2026 07:23:35 +0000 Subject: [PATCH 4/5] Update test files to use new JwtVerify namespace Co-authored-by: wbpcode <12389633+wbpcode@users.noreply.github.com> --- test/extensions/filters/http/common/mock.h | 4 +-- .../http/gcp_authn/token_cache_test.cc | 20 +++++------ .../http/jwt_authn/all_verifier_test.cc | 6 +++- .../http/jwt_authn/authenticator_test.cc | 34 +++++++++++-------- .../filters/http/jwt_authn/filter_test.cc | 7 ++-- .../http/jwt_authn/jwks_async_fetcher_test.cc | 10 +++--- .../filters/http/jwt_authn/jwks_cache_test.cc | 10 ++++-- 7 files changed, 53 insertions(+), 38 deletions(-) diff --git a/test/extensions/filters/http/common/mock.h b/test/extensions/filters/http/common/mock.h index 35d10d1203215..bae66731eec93 100644 --- a/test/extensions/filters/http/common/mock.h +++ b/test/extensions/filters/http/common/mock.h @@ -49,11 +49,11 @@ class MockJwksReceiver : public JwksFetcher::JwksReceiver { * Expectations and assertions should be made on onJwksSuccessImpl in place * of onJwksSuccess. */ - void onJwksSuccess(google::jwt_verify::JwksPtr&& jwks) override { + void onJwksSuccess(JwtVerify::JwksPtr&& jwks) override { ASSERT(jwks); onJwksSuccessImpl(*jwks.get()); } - MOCK_METHOD(void, onJwksSuccessImpl, (const google::jwt_verify::Jwks& jwks)); + MOCK_METHOD(void, onJwksSuccessImpl, (const JwtVerify::Jwks& jwks)); MOCK_METHOD(void, onJwksError, (JwksFetcher::JwksReceiver::Failure reason)); }; diff --git a/test/extensions/filters/http/gcp_authn/token_cache_test.cc b/test/extensions/filters/http/gcp_authn/token_cache_test.cc index 7efa806d8e081..cf1d5213b3172 100644 --- a/test/extensions/filters/http/gcp_authn/token_cache_test.cc +++ b/test/extensions/filters/http/gcp_authn/token_cache_test.cc @@ -65,20 +65,20 @@ class TokenCacheTest : public testing::Test { TestUtility::loadFromYaml(DefaultConfig, config); token_cache_ = std::make_unique>(config.cache_config(), time_system_); - jwt_ = std::make_unique<::google::jwt_verify::Jwt>(); + jwt_ = std::make_unique(); } NiceMock context_; std::unique_ptr> token_cache_; Event::SimulatedTimeSystem time_system_; - std::unique_ptr<::google::jwt_verify::Jwt> jwt_ = nullptr; + std::unique_ptr jwt_ = nullptr; }; TEST_F(TokenCacheTest, ValidToken) { EXPECT_EQ(token_cache_->capacity(), 100); std::string good_token = std::string(GoodTokenStr); - ::google::jwt_verify::Status status = jwt_->parseFromString(good_token); - EXPECT_TRUE(status == ::google::jwt_verify::Status::Ok); + JwtVerify::Status status = jwt_->parseFromString(good_token); + EXPECT_TRUE(status == JwtVerify::Status::Ok); auto* old_jwt = jwt_.get(); token_cache_->insert(good_token, std::move(jwt_)); auto* found_jwt = token_cache_->lookUp(good_token); @@ -88,8 +88,8 @@ TEST_F(TokenCacheTest, ValidToken) { TEST_F(TokenCacheTest, ExpiredToken) { std::string expired_token = std::string(ExpiredToken); - ::google::jwt_verify::Status status = jwt_->parseFromString(expired_token); - EXPECT_TRUE(status == ::google::jwt_verify::Status::Ok); + JwtVerify::Status status = jwt_->parseFromString(expired_token); + EXPECT_TRUE(status == JwtVerify::Status::Ok); token_cache_->insert(expired_token, std::move(jwt_)); auto* found_jwt = token_cache_->lookUp(expired_token); EXPECT_TRUE(found_jwt == nullptr); @@ -100,16 +100,16 @@ TEST_F(TokenCacheTest, TokenWithClockSkew) { // Set the time to exp_time + 1s. // i.e., The expiration time in the token is `Sun May 29 2033 13:36:41 GMT-0400` and the time we // set to is `Sun May 29 2033 13:35:42 GMT-0400`. - const time_t exp_time_with_skew = ExpTime - ::google::jwt_verify::kClockSkewInSecond; + const time_t exp_time_with_skew = ExpTime - JwtVerify::kClockSkewInSecond; time_system_.setSystemTime(std::chrono::system_clock::from_time_t(exp_time_with_skew + 1)); std::string token = std::string(GoodTokenStr); - ::google::jwt_verify::Status status = jwt_->parseFromString(token); - EXPECT_TRUE(status == ::google::jwt_verify::Status::Ok); + JwtVerify::Status status = jwt_->parseFromString(token); + EXPECT_TRUE(status == JwtVerify::Status::Ok); token_cache_->insert(token, std::move(jwt_)); auto* found_jwt = token_cache_->lookUp(token); EXPECT_TRUE(found_jwt == nullptr); - std::unique_ptr<::google::jwt_verify::Jwt> jwt = std::make_unique<::google::jwt_verify::Jwt>(); + std::unique_ptr jwt = std::make_unique(); // Set the time to exp_time - 1s. time_system_.setSystemTime(std::chrono::system_clock::from_time_t(exp_time_with_skew)); auto* old_jwt = jwt.get(); diff --git a/test/extensions/filters/http/jwt_authn/all_verifier_test.cc b/test/extensions/filters/http/jwt_authn/all_verifier_test.cc index 654bf21b2c2e5..be14b44222610 100644 --- a/test/extensions/filters/http/jwt_authn/all_verifier_test.cc +++ b/test/extensions/filters/http/jwt_authn/all_verifier_test.cc @@ -12,13 +12,17 @@ #include "gmock/gmock.h" using envoy::extensions::filters::http::jwt_authn::v3::JwtAuthentication; -using ::google::jwt_verify::Status; using ::testing::NiceMock; namespace Envoy { namespace Extensions { namespace HttpFilters { namespace JwtAuthn { + +// Namespace alias for test code. +namespace JwtVerify = Common::JwtVerify; +using JwtVerify::Status; + namespace { constexpr char kConfigTemplate[] = R"( diff --git a/test/extensions/filters/http/jwt_authn/authenticator_test.cc b/test/extensions/filters/http/jwt_authn/authenticator_test.cc index 96ac8e602d37e..a84129353a2ba 100644 --- a/test/extensions/filters/http/jwt_authn/authenticator_test.cc +++ b/test/extensions/filters/http/jwt_authn/authenticator_test.cc @@ -21,8 +21,6 @@ using envoy::extensions::filters::http::jwt_authn::v3::RemoteJwks; using Envoy::Extensions::HttpFilters::Common::JwksFetcher; using Envoy::Extensions::HttpFilters::Common::JwksFetcherPtr; using Envoy::Extensions::HttpFilters::Common::MockJwksFetcher; -using ::google::jwt_verify::Jwks; -using ::google::jwt_verify::Status; using ::testing::_; using ::testing::Invoke; using ::testing::MockFunction; @@ -33,6 +31,12 @@ namespace Envoy { namespace Extensions { namespace HttpFilters { namespace JwtAuthn { + +// Namespace alias for test code. +namespace JwtVerify = Common::JwtVerify; +using JwtVerify::Jwks; +using JwtVerify::Status; + namespace { class AuthenticatorTest : public testing::Test { @@ -43,7 +47,7 @@ class AuthenticatorTest : public testing::Test { } void createAuthenticator( - ::google::jwt_verify::CheckAudience* check_audience = nullptr, + JwtVerify::CheckAudience* check_audience = nullptr, const absl::optional& provider = absl::make_optional(ProviderName), bool allow_failed = false, bool allow_missing = false) { filter_config_ = std::make_unique(proto_config_, "", mock_factory_ctx_); @@ -63,8 +67,8 @@ class AuthenticatorTest : public testing::Test { void expectVerifyStatus(Status expected_status, Http::RequestHeaderMap& headers, bool expect_clear_route = false) { std::function on_complete_cb = [&expected_status](const Status& status) { - ASSERT_STREQ(google::jwt_verify::getStatusString(status).c_str(), - google::jwt_verify::getStatusString(expected_status).c_str()); + ASSERT_STREQ(JwtVerify::getStatusString(status).c_str(), + JwtVerify::getStatusString(expected_status).c_str()); }; auto set_extracted_jwt_data_cb = [this](const std::string& name, const Protobuf::Struct& extracted_data) { @@ -99,7 +103,7 @@ class AuthenticatorTest : public testing::Test { MockJwksFetcher* raw_fetcher_; JwksFetcherPtr fetcher_; AuthenticatorPtr auth_; - ::google::jwt_verify::JwksPtr jwks_; + JwtVerify::JwksPtr jwks_; Protobuf::Struct out_extracted_data_; NiceMock parent_span_; }; @@ -408,7 +412,7 @@ TEST_F(AuthenticatorTest, TestSetExpiredJwtToGetStatus) { .at("code") .number_value()); - EXPECT_EQ(google::jwt_verify::getStatusString(Status::JwtExpired), out_extracted_data_.fields() + EXPECT_EQ(JwtVerify::getStatusString(Status::JwtExpired), out_extracted_data_.fields() .at("jwt-failure-reason") .struct_value() .fields() @@ -442,7 +446,7 @@ TEST_F(AuthenticatorTest, TestSetInvalidJwtInvalidAudienceToGetStatus) { .at("code") .number_value()); - EXPECT_EQ(google::jwt_verify::getStatusString(Status::JwtAudienceNotAllowed), + EXPECT_EQ(JwtVerify::getStatusString(Status::JwtAudienceNotAllowed), out_extracted_data_.fields() .at("jwt-failure-reason") .struct_value() @@ -475,7 +479,7 @@ TEST_F(AuthenticatorTest, TestSetMissingJwtToGetStatus) { .at("code") .number_value()); - EXPECT_EQ(google::jwt_verify::getStatusString(Status::JwtMissed), out_extracted_data_.fields() + EXPECT_EQ(JwtVerify::getStatusString(Status::JwtMissed), out_extracted_data_.fields() .at("jwt-failure-reason") .struct_value() .fields() @@ -515,7 +519,7 @@ TEST_F(AuthenticatorTest, TestSetInvalidAndValidJwtToGetStatus) { .at("code") .number_value()); - EXPECT_EQ(google::jwt_verify::getStatusString(Status::JwtAudienceNotAllowed), + EXPECT_EQ(JwtVerify::getStatusString(Status::JwtAudienceNotAllowed), out_extracted_data_.fields() .at("jwt-failure-reason") .struct_value() @@ -553,7 +557,7 @@ TEST_F(AuthenticatorTest, TestSetTwoInvalidJwtToGetStatus) { .at("code") .number_value()); - EXPECT_EQ(google::jwt_verify::getStatusString(Status::JwtExpired), out_extracted_data_.fields() + EXPECT_EQ(JwtVerify::getStatusString(Status::JwtExpired), out_extracted_data_.fields() .at("jwt-failure-reason") .struct_value() .fields() @@ -1037,7 +1041,7 @@ TEST_F(AuthenticatorTest, TestAllowFailedMultipleIssuers) { EXPECT_CALL(*raw_fetcher_, fetch(_, _)) .Times(2) .WillRepeatedly(Invoke([](Tracing::Span&, JwksFetcher::JwksReceiver& receiver) { - ::google::jwt_verify::JwksPtr jwks = Jwks::createFrom(PublicKey, Jwks::JWKS); + JwtVerify::JwksPtr jwks = Jwks::createFrom(PublicKey, Jwks::JWKS); EXPECT_TRUE(jwks->getStatus() == Status::Ok); receiver.onJwksSuccess(std::move(jwks)); })); @@ -1057,7 +1061,7 @@ TEST_F(AuthenticatorTest, TestAllowFailedMultipleIssuers) { // Test checks that supplying a CheckAudience to auth will override the one in JwksCache. TEST_F(AuthenticatorTest, TestCustomCheckAudience) { - auto check_audience = std::make_unique<::google::jwt_verify::CheckAudience>( + auto check_audience = std::make_unique( std::vector{"invalid_service"}); createAuthenticator(check_audience.get()); EXPECT_CALL(*raw_fetcher_, fetch(_, _)) @@ -1114,7 +1118,7 @@ class AuthenticatorJwtCacheTest : public testing::Test { on_complete_cb, nullptr); } - ::google::jwt_verify::JwksPtr jwks_; + JwtVerify::JwksPtr jwks_; NiceMock jwks_cache_; MockFunction @@ -1170,7 +1174,7 @@ TEST_F(AuthenticatorJwtCacheTest, TestCacheHit) { createAuthenticator("provider"); - ::google::jwt_verify::Jwt cached_jwt; + JwtVerify::Jwt cached_jwt; cached_jwt.parseFromString(GoodToken); // jwt_cache hit: lookup return a cached jwt. EXPECT_CALL(jwks_cache_.jwks_data_.jwt_cache_, lookup(_)).WillOnce(Return(&cached_jwt)); diff --git a/test/extensions/filters/http/jwt_authn/filter_test.cc b/test/extensions/filters/http/jwt_authn/filter_test.cc index 6870ce1bcb6e9..a75de3d678fc7 100644 --- a/test/extensions/filters/http/jwt_authn/filter_test.cc +++ b/test/extensions/filters/http/jwt_authn/filter_test.cc @@ -8,8 +8,6 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" -using ::google::jwt_verify::Status; - using testing::_; using testing::Invoke; using testing::Return; @@ -19,6 +17,11 @@ namespace Envoy { namespace Extensions { namespace HttpFilters { namespace JwtAuthn { + +// Namespace alias for test code. +namespace JwtVerify = Common::JwtVerify; +using JwtVerify::Status; + namespace { class MockMatcher : public Matcher { diff --git a/test/extensions/filters/http/jwt_authn/jwks_async_fetcher_test.cc b/test/extensions/filters/http/jwt_authn/jwks_async_fetcher_test.cc index 52ec210e6ed38..371f08174a25e 100644 --- a/test/extensions/filters/http/jwt_authn/jwks_async_fetcher_test.cc +++ b/test/extensions/filters/http/jwt_authn/jwks_async_fetcher_test.cc @@ -83,7 +83,7 @@ class JwksAsyncFetcherTest : public testing::TestWithParam { }); }, stats_, - [this](google::jwt_verify::JwksPtr&& jwks) { out_jwks_array_.push_back(std::move(jwks)); }); + [this](JwtVerify::JwksPtr&& jwks) { out_jwks_array_.push_back(std::move(jwks)); }); if (initManagerUsed()) { init_target_handle_->initialize(init_watcher_); @@ -95,7 +95,7 @@ class JwksAsyncFetcherTest : public testing::TestWithParam { NiceMock context_; JwtAuthnFilterStats stats_; std::vector fetch_receiver_array_; - std::vector out_jwks_array_; + std::vector out_jwks_array_; Init::TargetHandlePtr init_target_handle_; NiceMock init_watcher_; @@ -145,7 +145,7 @@ TEST_P(JwksAsyncFetcherTest, TestGoodFetch) { // Trigger the Jwks response EXPECT_EQ(fetch_receiver_array_.size(), 1); - auto jwks = google::jwt_verify::Jwks::createFrom(PublicKey, google::jwt_verify::Jwks::JWKS); + auto jwks = JwtVerify::Jwks::createFrom(PublicKey, JwtVerify::Jwks::JWKS); fetch_receiver_array_[0]->onJwksSuccess(std::move(jwks)); // Output 1 jwks. @@ -198,7 +198,7 @@ TEST_P(JwksAsyncFetcherTest, TestGoodFetchAndRefresh) { setupAsyncFetcher(config); // Initial fetch is successful EXPECT_EQ(fetch_receiver_array_.size(), 1); - auto jwks = google::jwt_verify::Jwks::createFrom(PublicKey, google::jwt_verify::Jwks::JWKS); + auto jwks = JwtVerify::Jwks::createFrom(PublicKey, JwtVerify::Jwks::JWKS); fetch_receiver_array_[0]->onJwksSuccess(std::move(jwks)); // Output 1 jwks. @@ -213,7 +213,7 @@ TEST_P(JwksAsyncFetcherTest, TestGoodFetchAndRefresh) { // refetch again after cache duration interval: successful. EXPECT_EQ(fetch_receiver_array_.size(), 2); - auto jwks1 = google::jwt_verify::Jwks::createFrom(PublicKey, google::jwt_verify::Jwks::JWKS); + auto jwks1 = JwtVerify::Jwks::createFrom(PublicKey, JwtVerify::Jwks::JWKS); fetch_receiver_array_[1]->onJwksSuccess(std::move(jwks1)); // Output 2 jwks. diff --git a/test/extensions/filters/http/jwt_authn/jwks_cache_test.cc b/test/extensions/filters/http/jwt_authn/jwks_cache_test.cc index 7a72c26540184..1de003150ddac 100644 --- a/test/extensions/filters/http/jwt_authn/jwks_cache_test.cc +++ b/test/extensions/filters/http/jwt_authn/jwks_cache_test.cc @@ -15,13 +15,17 @@ using envoy::extensions::filters::http::jwt_authn::v3::JwtAuthentication; using envoy::extensions::filters::http::jwt_authn::v3::RemoteJwks; -using ::google::jwt_verify::Status; using ::testing::MockFunction; namespace Envoy { namespace Extensions { namespace HttpFilters { namespace JwtAuthn { + +// Namespace alias for test code. +namespace JwtVerify = Common::JwtVerify; +using JwtVerify::Status; + namespace { JwtAuthnFilterStats generateMockStats(Stats::Scope& scope) { @@ -36,7 +40,7 @@ class JwksCacheTest : public testing::Test { // fetcher is only called at async_fetch. In this test, it is never called. EXPECT_CALL(mock_fetcher_, Call(_, _, _)).Times(0); setupCache(ExampleConfig); - jwks_ = google::jwt_verify::Jwks::createFrom(PublicKey, google::jwt_verify::Jwks::JWKS); + jwks_ = JwtVerify::Jwks::createFrom(PublicKey, JwtVerify::Jwks::JWKS); } void setupCache(const std::string& config_str) { @@ -47,7 +51,7 @@ class JwksCacheTest : public testing::Test { JwtAuthentication config_; NiceMock context_; JwksCachePtr cache_; - google::jwt_verify::JwksPtr jwks_; + JwtVerify::JwksPtr jwks_; MockFunction mock_fetcher_; From 2a0a89ad4785921d3ce5ab801969512aeb4664c2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 17 Jan 2026 07:25:03 +0000 Subject: [PATCH 5/5] Update remaining test files with proper namespace aliases Co-authored-by: wbpcode <12389633+wbpcode@users.noreply.github.com> --- .../http/jwt_authn/group_verifier_test.cc | 8 ++++++-- .../filters/http/jwt_authn/jwt_cache_test.cc | 11 +++++++---- test/extensions/filters/http/jwt_authn/mock.h | 16 +++++++++------- .../http/jwt_authn/provider_verifier_test.cc | 6 +++++- 4 files changed, 27 insertions(+), 14 deletions(-) diff --git a/test/extensions/filters/http/jwt_authn/group_verifier_test.cc b/test/extensions/filters/http/jwt_authn/group_verifier_test.cc index 97e61234afef7..e9d4842965de4 100644 --- a/test/extensions/filters/http/jwt_authn/group_verifier_test.cc +++ b/test/extensions/filters/http/jwt_authn/group_verifier_test.cc @@ -9,13 +9,17 @@ #include "gmock/gmock.h" using envoy::extensions::filters::http::jwt_authn::v3::JwtAuthentication; -using ::google::jwt_verify::Status; using ::testing::NiceMock; namespace Envoy { namespace Extensions { namespace HttpFilters { namespace JwtAuthn { + +// Namespace alias for test code. +namespace JwtVerify = Common::JwtVerify; +using JwtVerify::Status; + namespace { const char AllWithAny[] = R"( @@ -71,7 +75,7 @@ class GroupVerifierTest : public testing::Test { public: void createVerifier() { ON_CALL(mock_factory_, create(_, _, _, _)) - .WillByDefault(Invoke([&](const ::google::jwt_verify::CheckAudience*, + .WillByDefault(Invoke([&](const JwtVerify::CheckAudience*, const absl::optional& provider, bool, bool) { return std::move(mock_auths_[provider ? provider.value() : allowfailed]); })); diff --git a/test/extensions/filters/http/jwt_authn/jwt_cache_test.cc b/test/extensions/filters/http/jwt_authn/jwt_cache_test.cc index 093226f4784a2..fa5bbc5799175 100644 --- a/test/extensions/filters/http/jwt_authn/jwt_cache_test.cc +++ b/test/extensions/filters/http/jwt_authn/jwt_cache_test.cc @@ -10,12 +10,15 @@ #include "test/test_common/simulated_time_system.h" #include "test/test_common/utility.h" -using ::google::jwt_verify::Status; - namespace Envoy { namespace Extensions { namespace HttpFilters { namespace JwtAuthn { + +// Namespace alias for test code. +namespace JwtVerify = Common::JwtVerify; +using JwtVerify::Status; + namespace { class JwtCacheTest : public testing::Test { @@ -28,14 +31,14 @@ class JwtCacheTest : public testing::Test { } void loadJwt(const char* jwt_str) { - jwt_ = std::make_unique<::google::jwt_verify::Jwt>(); + jwt_ = std::make_unique(); Status status = jwt_->parseFromString(jwt_str); EXPECT_EQ(status, Status::Ok); } Event::SimulatedTimeSystem time_system_; JwtCachePtr cache_; - std::unique_ptr<::google::jwt_verify::Jwt> jwt_; + std::unique_ptr jwt_; }; TEST_F(JwtCacheTest, TestEnabledCache) { diff --git a/test/extensions/filters/http/jwt_authn/mock.h b/test/extensions/filters/http/jwt_authn/mock.h index 835ec8632b381..74d6eb00b7a58 100644 --- a/test/extensions/filters/http/jwt_authn/mock.h +++ b/test/extensions/filters/http/jwt_authn/mock.h @@ -11,17 +11,19 @@ #include "absl/strings/string_view.h" #include "gmock/gmock.h" -using ::google::jwt_verify::Status; - namespace Envoy { namespace Extensions { namespace HttpFilters { namespace JwtAuthn { +// Namespace alias for test code. +namespace JwtVerify = Common::JwtVerify; +using JwtVerify::Status; + class MockAuthFactory : public AuthFactory { public: MOCK_METHOD(AuthenticatorPtr, create, - (const ::google::jwt_verify::CheckAudience*, const absl::optional&, bool, + (const JwtVerify::CheckAudience*, const absl::optional&, bool, bool), (const)); }; @@ -66,8 +68,8 @@ class MockExtractor : public Extractor { class MockJwtCache : public JwtCache { public: - MOCK_METHOD(::google::jwt_verify::Jwt*, lookup, (const std::string&), ()); - MOCK_METHOD(void, insert, (const std::string&, std::unique_ptr<::google::jwt_verify::Jwt>&&), ()); + MOCK_METHOD(JwtVerify::Jwt*, lookup, (const std::string&), ()); + MOCK_METHOD(void, insert, (const std::string&, std::unique_ptr&&), ()); }; class MockJwksData : public JwksCache::JwksData { @@ -88,9 +90,9 @@ class MockJwksData : public JwksCache::JwksData { MOCK_METHOD(const envoy::extensions::filters::http::jwt_authn::v3::JwtProvider&, getJwtProvider, (), (const)); MOCK_METHOD(const Router::RetryPolicyConstSharedPtr&, retryPolicy, (), (const)); - MOCK_METHOD(const ::google::jwt_verify::Jwks*, getJwksObj, (), (const)); + MOCK_METHOD(const JwtVerify::Jwks*, getJwksObj, (), (const)); MOCK_METHOD(bool, isExpired, (), (const)); - MOCK_METHOD(const ::google::jwt_verify::Jwks*, setRemoteJwks, (JwksConstPtr&&), ()); + MOCK_METHOD(const JwtVerify::Jwks*, setRemoteJwks, (JwksConstPtr&&), ()); MOCK_METHOD(JwtCache&, getJwtCache, (), ()); envoy::extensions::filters::http::jwt_authn::v3::JwtProvider jwt_provider_; diff --git a/test/extensions/filters/http/jwt_authn/provider_verifier_test.cc b/test/extensions/filters/http/jwt_authn/provider_verifier_test.cc index 0c27591c6f98a..367069bd16bfc 100644 --- a/test/extensions/filters/http/jwt_authn/provider_verifier_test.cc +++ b/test/extensions/filters/http/jwt_authn/provider_verifier_test.cc @@ -12,7 +12,6 @@ using envoy::extensions::filters::http::jwt_authn::v3::JwtAuthentication; using envoy::extensions::filters::http::jwt_authn::v3::JwtRequirement; -using ::google::jwt_verify::Status; using ::testing::Eq; using ::testing::NiceMock; @@ -20,6 +19,11 @@ namespace Envoy { namespace Extensions { namespace HttpFilters { namespace JwtAuthn { + +// Namespace alias for test code. +namespace JwtVerify = Common::JwtVerify; +using JwtVerify::Status; + namespace { Protobuf::Struct getExpectedPayload(const std::string& name) {