diff --git a/api/envoy/config/filter/http/jwt_authn/v2alpha/config.proto b/api/envoy/config/filter/http/jwt_authn/v2alpha/config.proto index 1350070f6806b..85e134bcc82d3 100644 --- a/api/envoy/config/filter/http/jwt_authn/v2alpha/config.proto +++ b/api/envoy/config/filter/http/jwt_authn/v2alpha/config.proto @@ -7,6 +7,7 @@ import "envoy/api/v2/core/base.proto"; import "envoy/api/v2/core/http_uri.proto"; import "envoy/api/v2/route/route.proto"; import "google/protobuf/duration.proto"; +import "google/protobuf/empty.proto"; import "google/protobuf/wrappers.proto"; import "validate/validate.proto"; @@ -30,7 +31,6 @@ import "validate/validate.proto"; // cache_duration: // seconds: 300 // -// [#not-implemented-hide:] message JwtProvider { // Identifies the principal that issued the JWT. See `here // `_. Usually a URL or an email address. @@ -253,7 +253,7 @@ message JwtRequirement { // verification fails. A typical usage is: this filter is used to only verify // JWTs and pass the verified JWT payloads to another filter, the other filter // will make decision. In this mode, all JWT tokens will be verified. - google.protobuf.BoolValue allow_missing_or_failed = 5; + google.protobuf.Empty allow_missing_or_failed = 5; } } @@ -350,7 +350,6 @@ message RequirementRule { // - provider_name: "provider1" // - provider_name: "provider2" // -//// [#not-implemented-hide:] message JwtAuthentication { // Map of provider names to JwtProviders. // diff --git a/docs/root/intro/version_history.rst b/docs/root/intro/version_history.rst index 6ecdc64c73402..b0a22d9d0dd5c 100644 --- a/docs/root/intro/version_history.rst +++ b/docs/root/intro/version_history.rst @@ -55,6 +55,7 @@ Version history dynamic table size of both: encoder and decoder. * http: added support for removing request headers using :ref:`request_headers_to_remove `. +* jwt-authn filter: add support for per route JWT requirements. * listeners: added the ability to match :ref:`FilterChain ` using :ref:`destination_port ` and :ref:`prefix_ranges `. diff --git a/source/extensions/filters/http/jwt_authn/BUILD b/source/extensions/filters/http/jwt_authn/BUILD index d7e1b572fcfbb..c970bbe654aee 100644 --- a/source/extensions/filters/http/jwt_authn/BUILD +++ b/source/extensions/filters/http/jwt_authn/BUILD @@ -36,10 +36,7 @@ envoy_cc_library( envoy_cc_library( name = "authenticator_lib", srcs = ["authenticator.cc"], - hdrs = [ - "authenticator.h", - "filter_config.h", - ], + hdrs = ["authenticator.h"], deps = [ ":extractor_lib", ":jwks_cache_lib", @@ -55,7 +52,8 @@ envoy_cc_library( srcs = ["filter.cc"], hdrs = ["filter.h"], deps = [ - ":authenticator_lib", + ":filter_config_interface", + ":matchers_lib", "//include/envoy/http:filter_interface", "//source/extensions/filters/http:well_known_names", ], @@ -72,3 +70,31 @@ envoy_cc_library( "//source/extensions/filters/http/common:factory_base_lib", ], ) + +envoy_cc_library( + name = "matchers_lib", + srcs = ["matcher.cc"], + hdrs = ["matcher.h"], + deps = [ + ":verifier_lib", + "//source/common/http:header_utility_lib", + "//source/common/router:config_lib", + ], +) + +envoy_cc_library( + name = "verifier_lib", + srcs = ["verifier.cc"], + hdrs = ["verifier.h"], + deps = [ + ":authenticator_lib", + ":extractor_lib", + "//include/envoy/http:header_map_interface", + "@envoy_api//envoy/config/filter/http/jwt_authn/v2alpha:jwt_authn_cc", + ], +) + +envoy_cc_library( + name = "filter_config_interface", + hdrs = ["filter_config.h"], +) diff --git a/source/extensions/filters/http/jwt_authn/authenticator.cc b/source/extensions/filters/http/jwt_authn/authenticator.cc index da5b05300e600..234d8279643a3 100644 --- a/source/extensions/filters/http/jwt_authn/authenticator.cc +++ b/source/extensions/filters/http/jwt_authn/authenticator.cc @@ -11,6 +11,8 @@ #include "jwt_verify_lib/jwt.h" #include "jwt_verify_lib/verify.h" +using ::envoy::config::filter::http::jwt_authn::v2alpha::JwtProvider; +using ::google::jwt_verify::CheckAudience; using ::google::jwt_verify::Status; namespace Envoy { @@ -20,24 +22,29 @@ namespace JwtAuthn { namespace { /** - * Object to implement Authenticator interface. It only processes one token. + * Object to implement Authenticator interface. */ class AuthenticatorImpl : public Logger::Loggable, public Authenticator, public Common::JwksFetcher::JwksReceiver { public: - AuthenticatorImpl(FilterConfigSharedPtr config, CreateJwksFetcherCb createJwksFetcherCb) - : config_(config), createJwksFetcherCb_(createJwksFetcherCb) {} + AuthenticatorImpl(const CheckAudience* check_audience, + const absl::optional& provider, bool allow_failed, + JwksCache& jwks_cache, Upstream::ClusterManager& cluster_manager, + CreateJwksFetcherCb create_jwks_fetcher_cb, TimeSource& time_source) + : jwks_cache_(jwks_cache), cm_(cluster_manager), + create_jwks_fetcher_cb_(create_jwks_fetcher_cb), check_audience_(check_audience), + provider_(provider), is_allow_failed_(allow_failed), time_source_(time_source) {} // Following functions are for JwksFetcher::JwksReceiver interface void onJwksSuccess(google::jwt_verify::JwksPtr&& jwks) override; void onJwksError(Failure reason) override; // Following functions are for Authenticator interface - void verify(Http::HeaderMap& headers, Authenticator::Callbacks* callback) override; + void verify(Http::HeaderMap& headers, std::vector&& tokens, + AuthenticatorCallback callback) override; void onDestroy() override; - void sanitizePayloadHeaders(Http::HeaderMap& headers) const override; - TimeSource& timeSource() { return config_->timeSource(); } + TimeSource& timeSource() { return time_source_; } private: // Verify with a specific public key. @@ -46,69 +53,71 @@ class AuthenticatorImpl : public Logger::Loggable, // Calls the callback with status. void doneWithStatus(const Status& status); - // Return true if it is OK to forward this request without JWT. - bool okToBypass() const; + // Start verification process. It will continue to eliminate tokens with invalid claims until it + // finds one to verify with key. + void startVerify(); - // The config object. - FilterConfigSharedPtr config_; + // The jwks cache object. + JwksCache& jwks_cache_; + // the cluster manager object. + Upstream::ClusterManager& cm_; // The callback used to create a JwksFetcher instance. - CreateJwksFetcherCb createJwksFetcherCb_; + CreateJwksFetcherCb create_jwks_fetcher_cb_; // The Jwks fetcher object Common::JwksFetcherPtr fetcher_; // The token data - JwtLocationConstPtr token_; + std::vector tokens_; + JwtLocationConstPtr curr_token_; // The JWT object. - ::google::jwt_verify::Jwt jwt_; + std::unique_ptr<::google::jwt_verify::Jwt> jwt_; // The JWKS data object JwksCache::JwksData* jwks_data_{}; // The HTTP request headers Http::HeaderMap* headers_{}; // The on_done function. - Authenticator::Callbacks* callback_{}; + AuthenticatorCallback callback_; + // check audience object. + const CheckAudience* check_audience_; + // specific provider or not when it is allow missing or failed. + const absl::optional provider_; + const bool is_allow_failed_; + TimeSource& time_source_; }; -void AuthenticatorImpl::sanitizePayloadHeaders(Http::HeaderMap& headers) const { - for (const auto& it : config_->getProtoConfig().providers()) { - const auto& provider = it.second; - if (!provider.forward_payload_header().empty()) { - headers.remove(Http::LowerCaseString(provider.forward_payload_header())); - } - } -} - -void AuthenticatorImpl::verify(Http::HeaderMap& headers, Authenticator::Callbacks* callback) { +void AuthenticatorImpl::verify(Http::HeaderMap& headers, std::vector&& tokens, + AuthenticatorCallback callback) { ASSERT(!callback_); headers_ = &headers; - callback_ = callback; + tokens_ = std::move(tokens); + callback_ = std::move(callback); ENVOY_LOG(debug, "Jwt authentication starts"); - auto tokens = config_->getExtractor().extract(headers); - if (tokens.empty()) { - if (okToBypass()) { - doneWithStatus(Status::Ok); - } else { - doneWithStatus(Status::JwtMissed); - } + if (tokens_.empty()) { + doneWithStatus(Status::JwtMissed); return; } - // TODO(qiwzhang), add supports for multiple tokens. - // Only process the first token for now. - token_.swap(tokens[0]); + startVerify(); +} - const Status status = jwt_.parseFromString(token_->token()); +void AuthenticatorImpl::startVerify() { + ASSERT(!tokens_.empty()); + curr_token_ = std::move(tokens_.back()); + tokens_.pop_back(); + jwt_ = std::make_unique<::google::jwt_verify::Jwt>(); + const Status status = jwt_->parseFromString(curr_token_->token()); if (status != Status::Ok) { doneWithStatus(status); return; } - // Check if token is extracted from the location specified by the issuer. - if (!token_->isIssuerSpecified(jwt_.iss_)) { - ENVOY_LOG(debug, "Jwt for issuer {} is not extracted from the specified locations", jwt_.iss_); + // Check if token extracted from the location contains the issuer specified by config. + if (!curr_token_->isIssuerSpecified(jwt_->iss_)) { + ENVOY_LOG(debug, "Jwt issuer {} does not match required", jwt_->iss_); doneWithStatus(Status::JwtUnknownIssuer); return; } @@ -123,24 +132,27 @@ void AuthenticatorImpl::verify(Http::HeaderMap& headers, Authenticator::Callback .count(); // If the nbf claim does *not* appear in the JWT, then the nbf field is defaulted // to 0. - if (jwt_.nbf_ > unix_timestamp) { + if (jwt_->nbf_ > unix_timestamp) { doneWithStatus(Status::JwtNotYetValid); return; } // If the exp claim does *not* appear in the JWT then the exp field is defaulted // to 0. - if (jwt_.exp_ > 0 && jwt_.exp_ < unix_timestamp) { + if (jwt_->exp_ > 0 && jwt_->exp_ < unix_timestamp) { doneWithStatus(Status::JwtExpired); return; } // Check the issuer is configured or not. - jwks_data_ = config_->getCache().getJwksCache().findByIssuer(jwt_.iss_); + jwks_data_ = provider_ ? jwks_cache_.findByProvider(provider_.value()) + : jwks_cache_.findByIssuer(jwt_->iss_); // isIssuerSpecified() check already make sure the issuer is in the cache. ASSERT(jwks_data_ != nullptr); // Check if audience is allowed - if (!jwks_data_->areAudiencesAllowed(jwt_.audiences_)) { + bool is_allowed = check_audience_ ? check_audience_->areAudiencesAllowed(jwt_->audiences_) + : jwks_data_->areAudiencesAllowed(jwt_->audiences_); + if (!is_allowed) { doneWithStatus(Status::JwtAudienceNotAllowed); return; } @@ -158,20 +170,21 @@ void AuthenticatorImpl::verify(Http::HeaderMap& headers, Authenticator::Callback return; } - // TODO(qiwzhang): potential optimization. - // If request 1 triggers a remote jwks fetching, but is not yet replied when the request 2 - // of using the same jwks comes. The request 2 will trigger another remote fetching for the - // jwks. This can be optimized; the same remote jwks fetching can be shared by two requrests. + // TODO(potatop): potential optimization. + // Only one remote jwks will be fetched, verify will not continue util it is completed. This is + // fine for provider name requirements, as each provider has only one issuer, but for allow + // missing or failed there can be more than one issuers. This can be optimized; the same remote + // jwks fetching can be shared by two requests. if (jwks_data_->getJwtProvider().has_remote_jwks()) { if (!fetcher_) { - fetcher_ = createJwksFetcherCb_(config_->cm()); + fetcher_ = create_jwks_fetcher_cb_(cm_); } fetcher_->fetch(jwks_data_->getJwtProvider().remote_jwks().http_uri(), *this); - } else { - // No valid keys for this issuer. This may happen as a result of incorrect local - // JWKS configuration. - doneWithStatus(Status::JwksNoValidKeys); + return; } + // No valid keys for this issuer. This may happen as a result of incorrect local + // JWKS configuration. + doneWithStatus(Status::JwksNoValidKeys); } void AuthenticatorImpl::onJwksSuccess(google::jwt_verify::JwksPtr&& jwks) { @@ -193,7 +206,7 @@ void AuthenticatorImpl::onDestroy() { // Verify with a specific public key. void AuthenticatorImpl::verifyKey() { - const Status status = ::google::jwt_verify::verifyJwt(jwt_, *jwks_data_->getJwksObj()); + const Status status = ::google::jwt_verify::verifyJwt(*jwt_, *jwks_data_->getJwksObj()); if (status != Status::Ok) { doneWithStatus(status); return; @@ -203,34 +216,41 @@ void AuthenticatorImpl::verifyKey() { const auto& provider = jwks_data_->getJwtProvider(); if (!provider.forward_payload_header().empty()) { headers_->addCopy(Http::LowerCaseString(provider.forward_payload_header()), - jwt_.payload_str_base64url_); + jwt_->payload_str_base64url_); } if (!provider.forward()) { + // TODO(potatop) remove JWT from queries. // Remove JWT from headers. - token_->removeJwt(*headers_); + curr_token_->removeJwt(*headers_); } doneWithStatus(Status::Ok); } -bool AuthenticatorImpl::okToBypass() const { - // TODO(qiwzhang): use requirement field - return false; -} - void AuthenticatorImpl::doneWithStatus(const Status& status) { - ENVOY_LOG(debug, "Jwt authentication completed with: {}", - ::google::jwt_verify::getStatusString(status)); - callback_->onComplete(status); - callback_ = nullptr; + // if on allow missing or failed this should verify all tokens, otherwise stop on ok. + if ((Status::Ok == status && !is_allow_failed_) || tokens_.empty()) { + tokens_.clear(); + ENVOY_LOG(debug, "Jwt authentication completed with: {}", + ::google::jwt_verify::getStatusString(status)); + callback_(is_allow_failed_ ? Status::Ok : status); + callback_ = nullptr; + return; + } + startVerify(); } } // namespace -AuthenticatorPtr Authenticator::create(FilterConfigSharedPtr config, - CreateJwksFetcherCb createJwksFetcherCb) { - return std::make_unique(config, createJwksFetcherCb); +AuthenticatorPtr Authenticator::create(const CheckAudience* check_audience, + const absl::optional& provider, + bool allow_failed, JwksCache& jwks_cache, + Upstream::ClusterManager& cluster_manager, + CreateJwksFetcherCb create_jwks_fetcher_cb, + TimeSource& time_source) { + return std::make_unique(check_audience, provider, allow_failed, jwks_cache, + cluster_manager, create_jwks_fetcher_cb, time_source); } } // namespace JwtAuthn diff --git a/source/extensions/filters/http/jwt_authn/authenticator.h b/source/extensions/filters/http/jwt_authn/authenticator.h index 937f6c3f5aab1..76e8f96c05dec 100644 --- a/source/extensions/filters/http/jwt_authn/authenticator.h +++ b/source/extensions/filters/http/jwt_authn/authenticator.h @@ -1,9 +1,12 @@ #pragma once -#include + +#include "envoy/server/filter_config.h" #include "extensions/filters/http/common/jwks_fetcher.h" -#include "extensions/filters/http/jwt_authn/filter_config.h" +#include "extensions/filters/http/jwt_authn/extractor.h" +#include "extensions/filters/http/jwt_authn/jwks_cache.h" +#include "jwt_verify_lib/check_audience.h" #include "jwt_verify_lib/status.h" namespace Envoy { @@ -14,6 +17,8 @@ namespace JwtAuthn { class Authenticator; typedef std::unique_ptr AuthenticatorPtr; +typedef std::function AuthenticatorCallback; + /** * CreateJwksFetcherCb is a callback interface for creating a JwksFetcher instance. */ @@ -26,23 +31,33 @@ class Authenticator { public: virtual ~Authenticator() {} - // The callback interface to notify the completion event. - class Callbacks { - public: - virtual ~Callbacks() {} - virtual void onComplete(const ::google::jwt_verify::Status& status) PURE; - }; - virtual void verify(Http::HeaderMap& headers, Callbacks* callback) PURE; + // Verify if headers satisfyies the JWT requirements. Can be limited to single provider with + // extract_param. + virtual void verify(Http::HeaderMap& headers, std::vector&& tokens, + AuthenticatorCallback callback) PURE; // Called when the object is about to be destroyed. virtual void onDestroy() PURE; - // Remove headers that configured to send JWT payloads - virtual void sanitizePayloadHeaders(Http::HeaderMap& headers) const PURE; - // Authenticator factory function. - static AuthenticatorPtr create(FilterConfigSharedPtr config, - CreateJwksFetcherCb createJwksFetcherCb); + static AuthenticatorPtr create(const ::google::jwt_verify::CheckAudience* check_audience, + const absl::optional& provider, bool allow_failed, + JwksCache& jwks_cache, Upstream::ClusterManager& cluster_manager, + CreateJwksFetcherCb create_jwks_fetcher_cb, + TimeSource& time_source); +}; + +/** + * Interface for authenticator factory. + */ +class AuthFactory { +public: + virtual ~AuthFactory() {} + + // Factory method for creating authenticator, and populate it with provider config. + virtual AuthenticatorPtr create(const ::google::jwt_verify::CheckAudience* check_audience, + const absl::optional& provider, + bool allow_failed) const PURE; }; } // namespace JwtAuthn diff --git a/source/extensions/filters/http/jwt_authn/extractor.cc b/source/extensions/filters/http/jwt_authn/extractor.cc index 452232b67423c..1e5f6c0e7ab73 100644 --- a/source/extensions/filters/http/jwt_authn/extractor.cc +++ b/source/extensions/filters/http/jwt_authn/extractor.cc @@ -6,6 +6,8 @@ #include "common/singleton/const_singleton.h" using ::envoy::config::filter::http::jwt_authn::v2alpha::JwtAuthentication; +using ::envoy::config::filter::http::jwt_authn::v2alpha::JwtHeader; +using ::envoy::config::filter::http::jwt_authn::v2alpha::JwtProvider; using ::Envoy::Http::LowerCaseString; namespace Envoy { @@ -79,16 +81,22 @@ class JwtParamLocation : public JwtLocationBase { */ class ExtractorImpl : public Extractor { public: + ExtractorImpl(const JwtProvider& provider); + ExtractorImpl(const JwtAuthentication& config); std::vector extract(const Http::HeaderMap& headers) const override; + void sanitizePayloadHeaders(Http::HeaderMap& headers) const override; + private: // add a header config void addHeaderConfig(const std::string& issuer, const Http::LowerCaseString& header_name, const std::string& value_prefix); // add a query param config void addQueryParamConfig(const std::string& issuer, const std::string& param); + // ctor helper for a jwt provider config + void addProvider(const JwtProvider& provider); // HeaderMap value type to store prefix and issuers that specified this // header. @@ -113,24 +121,34 @@ class ExtractorImpl : public Extractor { }; // The map of a parameter key to set of issuers specified the parameter std::map param_locations_; + + std::vector forward_payload_headers_; }; ExtractorImpl::ExtractorImpl(const JwtAuthentication& config) { for (const auto& it : config.providers()) { const auto& provider = it.second; - for (const auto& header : provider.from_headers()) { - addHeaderConfig(provider.issuer(), LowerCaseString(header.name()), header.value_prefix()); - } - for (const std::string& param : provider.from_params()) { - addQueryParamConfig(provider.issuer(), param); - } + addProvider(provider); + } +} - // If not specified, use default locations. - if (provider.from_headers().empty() && provider.from_params().empty()) { - addHeaderConfig(provider.issuer(), Http::Headers::get().Authorization, - JwtConstValues::get().BearerPrefix); - addQueryParamConfig(provider.issuer(), JwtConstValues::get().AccessTokenParam); - } +ExtractorImpl::ExtractorImpl(const JwtProvider& provider) { addProvider(provider); } + +void ExtractorImpl::addProvider(const JwtProvider& provider) { + for (const auto& header : provider.from_headers()) { + addHeaderConfig(provider.issuer(), LowerCaseString(header.name()), header.value_prefix()); + } + for (const std::string& param : provider.from_params()) { + addQueryParamConfig(provider.issuer(), param); + } + // If not specified, use default locations. + if (provider.from_headers().empty() && provider.from_params().empty()) { + addHeaderConfig(provider.issuer(), Http::Headers::get().Authorization, + JwtConstValues::get().BearerPrefix); + addQueryParamConfig(provider.issuer(), JwtConstValues::get().AccessTokenParam); + } + if (!provider.forward_payload_header().empty()) { + forward_payload_headers_.emplace_back(provider.forward_payload_header()); } } @@ -189,10 +207,20 @@ std::vector ExtractorImpl::extract(const Http::HeaderMap& h return tokens; } +void ExtractorImpl::sanitizePayloadHeaders(Http::HeaderMap& headers) const { + for (const auto& header : forward_payload_headers_) { + headers.remove(header); + } +} + } // namespace ExtractorConstPtr Extractor::create(const JwtAuthentication& config) { - return ExtractorConstPtr(new ExtractorImpl(config)); + return std::make_unique(config); +} + +ExtractorConstPtr Extractor::create(const JwtProvider& provider) { + return std::make_unique(provider); } } // namespace JwtAuthn diff --git a/source/extensions/filters/http/jwt_authn/extractor.h b/source/extensions/filters/http/jwt_authn/extractor.h index 1d12a9ea4d94e..314a5bf721ff7 100644 --- a/source/extensions/filters/http/jwt_authn/extractor.h +++ b/source/extensions/filters/http/jwt_authn/extractor.h @@ -66,12 +66,21 @@ class Extractor { virtual ~Extractor() {} /** - * Extract all JWT tokens from the headers + * Extract all JWT tokens from the headers. If set of header_keys or param_keys + * is not empty only those in the matching locations wil be returned. + * * @param headers is the HTTP request headers. * @return list of extracted Jwt location info. */ virtual std::vector extract(const Http::HeaderMap& headers) const PURE; + /** + * Remove headers that configured to send JWT payloads. + * + * @param headers is the HTTP request headers. + */ + virtual void sanitizePayloadHeaders(Http::HeaderMap& headers) const PURE; + /** * Create an instance of Extractor for a given config. * @param the JwtAuthentication config. @@ -79,6 +88,15 @@ class Extractor { */ static ExtractorConstPtr create(const ::envoy::config::filter::http::jwt_authn::v2alpha::JwtAuthentication& config); + + /** + * Create an instance of Extractor for a given config. + * @param from_headers header location config. + * @param from_params query param location config. + * @return the extractor object. + */ + static ExtractorConstPtr + create(const ::envoy::config::filter::http::jwt_authn::v2alpha::JwtProvider& provider); }; } // namespace JwtAuthn diff --git a/source/extensions/filters/http/jwt_authn/filter.cc b/source/extensions/filters/http/jwt_authn/filter.cc index ac23444423e9d..0589d8667c9dc 100644 --- a/source/extensions/filters/http/jwt_authn/filter.cc +++ b/source/extensions/filters/http/jwt_authn/filter.cc @@ -9,27 +9,29 @@ namespace Extensions { namespace HttpFilters { namespace JwtAuthn { -Filter::Filter(JwtAuthnFilterStats& stats, AuthenticatorPtr auth) - : stats_(stats), auth_(std::move(auth)) {} +Filter::Filter(FilterConfigSharedPtr config) : stats_(config->stats()), config_(config) {} void Filter::onDestroy() { ENVOY_LOG(debug, "Called Filter : {}", __func__); - if (auth_) { - auth_->onDestroy(); + if (context_) { + context_->cancel(); } } Http::FilterHeadersStatus Filter::decodeHeaders(Http::HeaderMap& headers, bool) { ENVOY_LOG(debug, "Called Filter : {}", __func__); - // Remove headers configured to pass payload - auth_->sanitizePayloadHeaders(headers); state_ = Calling; stopped_ = false; - - // TODO(qiwzhang): support per-route config. // Verify the JWT token, onComplete() will be called when completed. - auth_->verify(headers, this); + auto matcher = config_->findMatcher(headers); + if (!matcher) { + onComplete(Status::Ok); + } else { + context_ = Verifier::createContext(headers, this); + matcher->verifier()->verify(context_); + } + if (state_ == Complete) { return Http::FilterHeadersStatus::Continue; } @@ -44,7 +46,7 @@ void Filter::onComplete(const Status& status) { if (state_ == Responded) { return; } - if (status != Status::Ok) { + if (Status::Ok != status) { stats_.denied_.inc(); state_ = Responded; // verification failed @@ -54,7 +56,6 @@ void Filter::onComplete(const Status& status) { nullptr); return; } - stats_.allowed_.inc(); state_ = Complete; if (stopped_) { diff --git a/source/extensions/filters/http/jwt_authn/filter.h b/source/extensions/filters/http/jwt_authn/filter.h index 277b7b199fdc4..2366fd6990f4a 100644 --- a/source/extensions/filters/http/jwt_authn/filter.h +++ b/source/extensions/filters/http/jwt_authn/filter.h @@ -2,9 +2,12 @@ #include "envoy/http/filter.h" +#include "common/common/lock_guard.h" #include "common/common/logger.h" +#include "common/common/thread.h" -#include "extensions/filters/http/jwt_authn/authenticator.h" +#include "extensions/filters/http/jwt_authn/filter_config.h" +#include "extensions/filters/http/jwt_authn/matcher.h" namespace Envoy { namespace Extensions { @@ -13,10 +16,10 @@ namespace JwtAuthn { // The Envoy filter to process JWT auth. class Filter : public Http::StreamDecoderFilter, - public Authenticator::Callbacks, + public Verifier::Callbacks, public Logger::Loggable { public: - Filter(JwtAuthnFilterStats& stats, AuthenticatorPtr auth); + Filter(FilterConfigSharedPtr config); // Http::StreamFilterBase void onDestroy() override; @@ -28,7 +31,7 @@ class Filter : public Http::StreamDecoderFilter, void setDecoderFilterCallbacks(Http::StreamDecoderFilterCallbacks& callbacks) override; private: - // the function for Authenticator::Callbacks interface. + // the function for Verifier::Callbacks interface. // It will be called when its verify() call is completed. void onComplete(const ::google::jwt_verify::Status& status) override; @@ -36,13 +39,15 @@ class Filter : public Http::StreamDecoderFilter, Http::StreamDecoderFilterCallbacks* decoder_callbacks_; // The stats object. JwtAuthnFilterStats& stats_; - // The auth object. - AuthenticatorPtr auth_; // The state of the request enum State { Init, Calling, Responded, Complete }; State state_ = Init; // Mark if request has been stopped. bool stopped_ = false; + // Filter config object. + FilterConfigSharedPtr config_; + // Verify context for current request. + ContextSharedPtr context_; }; } // namespace JwtAuthn diff --git a/source/extensions/filters/http/jwt_authn/filter_config.h b/source/extensions/filters/http/jwt_authn/filter_config.h index cb400e6dffcb6..0a71374c8588a 100644 --- a/source/extensions/filters/http/jwt_authn/filter_config.h +++ b/source/extensions/filters/http/jwt_authn/filter_config.h @@ -5,10 +5,7 @@ #include "envoy/stats/stats_macros.h" #include "envoy/thread_local/thread_local.h" -#include "common/common/logger.h" - -#include "extensions/filters/http/jwt_authn/extractor.h" -#include "extensions/filters/http/jwt_authn/jwks_cache.h" +#include "extensions/filters/http/jwt_authn/matcher.h" namespace Envoy { namespace Extensions { @@ -57,8 +54,10 @@ struct JwtAuthnFilterStats { /** * The filer config object to hold config and relavant objects. */ -class FilterConfig : public Logger::Loggable { +class FilterConfig : public Logger::Loggable, public AuthFactory { public: + virtual ~FilterConfig() {} + FilterConfig( const ::envoy::config::filter::http::jwt_authn::v2alpha::JwtAuthentication& proto_config, const std::string& stats_prefix, Server::Configuration::FactoryContext& context) @@ -70,6 +69,11 @@ class FilterConfig : public Logger::Loggable { return std::make_shared(proto_config_, time_source_); }); extractor_ = Extractor::create(proto_config_); + + for (const auto& rule : proto_config_.rules()) { + rule_matchers_.push_back( + Matcher::create(rule, proto_config_.providers(), *this, getExtractor())); + } } JwtAuthnFilterStats& stats() { return stats_; } @@ -81,14 +85,32 @@ class FilterConfig : public Logger::Loggable { } // Get per-thread cache object. - ThreadLocalCache& getCache() { return tls_->getTyped(); } + ThreadLocalCache& getCache() const { return tls_->getTyped(); } - Upstream::ClusterManager& cm() { return cm_; } - TimeSource& timeSource() { return time_source_; } + Upstream::ClusterManager& cm() const { return cm_; } + TimeSource& timeSource() const { return time_source_; } // Get the token extractor. const Extractor& getExtractor() const { return *extractor_; } + // Finds the matcher that matched the header + virtual const MatcherConstSharedPtr findMatcher(const Http::HeaderMap& headers) const { + for (const auto& matcher : rule_matchers_) { + if (matcher->matches(headers)) { + return matcher; + } + } + return nullptr; + } + + // methods for AuthFactory interface. Factory method to help create authenticators. + AuthenticatorPtr create(const ::google::jwt_verify::CheckAudience* check_audience, + const absl::optional& provider, + bool allow_failed) const override { + return Authenticator::create(check_audience, provider, allow_failed, getCache().getJwksCache(), + cm(), Common::JwksFetcher::create, timeSource()); + } + private: JwtAuthnFilterStats generateStats(const std::string& prefix, Stats::Scope& scope) { const std::string final_prefix = prefix + "jwt_authn."; @@ -105,6 +127,8 @@ class FilterConfig : public Logger::Loggable { Upstream::ClusterManager& cm_; // The object to extract tokens. ExtractorConstPtr extractor_; + // The list of rule matchers. + std::vector rule_matchers_; TimeSource& time_source_; }; typedef std::shared_ptr FilterConfigSharedPtr; diff --git a/source/extensions/filters/http/jwt_authn/filter_factory.cc b/source/extensions/filters/http/jwt_authn/filter_factory.cc index ecec7bf035059..d63f1253215d8 100644 --- a/source/extensions/filters/http/jwt_authn/filter_factory.cc +++ b/source/extensions/filters/http/jwt_authn/filter_factory.cc @@ -46,8 +46,7 @@ FilterFactory::createFilterFactoryFromProtoTyped(const JwtAuthentication& proto_ validateJwtConfig(proto_config); auto filter_config = std::make_shared(proto_config, prefix, context); return [filter_config](Http::FilterChainFactoryCallbacks& callbacks) -> void { - callbacks.addStreamDecoderFilter(std::make_shared( - filter_config->stats(), Authenticator::create(filter_config, Common::JwksFetcher::create))); + callbacks.addStreamDecoderFilter(std::make_shared(filter_config)); }; } diff --git a/source/extensions/filters/http/jwt_authn/jwks_cache.cc b/source/extensions/filters/http/jwt_authn/jwks_cache.cc index 95a89751b7ef3..225092610c538 100644 --- a/source/extensions/filters/http/jwt_authn/jwks_cache.cc +++ b/source/extensions/filters/http/jwt_authn/jwks_cache.cc @@ -99,12 +99,23 @@ class JwksCacheImpl : public JwksCache { JwksCacheImpl(const JwtAuthentication& config, TimeSource& time_source) { for (const auto& it : config.providers()) { const auto& provider = it.second; - jwks_data_map_.emplace(provider.issuer(), JwksDataImpl(provider, time_source)); + jwks_data_map_.emplace(it.first, JwksDataImpl(provider, time_source)); + if (issuer_ptr_map_.find(provider.issuer()) == issuer_ptr_map_.end()) { + issuer_ptr_map_.emplace(provider.issuer(), findByProvider(it.first)); + } + } + } + + JwksData* findByIssuer(const std::string& issuer) override { + const auto it = issuer_ptr_map_.find(issuer); + if (it == issuer_ptr_map_.end()) { + return nullptr; } + return it->second; } - JwksData* findByIssuer(const std::string& name) override { - auto it = jwks_data_map_.find(name); + JwksData* findByProvider(const std::string& provider) override { + const auto it = jwks_data_map_.find(provider); if (it == jwks_data_map_.end()) { return nullptr; } @@ -112,8 +123,10 @@ class JwksCacheImpl : public JwksCache { } private: - // The Jwks data map indexed by issuer. + // The Jwks data map indexed by provider. std::unordered_map jwks_data_map_; + // The Jwks data pointer map indexed by issuer. + std::unordered_map issuer_ptr_map_; }; } // namespace diff --git a/source/extensions/filters/http/jwt_authn/jwks_cache.h b/source/extensions/filters/http/jwt_authn/jwks_cache.h index d2a65a5cad310..c279184d8fbfc 100644 --- a/source/extensions/filters/http/jwt_authn/jwks_cache.h +++ b/source/extensions/filters/http/jwt_authn/jwks_cache.h @@ -41,13 +41,13 @@ class JwksCache { public: virtual ~JwksData() {} + // Check if a list of audiences are allowed. + virtual bool areAudiencesAllowed(const std::vector& audiences) const PURE; + // Get the cached config: JWT rule. virtual const ::envoy::config::filter::http::jwt_authn::v2alpha::JwtProvider& getJwtProvider() const PURE; - // Check if a list of audiences are allowed. - virtual bool areAudiencesAllowed(const std::vector& audiences) const PURE; - // Get the Jwks object. virtual const ::google::jwt_verify::Jwks* getJwksObj() const PURE; @@ -60,7 +60,9 @@ class JwksCache { }; // Lookup issuer cache map. The cache only stores Jwks specified in the config. - virtual JwksData* findByIssuer(const std::string& name) PURE; + virtual JwksData* findByIssuer(const std::string& issuer) PURE; + + virtual JwksData* findByProvider(const std::string& provider) PURE; // Factory function to create an instance. static JwksCachePtr diff --git a/source/extensions/filters/http/jwt_authn/matcher.cc b/source/extensions/filters/http/jwt_authn/matcher.cc new file mode 100644 index 0000000000000..5294c1cedd722 --- /dev/null +++ b/source/extensions/filters/http/jwt_authn/matcher.cc @@ -0,0 +1,169 @@ +#include "extensions/filters/http/jwt_authn/matcher.h" + +#include "common/router/config_impl.h" + +using ::envoy::api::v2::route::RouteMatch; +using ::envoy::config::filter::http::jwt_authn::v2alpha::JwtProvider; +using ::envoy::config::filter::http::jwt_authn::v2alpha::RequirementRule; +using Envoy::Router::ConfigUtility; + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace JwtAuthn { +namespace { + +/** + * Perform a match against any HTTP header or pseudo-header. + */ +class BaseMatcherImpl : public Matcher, public Logger::Loggable { +public: + BaseMatcherImpl(const RequirementRule& rule, + const Protobuf::Map& providers, + const AuthFactory& factory, const Extractor& extractor) + : case_sensitive_(PROTOBUF_GET_WRAPPED_OR_DEFAULT(rule.match(), case_sensitive, true)) { + + for (const auto& header_map : rule.match().headers()) { + config_headers_.push_back(header_map); + } + + for (const auto& query_parameter : rule.match().query_parameters()) { + config_query_parameters_.push_back(query_parameter); + } + + verifier_ = Verifier::create(rule.requires(), providers, factory, extractor); + } + + // Check match for HeaderMatcher and QueryParameterMatcher + bool matchRoute(const Http::HeaderMap& headers) const { + bool matches = true; + // TODO(potatop): matching on RouteMatch runtime is not implemented. + + matches &= Http::HeaderUtility::matchHeaders(headers, config_headers_); + if (!config_query_parameters_.empty()) { + Http::Utility::QueryParams query_parameters = + Http::Utility::parseQueryString(headers.Path()->value().getStringView()); + matches &= ConfigUtility::matchQueryParams(query_parameters, config_query_parameters_); + } + return matches; + } + + const VerifierPtr& verifier() const override { return verifier_; } + +protected: + const bool case_sensitive_; + +private: + std::vector config_headers_; + std::vector config_query_parameters_; + VerifierPtr verifier_; +}; + +/** + * Perform a match against any path with prefix rule. + */ +class PrefixMatcherImpl : public BaseMatcherImpl { +public: + PrefixMatcherImpl(const RequirementRule& rule, + const Protobuf::Map& providers, + const AuthFactory& factory, const Extractor& extractor) + : BaseMatcherImpl(rule, providers, factory, extractor), prefix_(rule.match().prefix()) {} + + bool matches(const Http::HeaderMap& headers) const override { + if (BaseMatcherImpl::matchRoute(headers) && + StringUtil::startsWith(headers.Path()->value().c_str(), prefix_, case_sensitive_)) { + ENVOY_LOG(debug, "Prefix requirement '{}' matched.", prefix_); + return true; + } + return false; + } + +private: + // prefix string + const std::string prefix_; +}; + +/** + * Perform a match against any path with a specific path rule. + */ +class PathMatcherImpl : public BaseMatcherImpl { +public: + PathMatcherImpl(const RequirementRule& rule, + const Protobuf::Map& providers, + const AuthFactory& factory, const Extractor& extractor) + : BaseMatcherImpl(rule, providers, factory, extractor), path_(rule.match().path()) {} + + bool matches(const Http::HeaderMap& headers) const override { + if (BaseMatcherImpl::matchRoute(headers)) { + const Http::HeaderString& path = headers.Path()->value(); + size_t compare_length = Http::Utility::findQueryStringStart(path) - path.c_str(); + + auto real_path = path.getStringView().substr(0, compare_length); + bool match = case_sensitive_ ? real_path == path_ : StringUtil::caseCompare(real_path, path_); + if (match) { + ENVOY_LOG(debug, "Path requirement '{}' matched.", path_); + return true; + } + } + return false; + } + +private: + // path string. + const std::string path_; +}; + +/** + * Perform a match against any path with a regex rule. + */ +class RegexMatcherImpl : public BaseMatcherImpl { +public: + RegexMatcherImpl(const RequirementRule& rule, + const Protobuf::Map& providers, + const AuthFactory& factory, const Extractor& extractor) + : BaseMatcherImpl(rule, providers, factory, extractor), + regex_(RegexUtil::parseRegex(rule.match().regex())), regex_str_(rule.match().regex()) {} + + bool matches(const Http::HeaderMap& headers) const override { + if (BaseMatcherImpl::matchRoute(headers)) { + const Http::HeaderString& path = headers.Path()->value(); + const char* query_string_start = Http::Utility::findQueryStringStart(path); + if (std::regex_match(path.c_str(), query_string_start, regex_)) { + ENVOY_LOG(debug, "Regex requirement '{}' matched.", regex_str_); + return true; + } + } + return false; + } + +private: + // regex object + const std::regex regex_; + // raw regex string, for logging. + const std::string regex_str_; +}; + +} // namespace + +MatcherConstSharedPtr +Matcher::create(const RequirementRule& rule, + const Protobuf::Map& providers, + const AuthFactory& factory, const Extractor& extractor) { + switch (rule.match().path_specifier_case()) { + case RouteMatch::PathSpecifierCase::kPrefix: + return std::make_shared(rule, providers, factory, extractor); + case RouteMatch::PathSpecifierCase::kPath: + return std::make_shared(rule, providers, factory, extractor); + case RouteMatch::PathSpecifierCase::kRegex: + return std::make_shared(rule, providers, factory, extractor); + // path specifier is required. + case RouteMatch::PathSpecifierCase::PATH_SPECIFIER_NOT_SET: + default: + NOT_REACHED_GCOVR_EXCL_LINE; + } +} + +} // namespace JwtAuthn +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/jwt_authn/matcher.h b/source/extensions/filters/http/jwt_authn/matcher.h new file mode 100644 index 0000000000000..ebbc87d3a406a --- /dev/null +++ b/source/extensions/filters/http/jwt_authn/matcher.h @@ -0,0 +1,57 @@ +#pragma once + +#include "envoy/http/header_map.h" + +#include "extensions/filters/http/jwt_authn/verifier.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace JwtAuthn { + +class Matcher; +typedef std::shared_ptr MatcherConstSharedPtr; + +/** + * Supports matching a HTTP requests with JWT requirements. + */ +class Matcher { +public: + virtual ~Matcher() {} + + /** + * Returns if a HTTP request matches with the rules of the matcher. + * + * @param headers the request headers used to match against. An empty map should be used if + * there are none headers available. + * @return true if request is a match, false otherwise. + */ + virtual bool matches(const Http::HeaderMap& headers) const PURE; + + /** + * Returns the configured verifier for this route. + * + * @return reference to verifier pointer. + */ + virtual const VerifierPtr& verifier() const PURE; + + /** + * Factory method to create a shared instance of a matcher based on the rule defined. + * + * @param rule the proto rule match message. + * @param providers the provider name to config map + * @param factory the Authenticator factory + * @return the matcher instance. + */ + static MatcherConstSharedPtr + create(const ::envoy::config::filter::http::jwt_authn::v2alpha::RequirementRule& rule, + const Protobuf::Map& + providers, + const AuthFactory& factory, const Extractor& extractor); +}; + +} // namespace JwtAuthn +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/jwt_authn/verifier.cc b/source/extensions/filters/http/jwt_authn/verifier.cc new file mode 100644 index 0000000000000..bffb7cb6683bd --- /dev/null +++ b/source/extensions/filters/http/jwt_authn/verifier.cc @@ -0,0 +1,303 @@ +#include "extensions/filters/http/jwt_authn/verifier.h" + +#include "jwt_verify_lib/check_audience.h" + +using ::envoy::config::filter::http::jwt_authn::v2alpha::JwtProvider; +using ::envoy::config::filter::http::jwt_authn::v2alpha::JwtRequirement; +using ::envoy::config::filter::http::jwt_authn::v2alpha::JwtRequirementAndList; +using ::envoy::config::filter::http::jwt_authn::v2alpha::JwtRequirementOrList; +using ::google::jwt_verify::CheckAudience; +using ::google::jwt_verify::Status; + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace JwtAuthn { +namespace { + +/** + * Struct to keep track of verifier completed and responded state for a request. + */ +struct CompletionState { + // if verifier node has responded to a request or not. + bool is_completed_{false}; + // number of completed inner verifier for an any/all verifier. + std::size_t number_completed_children_{0}; +}; + +class ContextImpl : public Verifier::Context { +public: + ContextImpl(Http::HeaderMap& headers, Verifier::Callbacks* callback) + : headers_(headers), callback_(callback) {} + + Http::HeaderMap& headers() const override { return headers_; } + + Verifier::Callbacks* callback() const override { return callback_; } + + void cancel() override { + for (const auto& it : auths_) { + it->onDestroy(); + } + } + + // Get Response data which can be used to check if a verifier node has responded or not. + CompletionState& getCompletionState(const Verifier* verifier) { + return completion_states_[verifier]; + } + + // Stores an authenticator object for this request. + void storeAuth(AuthenticatorPtr&& auth) { auths_.emplace_back(std::move(auth)); } + +private: + Http::HeaderMap& headers_; + Verifier::Callbacks* callback_; + std::unordered_map completion_states_; + std::vector auths_; +}; + +// base verifier for provider_name, provider_and_audiences, and allow_missing_or_failed. +class BaseVerifierImpl : public Verifier { +public: + BaseVerifierImpl(const BaseVerifierImpl* parent) : parent_(parent) {} + + void completeWithStatus(Status status, ContextImpl& context) const { + if (parent_ != nullptr) { + return parent_->onComplete(status, context); + } + + context.callback()->onComplete(status); + context.cancel(); + } + + // Check if next verifier should be notified of status, or if no next verifier exists signal + // callback in context. + virtual void onComplete(const Status& status, ContextImpl& context) const { + auto& completion_state = context.getCompletionState(this); + if (!completion_state.is_completed_) { + completion_state.is_completed_ = true; + completeWithStatus(status, context); + } + } + +protected: + // The parent group verifier. + const BaseVerifierImpl* const parent_; +}; + +// Provider specific verifier +class ProviderVerifierImpl : public BaseVerifierImpl { +public: + ProviderVerifierImpl(const std::string& provider_name, const AuthFactory& factory, + const JwtProvider& provider, const BaseVerifierImpl* parent) + : BaseVerifierImpl(parent), auth_factory_(factory), extractor_(Extractor::create(provider)), + provider_name_(absl::make_optional(provider_name)) {} + + void verify(ContextSharedPtr context) const override { + auto& ctximpl = static_cast(*context); + auto auth = auth_factory_.create(getAudienceChecker(), provider_name_, false); + extractor_->sanitizePayloadHeaders(ctximpl.headers()); + auth->verify(ctximpl.headers(), extractor_->extract(ctximpl.headers()), + [this, context](const Status& status) { + onComplete(status, static_cast(*context)); + }); + if (!ctximpl.getCompletionState(this).is_completed_) { + ctximpl.storeAuth(std::move(auth)); + } else { + auth->onDestroy(); + } + } + +protected: + virtual const CheckAudience* getAudienceChecker() const { return nullptr; } + +private: + const AuthFactory& auth_factory_; + const ExtractorConstPtr extractor_; + const absl::optional provider_name_; +}; + +class ProviderAndAudienceVerifierImpl : public ProviderVerifierImpl { +public: + ProviderAndAudienceVerifierImpl(const std::string& provider_name, const AuthFactory& factory, + const JwtProvider& provider, const BaseVerifierImpl* parent, + const std::vector& config_audiences) + : ProviderVerifierImpl(provider_name, factory, provider, parent), + check_audience_(std::make_unique(config_audiences)) {} + +private: + const CheckAudience* getAudienceChecker() const override { return check_audience_.get(); } + + // Check audience object + ::google::jwt_verify::CheckAudiencePtr check_audience_; +}; + +// Allow missing or failed verifier +class AllowFailedVerifierImpl : public BaseVerifierImpl { +public: + AllowFailedVerifierImpl(const AuthFactory& factory, const Extractor& extractor, + const BaseVerifierImpl* parent) + : BaseVerifierImpl(parent), auth_factory_(factory), extractor_(extractor) {} + + void verify(ContextSharedPtr context) const override { + auto& ctximpl = static_cast(*context); + auto auth = auth_factory_.create(nullptr, absl::nullopt, true); + extractor_.sanitizePayloadHeaders(ctximpl.headers()); + auth->verify(ctximpl.headers(), extractor_.extract(ctximpl.headers()), + [this, context](const Status& status) { + onComplete(status, static_cast(*context)); + }); + if (!ctximpl.getCompletionState(this).is_completed_) { + ctximpl.storeAuth(std::move(auth)); + } else { + auth->onDestroy(); + } + } + +private: + const AuthFactory& auth_factory_; + const Extractor& extractor_; +}; + +VerifierPtr innerCreate(const JwtRequirement& requirement, + const Protobuf::Map& providers, + const AuthFactory& factory, const Extractor& extractor, + const BaseVerifierImpl* parent); + +// Base verifier for requires all or any. +class BaseGroupVerifierImpl : public BaseVerifierImpl { +public: + BaseGroupVerifierImpl(const BaseVerifierImpl* parent) : BaseVerifierImpl(parent) {} + + void verify(ContextSharedPtr context) const override { + auto& ctximpl = static_cast(*context); + for (const auto& it : verifiers_) { + if (ctximpl.getCompletionState(this).is_completed_) { + return; + } + it->verify(context); + } + } + +protected: + // The list of requirement verifiers + std::vector verifiers_; +}; + +// Requires any verifier. +class AnyVerifierImpl : public BaseGroupVerifierImpl { +public: + AnyVerifierImpl(const JwtRequirementOrList& or_list, const AuthFactory& factory, + const Protobuf::Map& providers, + const Extractor& extractor_for_allow_fail, const BaseVerifierImpl* parent) + : BaseGroupVerifierImpl(parent) { + for (const auto& it : or_list.requirements()) { + verifiers_.emplace_back(innerCreate(it, providers, factory, extractor_for_allow_fail, this)); + } + } + + void onComplete(const Status& status, ContextImpl& context) const override { + auto& completion_state = context.getCompletionState(this); + if (completion_state.is_completed_) { + return; + } + if (++completion_state.number_completed_children_ == verifiers_.size() || + Status::Ok == status) { + completion_state.is_completed_ = true; + completeWithStatus(status, context); + } + } +}; + +// Requires all verifier +class AllVerifierImpl : public BaseGroupVerifierImpl { +public: + AllVerifierImpl(const JwtRequirementAndList& and_list, const AuthFactory& factory, + const Protobuf::Map& providers, + const Extractor& extractor_for_allow_fail, const BaseVerifierImpl* parent) + : BaseGroupVerifierImpl(parent) { + for (const auto& it : and_list.requirements()) { + verifiers_.emplace_back(innerCreate(it, providers, factory, extractor_for_allow_fail, this)); + } + } + + void onComplete(const Status& status, ContextImpl& context) const override { + auto& completion_state = context.getCompletionState(this); + if (completion_state.is_completed_) { + return; + } + if (++completion_state.number_completed_children_ == verifiers_.size() || + Status::Ok != status) { + completion_state.is_completed_ = true; + completeWithStatus(status, context); + } + } +}; + +// Match all, for requirement not set +class AllowAllVerifierImpl : public BaseVerifierImpl { +public: + AllowAllVerifierImpl(const BaseVerifierImpl* parent) : BaseVerifierImpl(parent) {} + + void verify(ContextSharedPtr context) const override { + completeWithStatus(Status::Ok, static_cast(*context)); + } +}; + +VerifierPtr innerCreate(const JwtRequirement& requirement, + const Protobuf::Map& providers, + const AuthFactory& factory, const Extractor& extractor_for_allow_fail, + const BaseVerifierImpl* parent) { + std::string provider_name; + std::vector audiences; + switch (requirement.requires_type_case()) { + case JwtRequirement::RequiresTypeCase::kProviderName: + provider_name = requirement.provider_name(); + break; + case JwtRequirement::RequiresTypeCase::kProviderAndAudiences: + for (const auto& it : requirement.provider_and_audiences().audiences()) { + audiences.emplace_back(it); + } + provider_name = requirement.provider_and_audiences().provider_name(); + break; + case JwtRequirement::RequiresTypeCase::kRequiresAny: + return std::make_unique(requirement.requires_any(), factory, providers, + extractor_for_allow_fail, parent); + case JwtRequirement::RequiresTypeCase::kRequiresAll: + return std::make_unique(requirement.requires_all(), factory, providers, + extractor_for_allow_fail, parent); + case JwtRequirement::RequiresTypeCase::kAllowMissingOrFailed: + return std::make_unique(factory, extractor_for_allow_fail, parent); + case JwtRequirement::RequiresTypeCase::REQUIRES_TYPE_NOT_SET: + return std::make_unique(parent); + default: + NOT_REACHED_GCOVR_EXCL_LINE; + } + + const auto& it = providers.find(provider_name); + if (it == providers.end()) { + throw EnvoyException(fmt::format("Required provider ['{}'] is not configured.", provider_name)); + } + if (audiences.empty()) { + return std::make_unique(provider_name, factory, it->second, parent); + } + return std::make_unique(provider_name, factory, it->second, + parent, audiences); +} + +} // namespace + +ContextSharedPtr Verifier::createContext(Http::HeaderMap& headers, Callbacks* callback) { + return std::make_shared(headers, callback); +} + +VerifierPtr Verifier::create(const JwtRequirement& requirement, + const Protobuf::Map& providers, + const AuthFactory& factory, + const Extractor& extractor_for_allow_fail) { + return innerCreate(requirement, providers, factory, extractor_for_allow_fail, nullptr); +} + +} // namespace JwtAuthn +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/jwt_authn/verifier.h b/source/extensions/filters/http/jwt_authn/verifier.h new file mode 100644 index 0000000000000..46c3324dc9ad6 --- /dev/null +++ b/source/extensions/filters/http/jwt_authn/verifier.h @@ -0,0 +1,82 @@ +#pragma once + +#include "extensions/filters/http/jwt_authn/authenticator.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace JwtAuthn { + +class Verifier; +typedef std::unique_ptr VerifierPtr; + +/** + * Supports verification of JWTs with configured requirments. + */ +class Verifier { +public: + virtual ~Verifier() {} + + /** + * Handle for notifying Verifier callers of request completion. + */ + class Callbacks { + public: + virtual ~Callbacks() {} + + /** + * Called on completion of request. + * + * @param status the status of the request. + */ + virtual void onComplete(const ::google::jwt_verify::Status& status) PURE; + }; + + // Context object to hold data needed for verifier. + class Context { + public: + virtual ~Context() {} + + /** + * Returns the request headers wrapped in this context. + * + * @return the request headers. + */ + virtual Http::HeaderMap& headers() const PURE; + + /** + * Returns the request callback wrapped in this context. + * + * @returns the request callback. + */ + virtual Callbacks* callback() const PURE; + + /** + * Cancel any pending reuqets for this context. + */ + virtual void cancel() PURE; + }; + + typedef std::shared_ptr ContextSharedPtr; + + // Verify all tokens on headers, and signal the caller with callback. + virtual void verify(ContextSharedPtr context) const PURE; + + // Factory method for creating verifiers. + static VerifierPtr + create(const ::envoy::config::filter::http::jwt_authn::v2alpha::JwtRequirement& requirement, + const Protobuf::Map& + providers, + const AuthFactory& factory, const Extractor& extractor_for_allow_fail); + + // Factory method for creating verifier contexts. + static ContextSharedPtr createContext(Http::HeaderMap& headers, Callbacks* callback); +}; + +typedef std::shared_ptr ContextSharedPtr; + +} // namespace JwtAuthn +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/jwt_authn/BUILD b/test/extensions/filters/http/jwt_authn/BUILD index c25aca49c368c..28a5a3778e794 100644 --- a/test/extensions/filters/http/jwt_authn/BUILD +++ b/test/extensions/filters/http/jwt_authn/BUILD @@ -3,6 +3,7 @@ licenses(["notice"]) # Apache 2 load( "//bazel:envoy_build_system.bzl", "envoy_cc_library", + "envoy_cc_mock", "envoy_package", ) load( @@ -17,16 +18,15 @@ envoy_cc_library( hdrs = ["test_common.h"], ) -envoy_cc_library( +envoy_cc_mock( name = "mock_lib", hdrs = ["mock.h"], + deps = ["//test/mocks/server:server_mocks"], ) envoy_extension_cc_test( name = "extractor_test", - srcs = [ - "extractor_test.cc", - ], + srcs = ["extractor_test.cc"], extension_name = "envoy.filters.http.jwt_authn", deps = [ "//source/extensions/filters/http/jwt_authn:extractor_lib", @@ -36,9 +36,7 @@ envoy_extension_cc_test( envoy_extension_cc_test( name = "filter_test", - srcs = [ - "filter_test.cc", - ], + srcs = ["filter_test.cc"], extension_name = "envoy.filters.http.jwt_authn", deps = [ ":mock_lib", @@ -50,9 +48,7 @@ envoy_extension_cc_test( envoy_extension_cc_test( name = "filter_factory_test", - srcs = [ - "filter_factory_test.cc", - ], + srcs = ["filter_factory_test.cc"], extension_name = "envoy.filters.http.jwt_authn", deps = [ "//source/extensions/filters/http/jwt_authn:config", @@ -63,9 +59,7 @@ envoy_extension_cc_test( envoy_extension_cc_test( name = "jwks_cache_test", - srcs = [ - "jwks_cache_test.cc", - ], + srcs = ["jwks_cache_test.cc"], extension_name = "envoy.filters.http.jwt_authn", deps = [ "//source/extensions/filters/http/common:jwks_fetcher_lib", @@ -78,14 +72,14 @@ envoy_extension_cc_test( envoy_extension_cc_test( name = "authenticator_test", - srcs = [ - "authenticator_test.cc", - ], + srcs = ["authenticator_test.cc"], extension_name = "envoy.filters.http.jwt_authn", deps = [ ":mock_lib", "//source/extensions/filters/http/common:jwks_fetcher_lib", "//source/extensions/filters/http/jwt_authn:authenticator_lib", + "//source/extensions/filters/http/jwt_authn:filter_config_interface", + "//source/extensions/filters/http/jwt_authn:matchers_lib", "//test/extensions/filters/http/common:mock_lib", "//test/extensions/filters/http/jwt_authn:test_common_lib", "//test/mocks/server:server_mocks", @@ -104,3 +98,44 @@ envoy_extension_cc_test( "//test/integration:http_protocol_integration_lib", ], ) + +envoy_extension_cc_test( + name = "matcher_test", + srcs = ["matcher_test.cc"], + extension_name = "envoy.filters.http.jwt_authn", + deps = [ + ":mock_lib", + ":test_common_lib", + "//source/extensions/filters/http/jwt_authn:matchers_lib", + "//test/test_common:utility_lib", + ], +) + +envoy_extension_cc_test( + name = "group_verifier_test", + srcs = ["group_verifier_test.cc"], + extension_name = "envoy.filters.http.jwt_authn", + deps = [ + ":mock_lib", + ":test_common_lib", + "//source/extensions/filters/http/jwt_authn:verifier_lib", + "//test/test_common:utility_lib", + ], +) + +envoy_extension_cc_test( + name = "verifier_test", + srcs = [ + "all_verifier_test.cc", + "provider_verifier_test.cc", + ], + extension_name = "envoy.filters.http.jwt_authn", + deps = [ + ":mock_lib", + ":test_common_lib", + "//source/extensions/filters/http/jwt_authn:filter_config_interface", + "//source/extensions/filters/http/jwt_authn:matchers_lib", + "//test/mocks/server:server_mocks", + "//test/test_common:utility_lib", + ], +) diff --git a/test/extensions/filters/http/jwt_authn/all_verifier_test.cc b/test/extensions/filters/http/jwt_authn/all_verifier_test.cc new file mode 100644 index 0000000000000..96a8c9b43c7c7 --- /dev/null +++ b/test/extensions/filters/http/jwt_authn/all_verifier_test.cc @@ -0,0 +1,81 @@ +#include "extensions/filters/http/jwt_authn/filter_config.h" +#include "extensions/filters/http/jwt_authn/verifier.h" + +#include "test/extensions/filters/http/jwt_authn/mock.h" +#include "test/extensions/filters/http/jwt_authn/test_common.h" +#include "test/mocks/server/mocks.h" +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" + +using ::envoy::config::filter::http::jwt_authn::v2alpha::JwtAuthentication; +using ::google::jwt_verify::Status; +using ::testing::NiceMock; + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace JwtAuthn { + +class AllVerifierTest : public ::testing::Test { +public: + void SetUp() { MessageUtil::loadFromYaml(ExampleConfig, proto_config_); } + + void createVerifier() { + filter_config_ = ::std::make_shared(proto_config_, "", mock_factory_ctx_); + verifier_ = Verifier::create(proto_config_.rules(0).requires(), proto_config_.providers(), + *filter_config_, filter_config_->getExtractor()); + } + + JwtAuthentication proto_config_; + FilterConfigSharedPtr filter_config_; + VerifierPtr verifier_; + NiceMock mock_factory_ctx_; + ContextSharedPtr context_; + MockVerifierCallbacks mock_cb_; +}; + +// tests rule that is just match no requries. +TEST_F(AllVerifierTest, TestAllAllow) { + proto_config_.mutable_rules(0)->clear_requires(); + createVerifier(); + + EXPECT_CALL(mock_cb_, onComplete(Status::Ok)).Times(2); + auto headers = Http::TestHeaderMapImpl{{"Authorization", "Bearer a"}}; + context_ = Verifier::createContext(headers, &mock_cb_); + verifier_->verify(context_); + headers = Http::TestHeaderMapImpl{}; + context_ = Verifier::createContext(headers, &mock_cb_); + verifier_->verify(context_); +} + +// tests requires allow missing or failed +TEST_F(AllVerifierTest, TestAllowFailed) { + std::vector names{"a", "b", "c"}; + for (const auto& it : names) { + auto header = + (*proto_config_.mutable_providers())[std::string(ProviderName)].add_from_headers(); + header->set_name(it); + header->set_value_prefix("Prefix "); + } + proto_config_.mutable_rules(0)->mutable_requires()->mutable_allow_missing_or_failed(); + createVerifier(); + MockUpstream mock_pubkey(mock_factory_ctx_.cluster_manager_, PublicKey); + + EXPECT_CALL(mock_cb_, onComplete(Status::Ok)).Times(1); + auto headers = Http::TestHeaderMapImpl{ + {"a", "Prefix " + std::string(GoodToken)}, + {"b", "Prefix " + std::string(NonExistKidToken)}, + {"c", "Prefix "}, + }; + context_ = Verifier::createContext(headers, &mock_cb_); + verifier_->verify(context_); + EXPECT_FALSE(headers.has("a")); + EXPECT_TRUE(headers.has("b")); + EXPECT_TRUE(headers.has("c")); +} + +} // namespace JwtAuthn +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/jwt_authn/authenticator_test.cc b/test/extensions/filters/http/jwt_authn/authenticator_test.cc index f6c2d90bc7666..a2725ff32aed8 100644 --- a/test/extensions/filters/http/jwt_authn/authenticator_test.cc +++ b/test/extensions/filters/http/jwt_authn/authenticator_test.cc @@ -3,12 +3,12 @@ #include "extensions/filters/http/common/jwks_fetcher.h" #include "extensions/filters/http/jwt_authn/authenticator.h" +#include "extensions/filters/http/jwt_authn/filter_config.h" #include "test/extensions/filters/http/common/mock.h" #include "test/extensions/filters/http/jwt_authn/mock.h" #include "test/extensions/filters/http/jwt_authn/test_common.h" #include "test/mocks/server/mocks.h" -#include "test/mocks/upstream/mocks.h" #include "test/test_common/utility.h" #include "gtest/gtest.h" @@ -17,6 +17,7 @@ using ::envoy::config::filter::http::jwt_authn::v2alpha::JwtAuthentication; 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; @@ -34,30 +35,41 @@ class AuthenticatorTest : public ::testing::Test { CreateAuthenticator(); } - void CreateAuthenticator() { + void CreateAuthenticator(::google::jwt_verify::CheckAudience* check_audience = nullptr, + const absl::optional& provider = + absl::make_optional(ProviderName)) { filter_config_ = ::std::make_shared(proto_config_, "", mock_factory_ctx_); - fetcher_ = new MockJwksFetcher; - fetcherPtr_.reset(fetcher_); - auth_ = Authenticator::create( - filter_config_, [this](Upstream::ClusterManager&) { return std::move(fetcherPtr_); }); - jwks_ = ::google::jwt_verify::Jwks::createFrom(PublicKey, ::google::jwt_verify::Jwks::JWKS); + raw_fetcher_ = new MockJwksFetcher; + fetcher_.reset(raw_fetcher_); + auth_ = Authenticator::create(check_audience, provider, !provider, + filter_config_->getCache().getJwksCache(), filter_config_->cm(), + [this](Upstream::ClusterManager&) { return std::move(fetcher_); }, + filter_config_->timeSource()); + jwks_ = Jwks::createFrom(PublicKey, Jwks::JWKS); EXPECT_TRUE(jwks_->getStatus() == Status::Ok); } + void expectVerifyStatus(Status expected_status, Http::HeaderMap& headers) { + std::function on_complete_cb = [&expected_status](const Status& status) { + ASSERT_EQ(status, expected_status); + }; + auto tokens = filter_config_->getExtractor().extract(headers); + auth_->verify(headers, std::move(tokens), std::move(on_complete_cb)); + } + JwtAuthentication proto_config_; FilterConfigSharedPtr filter_config_; - MockJwksFetcher* fetcher_; - JwksFetcherPtr fetcherPtr_; + MockJwksFetcher* raw_fetcher_; + JwksFetcherPtr fetcher_; AuthenticatorPtr auth_; ::google::jwt_verify::JwksPtr jwks_; NiceMock mock_factory_ctx_; - MockAuthenticatorCallbacks mock_cb_; }; // This test validates a good JWT authentication with a remote Jwks. // It also verifies Jwks cache with 10 JWT authentications, but only one Jwks fetch. TEST_F(AuthenticatorTest, TestOkJWTandCache) { - EXPECT_CALL(*fetcher_, fetch(_, _)) + EXPECT_CALL(*raw_fetcher_, fetch(_, _)) .WillOnce(Invoke( [this](const ::envoy::api::v2::core::HttpUri&, JwksFetcher::JwksReceiver& receiver) { receiver.onJwksSuccess(std::move(jwks_)); @@ -67,12 +79,7 @@ TEST_F(AuthenticatorTest, TestOkJWTandCache) { for (int i = 0; i < 10; i++) { auto headers = Http::TestHeaderMapImpl{{"Authorization", "Bearer " + std::string(GoodToken)}}; - MockAuthenticatorCallbacks mock_cb; - EXPECT_CALL(mock_cb, onComplete(_)).WillOnce(Invoke([](const Status& status) { - ASSERT_EQ(status, Status::Ok); - })); - - auth_->verify(headers, &mock_cb); + expectVerifyStatus(Status::Ok, headers); EXPECT_EQ(headers.get_("sec-istio-auth-userinfo"), ExpectedPayloadValue); // Verify the token is removed. @@ -82,10 +89,10 @@ TEST_F(AuthenticatorTest, TestOkJWTandCache) { // This test verifies the Jwt is forwarded if "forward" flag is set. TEST_F(AuthenticatorTest, TestForwardJwt) { - // Confirm forward_jwt flag + // Confit forward_jwt flag (*proto_config_.mutable_providers())[std::string(ProviderName)].set_forward(true); CreateAuthenticator(); - EXPECT_CALL(*fetcher_, fetch(_, _)) + EXPECT_CALL(*raw_fetcher_, fetch(_, _)) .WillOnce(Invoke( [this](const ::envoy::api::v2::core::HttpUri&, JwksFetcher::JwksReceiver& receiver) { receiver.onJwksSuccess(std::move(jwks_)); @@ -94,12 +101,7 @@ TEST_F(AuthenticatorTest, TestForwardJwt) { // Test OK pubkey and its cache auto headers = Http::TestHeaderMapImpl{{"Authorization", "Bearer " + std::string(GoodToken)}}; - MockAuthenticatorCallbacks mock_cb; - EXPECT_CALL(mock_cb, onComplete(_)).WillOnce(Invoke([](const Status& status) { - ASSERT_EQ(status, Status::Ok); - })); - - auth_->verify(headers, &mock_cb); + expectVerifyStatus(Status::Ok, headers); // Verify the token is NOT removed. EXPECT_TRUE(headers.Authorization()); @@ -107,91 +109,71 @@ TEST_F(AuthenticatorTest, TestForwardJwt) { // This test verifies the Jwt with non existing kid TEST_F(AuthenticatorTest, TestJwtWithNonExistKid) { - EXPECT_CALL(*fetcher_, fetch(_, _)) + EXPECT_CALL(*raw_fetcher_, fetch(_, _)) .WillOnce(Invoke( [this](const ::envoy::api::v2::core::HttpUri&, JwksFetcher::JwksReceiver& receiver) { receiver.onJwksSuccess(std::move(jwks_)); })); + // Test OK pubkey and its cache auto headers = Http::TestHeaderMapImpl{{"Authorization", "Bearer " + std::string(NonExistKidToken)}}; - MockAuthenticatorCallbacks mock_cb; - EXPECT_CALL(mock_cb, onComplete(_)).WillOnce(Invoke([](const Status& status) { - ASSERT_EQ(status, Status::JwtVerificationFail); - })); - - auth_->verify(headers, &mock_cb); + expectVerifyStatus(Status::JwtVerificationFail, headers); } // This test verifies if Jwt is missing, proper status is called. TEST_F(AuthenticatorTest, TestMissedJWT) { - EXPECT_CALL(*fetcher_, fetch(_, _)).Times(0); - MockAuthenticatorCallbacks mock_cb; - EXPECT_CALL(mock_cb_, onComplete(_)).WillOnce(Invoke([](const Status& status) { - ASSERT_EQ(status, Status::JwtMissed); - })); + EXPECT_CALL(*raw_fetcher_, fetch(_, _)).Times(0); // Empty headers. auto headers = Http::TestHeaderMapImpl{}; - auth_->verify(headers, &mock_cb_); + + expectVerifyStatus(Status::JwtMissed, headers); } // This test verifies if Jwt is invalid, JwtBadFormat status is returned. TEST_F(AuthenticatorTest, TestInvalidJWT) { - EXPECT_CALL(*fetcher_, fetch(_, _)).Times(0); - EXPECT_CALL(mock_cb_, onComplete(_)).WillOnce(Invoke([](const Status& status) { - ASSERT_EQ(status, Status::JwtBadFormat); - })); + EXPECT_CALL(*raw_fetcher_, fetch(_, _)).Times(0); std::string token = "invalidToken"; auto headers = Http::TestHeaderMapImpl{{"Authorization", "Bearer " + token}}; - auth_->verify(headers, &mock_cb_); + expectVerifyStatus(Status::JwtBadFormat, headers); } // This test verifies if Authorization header has invalid prefix, JwtMissed status is returned TEST_F(AuthenticatorTest, TestInvalidPrefix) { - EXPECT_CALL(*fetcher_, fetch(_, _)).Times(0); - EXPECT_CALL(mock_cb_, onComplete(_)).WillOnce(Invoke([](const Status& status) { - ASSERT_EQ(status, Status::JwtMissed); - })); + EXPECT_CALL(*raw_fetcher_, fetch(_, _)).Times(0); auto headers = Http::TestHeaderMapImpl{{"Authorization", "Bearer-invalid"}}; - auth_->verify(headers, &mock_cb_); + expectVerifyStatus(Status::JwtMissed, headers); } // This test verifies when a JWT is non-expiring without audience specified, JwtAudienceNotAllowed // is returned. TEST_F(AuthenticatorTest, TestNonExpiringJWT) { EXPECT_CALL(mock_factory_ctx_.cluster_manager_, httpAsyncClientForCluster(_)).Times(0); - EXPECT_CALL(mock_cb_, onComplete(_)).WillOnce(Invoke([](const Status& status) { - ASSERT_EQ(status, Status::JwtAudienceNotAllowed); - })); auto headers = Http::TestHeaderMapImpl{{"Authorization", "Bearer " + std::string(NonExpiringToken)}}; - auth_->verify(headers, &mock_cb_); + expectVerifyStatus(Status::JwtAudienceNotAllowed, headers); } // This test verifies when a JWT is expired, JwtExpired status is returned. TEST_F(AuthenticatorTest, TestExpiredJWT) { - EXPECT_CALL(*fetcher_, fetch(_, _)).Times(0); - EXPECT_CALL(mock_cb_, onComplete(_)).WillOnce(Invoke([](const Status& status) { - ASSERT_EQ(status, Status::JwtExpired); - })); + EXPECT_CALL(*raw_fetcher_, fetch(_, _)).Times(0); auto headers = Http::TestHeaderMapImpl{{"Authorization", "Bearer " + std::string(ExpiredToken)}}; - auth_->verify(headers, &mock_cb_); + expectVerifyStatus(Status::JwtExpired, headers); } // This test verifies when a JWT is not yet valid, JwtNotYetValid status is returned. TEST_F(AuthenticatorTest, TestNotYetValidJWT) { - EXPECT_CALL(*fetcher_, fetch(_, _)).Times(0); - EXPECT_CALL(mock_cb_, onComplete(Status::JwtNotYetValid)).Times(1); + EXPECT_CALL(*raw_fetcher_, fetch(_, _)).Times(0); auto headers = Http::TestHeaderMapImpl{{"Authorization", "Bearer " + std::string(NotYetValidToken)}}; - auth_->verify(headers, &mock_cb_); + expectVerifyStatus(Status::JwtNotYetValid, headers); } // This test verifies when an inline JWKS is misconfigured, JwksNoValidKeys is returns @@ -201,25 +183,19 @@ TEST_F(AuthenticatorTest, TestInvalidLocalJwks) { provider.mutable_local_jwks()->set_inline_string("invalid"); CreateAuthenticator(); - EXPECT_CALL(*fetcher_, fetch(_, _)).Times(0); - EXPECT_CALL(mock_cb_, onComplete(_)).WillOnce(Invoke([](const Status& status) { - ASSERT_EQ(status, Status::JwksNoValidKeys); - })); + EXPECT_CALL(*raw_fetcher_, fetch(_, _)).Times(0); auto headers = Http::TestHeaderMapImpl{{"Authorization", "Bearer " + std::string(GoodToken)}}; - auth_->verify(headers, &mock_cb_); + expectVerifyStatus(Status::JwksNoValidKeys, headers); } // This test verifies when a JWT is with invalid audience, JwtAudienceNotAllowed is returned. TEST_F(AuthenticatorTest, TestNonMatchAudJWT) { - EXPECT_CALL(*fetcher_, fetch(_, _)).Times(0); - EXPECT_CALL(mock_cb_, onComplete(_)).WillOnce(Invoke([](const Status& status) { - ASSERT_EQ(status, Status::JwtAudienceNotAllowed); - })); + EXPECT_CALL(*raw_fetcher_, fetch(_, _)).Times(0); auto headers = Http::TestHeaderMapImpl{{"Authorization", "Bearer " + std::string(InvalidAudToken)}}; - auth_->verify(headers, &mock_cb_); + expectVerifyStatus(Status::JwtAudienceNotAllowed, headers); } // This test verifies when Jwt issuer is not configured, JwtUnknownIssuer is returned. @@ -228,46 +204,41 @@ TEST_F(AuthenticatorTest, TestIssuerNotFound) { (*proto_config_.mutable_providers())[std::string(ProviderName)].set_issuer("other_issuer"); CreateAuthenticator(); - EXPECT_CALL(*fetcher_, fetch(_, _)).Times(0); - EXPECT_CALL(mock_cb_, onComplete(_)).WillOnce(Invoke([](const Status& status) { - ASSERT_EQ(status, Status::JwtUnknownIssuer); - })); + EXPECT_CALL(*raw_fetcher_, fetch(_, _)).Times(0); auto headers = Http::TestHeaderMapImpl{{"Authorization", "Bearer " + std::string(GoodToken)}}; - auth_->verify(headers, &mock_cb_); + expectVerifyStatus(Status::JwtUnknownIssuer, headers); } // This test verifies that when Jwks fetching fails, JwksFetchFail status is returned. TEST_F(AuthenticatorTest, TestPubkeyFetchFail) { - EXPECT_CALL(*fetcher_, fetch(_, _)) + EXPECT_CALL(*raw_fetcher_, fetch(_, _)) .WillOnce( Invoke([](const ::envoy::api::v2::core::HttpUri&, JwksFetcher::JwksReceiver& receiver) { receiver.onJwksError(JwksFetcher::JwksReceiver::Failure::InvalidJwks); })); - EXPECT_CALL(mock_cb_, onComplete(_)).WillOnce(Invoke([](const Status& status) { - ASSERT_EQ(status, Status::JwksFetchFail); - })); - auto headers = Http::TestHeaderMapImpl{{"Authorization", "Bearer " + std::string(GoodToken)}}; - auth_->verify(headers, &mock_cb_); + expectVerifyStatus(Status::JwksFetchFail, headers); Http::MessagePtr response_message(new Http::ResponseMessageImpl( Http::HeaderMapPtr{new Http::TestHeaderMapImpl{{":status", "401"}}})); } // This test verifies when a Jwks fetching is not completed yet, but onDestory() is called, -// onComplete() callback should not be called, but internal fetcher_->close() should be called. +// onComplete() callback should not be called, but internal request->cancel() should be called. // Most importantly, no crash. TEST_F(AuthenticatorTest, TestOnDestroy) { - EXPECT_CALL(*fetcher_, fetch(_, _)).Times(1); - EXPECT_CALL(*fetcher_, cancel()).Times(1); + EXPECT_CALL(*raw_fetcher_, fetch(_, _)).Times(1); - // onComplete() should not be called. - EXPECT_CALL(mock_cb_, onComplete(_)).Times(0); + // Cancel is called once. + EXPECT_CALL(*raw_fetcher_, cancel()).Times(1); auto headers = Http::TestHeaderMapImpl{{"Authorization", "Bearer " + std::string(GoodToken)}}; - auth_->verify(headers, &mock_cb_); + auto tokens = filter_config_->getExtractor().extract(headers); + // callback should not be called. + std::function on_complete_cb = [](const Status&) { FAIL(); }; + auth_->verify(headers, std::move(tokens), std::move(on_complete_cb)); // Destroy the authenticating process. auth_->onDestroy(); @@ -279,24 +250,132 @@ TEST_F(AuthenticatorTest, TestNoForwardPayloadHeader) { auto& provider0 = (*proto_config_.mutable_providers())[std::string(ProviderName)]; provider0.clear_forward_payload_header(); CreateAuthenticator(); - EXPECT_CALL(*fetcher_, fetch(_, _)) + EXPECT_CALL(*raw_fetcher_, fetch(_, _)) .WillOnce(Invoke( [this](const ::envoy::api::v2::core::HttpUri&, JwksFetcher::JwksReceiver& receiver) { receiver.onJwksSuccess(std::move(jwks_)); })); auto headers = Http::TestHeaderMapImpl{{"Authorization", "Bearer " + std::string(GoodToken)}}; - MockAuthenticatorCallbacks mock_cb; - EXPECT_CALL(mock_cb, onComplete(_)).WillOnce(Invoke([](const Status& status) { - ASSERT_EQ(status, Status::Ok); - })); - auth_->verify(headers, &mock_cb); + expectVerifyStatus(Status::Ok, headers); // Test when forward_payload_header is not set, the output should NOT // contain the sec-istio-auth-userinfo header. EXPECT_FALSE(headers.has("sec-istio-auth-userinfo")); } +// This test verifies that allow failed authenticator will verify all tokens. +TEST_F(AuthenticatorTest, TestAllowFailedMultipleTokens) { + auto& provider = (*proto_config_.mutable_providers())[std::string(ProviderName)]; + std::vector names = {"a", "b", "c"}; + for (const auto& it : names) { + auto header = provider.add_from_headers(); + header->set_name(it); + header->set_value_prefix("Bearer "); + } + + CreateAuthenticator(nullptr, absl::nullopt); + EXPECT_CALL(*raw_fetcher_, fetch(_, _)) + .WillOnce(Invoke( + [this](const ::envoy::api::v2::core::HttpUri&, JwksFetcher::JwksReceiver& receiver) { + receiver.onJwksSuccess(std::move(jwks_)); + })); + + auto headers = Http::TestHeaderMapImpl{ + {"a", "Bearer " + std::string(ExpiredToken)}, + {"b", "Bearer " + std::string(GoodToken)}, + {"c", "Bearer " + std::string(InvalidAudToken)}, + {":path", "/"}, + }; + expectVerifyStatus(Status::Ok, headers); + + EXPECT_TRUE(headers.has("a")); + EXPECT_FALSE(headers.has("b")); + EXPECT_TRUE(headers.has("c")); + + headers = Http::TestHeaderMapImpl{ + {"a", "Bearer " + std::string(GoodToken)}, + {"b", "Bearer " + std::string(GoodToken)}, + {"c", "Bearer " + std::string(GoodToken)}, + {":path", "/"}, + }; + expectVerifyStatus(Status::Ok, headers); + + EXPECT_FALSE(headers.has("a")); + EXPECT_FALSE(headers.has("b")); + EXPECT_FALSE(headers.has("c")); +} + +// This test verifies that allow failed authenticator will verify all tokens. +TEST_F(AuthenticatorTest, TestAllowFailedMultipleIssuers) { + auto& provider = (*proto_config_.mutable_providers())["other_provider"]; + provider.set_issuer("https://other.com"); + provider.add_audiences("other_service"); + auto& uri = *provider.mutable_remote_jwks()->mutable_http_uri(); + uri.set_uri("https://pubkey_server/pubkey_path"); + uri.set_cluster("pubkey_cluster"); + auto header = provider.add_from_headers(); + header->set_name("expired-auth"); + header->set_value_prefix("Bearer "); + header = provider.add_from_headers(); + header->set_name("other-auth"); + header->set_value_prefix("Bearer "); + + CreateAuthenticator(nullptr, absl::nullopt); + EXPECT_CALL(*raw_fetcher_, fetch(_, _)) + .Times(2) + .WillRepeatedly( + Invoke([](const ::envoy::api::v2::core::HttpUri&, JwksFetcher::JwksReceiver& receiver) { + ::google::jwt_verify::JwksPtr jwks = Jwks::createFrom(PublicKey, Jwks::JWKS); + EXPECT_TRUE(jwks->getStatus() == Status::Ok); + receiver.onJwksSuccess(std::move(jwks)); + })); + + auto headers = Http::TestHeaderMapImpl{ + {"Authorization", "Bearer " + std::string(GoodToken)}, + {"expired-auth", "Bearer " + std::string(ExpiredToken)}, + {"other-auth", "Bearer " + std::string(OtherGoodToken)}, + {":path", "/"}, + }; + expectVerifyStatus(Status::Ok, headers); + + EXPECT_FALSE(headers.has("Authorization")); + EXPECT_TRUE(headers.has("expired-auth")); + EXPECT_FALSE(headers.has("other-auth")); +} + +// 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>( + std::vector{"invalid_service"}); + CreateAuthenticator(check_audience.get()); + EXPECT_CALL(*raw_fetcher_, fetch(_, _)) + .WillOnce(Invoke( + [this](const ::envoy::api::v2::core::HttpUri&, JwksFetcher::JwksReceiver& receiver) { + receiver.onJwksSuccess(std::move(jwks_)); + })); + + auto headers = + Http::TestHeaderMapImpl{{"Authorization", "Bearer " + std::string(InvalidAudToken)}}; + expectVerifyStatus(Status::Ok, headers); + + headers = Http::TestHeaderMapImpl{{"Authorization", "Bearer " + std::string(GoodToken)}}; + expectVerifyStatus(Status::JwtAudienceNotAllowed, headers); +} + +// This test verifies that when invalid JWKS is fetched, an JWKS error status is returned. +TEST_F(AuthenticatorTest, TestInvalidPubkeyKey) { + EXPECT_CALL(*raw_fetcher_, fetch(_, _)) + .WillOnce( + Invoke([](const ::envoy::api::v2::core::HttpUri&, JwksFetcher::JwksReceiver& receiver) { + auto jwks = Jwks::createFrom(PublicKey, Jwks::PEM); + receiver.onJwksSuccess(std::move(jwks)); + })); + + auto headers = Http::TestHeaderMapImpl{{"Authorization", "Bearer " + std::string(GoodToken)}}; + expectVerifyStatus(Status::JwksPemBadBase64, headers); +} + } // namespace JwtAuthn } // namespace HttpFilters } // namespace Extensions diff --git a/test/extensions/filters/http/jwt_authn/extractor_test.cc b/test/extensions/filters/http/jwt_authn/extractor_test.cc index fe1937f3b71b2..6625e1fb4eb57 100644 --- a/test/extensions/filters/http/jwt_authn/extractor_test.cc +++ b/test/extensions/filters/http/jwt_authn/extractor_test.cc @@ -5,6 +5,7 @@ #include "test/test_common/utility.h" using ::envoy::config::filter::http::jwt_authn::v2alpha::JwtAuthentication; +using ::envoy::config::filter::http::jwt_authn::v2alpha::JwtProvider; using ::Envoy::Http::TestHeaderMapImpl; using ::testing::_; @@ -208,6 +209,32 @@ TEST_F(ExtractorTest, TestMultipleTokens) { EXPECT_EQ(tokens[4]->token(), "token3"); // from token_param param } +// Test selected extraction of multiple tokens. +TEST_F(ExtractorTest, TestExtractParam) { + auto headers = TestHeaderMapImpl{ + {":path", "/path?token_param=token3&access_token=token4"}, + {"token-header", "token2"}, + {"authorization", "Bearer token1"}, + {"prefix-header", "AAAtoken5"}, + }; + JwtProvider provider; + provider.set_issuer("foo"); + auto extractor = Extractor::create(provider); + auto tokens = extractor->extract(headers); + EXPECT_EQ(tokens.size(), 2); + EXPECT_EQ(tokens[0]->token(), "token1"); + EXPECT_EQ(tokens[1]->token(), "token4"); + auto header = provider.add_from_headers(); + header->set_name("prefix-header"); + header->set_value_prefix("AAA"); + provider.add_from_params("token_param"); + extractor = Extractor::create(provider); + tokens = extractor->extract(headers); + EXPECT_EQ(tokens.size(), 2); + EXPECT_EQ(tokens[0]->token(), "token5"); + EXPECT_EQ(tokens[1]->token(), "token3"); +} + } // namespace } // namespace JwtAuthn } // namespace HttpFilters diff --git a/test/extensions/filters/http/jwt_authn/filter_integration_test.cc b/test/extensions/filters/http/jwt_authn/filter_integration_test.cc index 3440c215d207f..c2e0a6cdc198c 100644 --- a/test/extensions/filters/http/jwt_authn/filter_integration_test.cc +++ b/test/extensions/filters/http/jwt_authn/filter_integration_test.cc @@ -106,6 +106,28 @@ TEST_P(LocalJwksIntegrationTest, ExpiredTokenHeadReply) { EXPECT_STREQ("", response->body().c_str()); } +// This test verifies a request is passed with a path that don't match any requirements. +TEST_P(LocalJwksIntegrationTest, NoRequiresPath) { + config_helper_.addFilter(getFilterConfig(true)); + initialize(); + + codec_client_ = makeHttpConnection(lookupPort("http")); + + auto response = codec_client_->makeHeaderOnlyRequest(Http::TestHeaderMapImpl{ + {":method", "GET"}, + {":path", "/foo"}, + {":scheme", "http"}, + {":authority", "host"}, + }); + + waitForNextUpstreamRequest(); + upstream_request_->encodeHeaders(Http::TestHeaderMapImpl{{":status", "200"}}, true); + + response->waitForEndStream(); + ASSERT_TRUE(response->complete()); + EXPECT_STREQ("200", response->headers().Status()->value().c_str()); +} + // The test case with a fake upstream for remote Jwks server. class RemoteJwksIntegrationTest : public HttpProtocolIntegrationTest { public: diff --git a/test/extensions/filters/http/jwt_authn/filter_test.cc b/test/extensions/filters/http/jwt_authn/filter_test.cc index 80bbe941fb61c..d1b1633751855 100644 --- a/test/extensions/filters/http/jwt_authn/filter_test.cc +++ b/test/extensions/filters/http/jwt_authn/filter_test.cc @@ -17,74 +17,100 @@ namespace Extensions { namespace HttpFilters { namespace JwtAuthn { +class MockMatcher : public Matcher { +public: + MOCK_CONST_METHOD1(matches, bool(const Http::HeaderMap& headers)); + MOCK_CONST_METHOD0(verifier, const VerifierPtr&()); +}; + +class MockFilterConfig : public FilterConfig { +public: + MockFilterConfig( + const ::envoy::config::filter::http::jwt_authn::v2alpha::JwtAuthentication& proto_config, + const std::string& stats_prefix, Server::Configuration::FactoryContext& context) + : FilterConfig(proto_config, stats_prefix, context) {} + MOCK_CONST_METHOD1(findMatcher, const MatcherConstSharedPtr(const Http::HeaderMap& headers)); +}; + class FilterTest : public ::testing::Test { public: void SetUp() { - config_ = ::std::make_shared(proto_config_, "", context_); - auto mock_auth = std::make_unique(); - raw_mock_auth_ = mock_auth.get(); - filter_ = std::make_unique(config_->stats(), std::move(mock_auth)); + mock_config_ = ::std::make_shared(proto_config_, "", mock_context_); + + mock_verifier_ = std::make_unique(); + raw_mock_verifier_ = static_cast(mock_verifier_.get()); + + filter_ = std::make_unique(mock_config_); filter_->setDecoderFilterCallbacks(filter_callbacks_); } + void setupMockConfig() { + EXPECT_CALL(*mock_config_.get(), findMatcher(_)).WillOnce(Invoke([&](const Http::HeaderMap&) { + auto mock_matcher = std::make_shared>(); + ON_CALL(*mock_matcher.get(), matches(_)).WillByDefault(Invoke([](const Http::HeaderMap&) { + return true; + })); + ON_CALL(*mock_matcher.get(), verifier()).WillByDefault(Invoke([&]() -> const VerifierPtr& { + return mock_verifier_; + })); + return mock_matcher; + })); + } + JwtAuthentication proto_config_; - NiceMock context_; - FilterConfigSharedPtr config_; + NiceMock mock_context_; + std::shared_ptr mock_config_; NiceMock filter_callbacks_; - MockAuthenticator* raw_mock_auth_{}; std::unique_ptr filter_; + VerifierPtr mock_verifier_; + MockVerifier* raw_mock_verifier_; + NiceMock verifier_callback_; }; -// This test verifies Authenticator::Callback is called inline with OK status. +// This test verifies Verifier::Callback is called inline with OK status. // All functions should return Continue. TEST_F(FilterTest, InlineOK) { - EXPECT_CALL(*raw_mock_auth_, sanitizePayloadHeaders(_)).Times(1); - + setupMockConfig(); // A successful authentication completed inline: callback is called inside verify(). - EXPECT_CALL(*raw_mock_auth_, verify(_, _)) - .WillOnce(Invoke([](Http::HeaderMap&, Authenticator::Callbacks* callback) { - callback->onComplete(Status::Ok); - })); + EXPECT_CALL(*raw_mock_verifier_, verify(_)).WillOnce(Invoke([](ContextSharedPtr context) { + context->callback()->onComplete(Status::Ok); + })); auto headers = Http::TestHeaderMapImpl{}; EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); - EXPECT_EQ(1U, config_->stats().allowed_.value()); + EXPECT_EQ(1U, mock_config_->stats().allowed_.value()); Buffer::OwnedImpl data(""); EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(data, false)); EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(headers)); } -// This test verifies Authenticator::Callback is called inline with a failure status. +// This test verifies Verifier::Callback is called inline with a failure status. // All functions should return Continue except decodeHeaders(), it returns StopIteraton. TEST_F(FilterTest, InlineFailure) { - EXPECT_CALL(*raw_mock_auth_, sanitizePayloadHeaders(_)).Times(1); - + setupMockConfig(); // A failed authentication completed inline: callback is called inside verify(). - EXPECT_CALL(*raw_mock_auth_, verify(_, _)) - .WillOnce(Invoke([](Http::HeaderMap&, Authenticator::Callbacks* callback) { - callback->onComplete(Status::JwtBadFormat); - })); + EXPECT_CALL(*raw_mock_verifier_, verify(_)).WillOnce(Invoke([](ContextSharedPtr context) { + context->callback()->onComplete(Status::JwtBadFormat); + })); auto headers = Http::TestHeaderMapImpl{}; EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, filter_->decodeHeaders(headers, false)); - EXPECT_EQ(1U, config_->stats().denied_.value()); + EXPECT_EQ(1U, mock_config_->stats().denied_.value()); Buffer::OwnedImpl data(""); EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(data, false)); EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(headers)); } -// This test verifies Authenticator::Callback is called with OK status after verify(). +// This test verifies Verifier::Callback is called with OK status after verify(). TEST_F(FilterTest, OutBoundOK) { - EXPECT_CALL(*raw_mock_auth_, sanitizePayloadHeaders(_)).Times(1); - - Authenticator::Callbacks* auth_cb{}; + setupMockConfig(); + Verifier::Callbacks* m_cb; // callback is saved, not called right - EXPECT_CALL(*raw_mock_auth_, verify(_, _)) - .WillOnce(Invoke([&auth_cb](Http::HeaderMap&, Authenticator::Callbacks* callback) { - auth_cb = callback; - })); + EXPECT_CALL(*raw_mock_verifier_, verify(_)).WillOnce(Invoke([&m_cb](ContextSharedPtr context) { + m_cb = context->callback(); + })); auto headers = Http::TestHeaderMapImpl{}; EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, filter_->decodeHeaders(headers, false)); @@ -94,23 +120,22 @@ TEST_F(FilterTest, OutBoundOK) { EXPECT_EQ(Http::FilterTrailersStatus::StopIteration, filter_->decodeTrailers(headers)); // Callback is called now with OK status. - auth_cb->onComplete(Status::Ok); + auto context = Verifier::createContext(headers, &verifier_callback_); + m_cb->onComplete(Status::Ok); - EXPECT_EQ(1U, config_->stats().allowed_.value()); + EXPECT_EQ(1U, mock_config_->stats().allowed_.value()); EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(data, false)); EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(headers)); } -// This test verifies Authenticator::Callback is called with a failure after verify(). +// This test verifies Verifier::Callback is called with a failure after verify(). TEST_F(FilterTest, OutBoundFailure) { - EXPECT_CALL(*raw_mock_auth_, sanitizePayloadHeaders(_)).Times(1); - - Authenticator::Callbacks* auth_cb{}; + setupMockConfig(); + Verifier::Callbacks* m_cb; // callback is saved, not called right - EXPECT_CALL(*raw_mock_auth_, verify(_, _)) - .WillOnce(Invoke([&auth_cb](Http::HeaderMap&, Authenticator::Callbacks* callback) { - auth_cb = callback; - })); + EXPECT_CALL(*raw_mock_verifier_, verify(_)).WillOnce(Invoke([&m_cb](ContextSharedPtr context) { + m_cb = context->callback(); + })); auto headers = Http::TestHeaderMapImpl{}; EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, filter_->decodeHeaders(headers, false)); @@ -119,21 +144,31 @@ TEST_F(FilterTest, OutBoundFailure) { EXPECT_EQ(Http::FilterDataStatus::StopIterationAndWatermark, filter_->decodeData(data, false)); EXPECT_EQ(Http::FilterTrailersStatus::StopIteration, filter_->decodeTrailers(headers)); + auto context = Verifier::createContext(headers, &verifier_callback_); // Callback is called now with a failure status. - auth_cb->onComplete(Status::JwtBadFormat); + m_cb->onComplete(Status::JwtBadFormat); - EXPECT_EQ(1U, config_->stats().denied_.value()); + EXPECT_EQ(1U, mock_config_->stats().denied_.value()); EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(data, false)); EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(headers)); // Should be OK to call the onComplete() again. - auth_cb->onComplete(Status::JwtBadFormat); + m_cb->onComplete(Status::JwtBadFormat); } -// This test verifies authenticator::onDestory() is called when Filter::onDestory() is called. -TEST_F(FilterTest, VerifyOnDestroy) { - EXPECT_CALL(*raw_mock_auth_, onDestroy()).Times(1); - filter_->onDestroy(); +// Test verifies that if no route matched requirement, then request is allowed. +TEST_F(FilterTest, TestNoRouteMatched) { + EXPECT_CALL(*mock_config_.get(), findMatcher(_)).WillOnce(Invoke([&](const Http::HeaderMap&) { + return nullptr; + })); + + auto headers = Http::TestHeaderMapImpl{}; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); + EXPECT_EQ(1U, mock_config_->stats().allowed_.value()); + + Buffer::OwnedImpl data(""); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(data, false)); + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(headers)); } } // namespace JwtAuthn diff --git a/test/extensions/filters/http/jwt_authn/group_verifier_test.cc b/test/extensions/filters/http/jwt_authn/group_verifier_test.cc new file mode 100644 index 0000000000000..38e86d81c7279 --- /dev/null +++ b/test/extensions/filters/http/jwt_authn/group_verifier_test.cc @@ -0,0 +1,482 @@ +#include "extensions/filters/http/jwt_authn/verifier.h" + +#include "test/extensions/filters/http/jwt_authn/mock.h" +#include "test/extensions/filters/http/jwt_authn/test_common.h" +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" + +using ::envoy::config::filter::http::jwt_authn::v2alpha::JwtAuthentication; +using ::google::jwt_verify::Status; +using ::testing::NiceMock; + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace JwtAuthn { +namespace { +const char AllWithAny[] = R"( +providers: + provider_1: + issuer: iss_1 + provider_2: + issuer: iss_2 + provider_3: + issuer: iss_3 +rules: +- match: { path: "/" } + requires: + requires_all: + requirements: + - requires_any: + requirements: + - provider_name: "provider_1" + - provider_name: "provider_2" + - provider_name: "provider_3" +)"; + +const char AnyWithAll[] = R"( +providers: + provider_1: + issuer: iss_1 + provider_2: + issuer: iss_2 + provider_3: + issuer: iss_3 + provider_4: + issuer: iss_4 +rules: +- match: { path: "/" } + requires: + requires_any: + requirements: + - requires_all: + requirements: + - provider_name: "provider_1" + - provider_name: "provider_2" + - requires_all: + requirements: + - provider_name: "provider_3" + - provider_name: "provider_4" +)"; + +typedef std::unordered_map StatusMap; + +constexpr auto allowfailed = "_allow_failed_"; + +class GroupVerifierTest : public ::testing::Test { +public: + void createVerifier() { + ON_CALL(mock_factory_, create(_, _, _)) + .WillByDefault(Invoke([&](const ::google::jwt_verify::CheckAudience*, + const absl::optional& provider, bool) { + return std::move(mock_auths_[provider ? provider.value() : allowfailed]); + })); + verifier_ = Verifier::create(proto_config_.rules(0).requires(), proto_config_.providers(), + mock_factory_, mock_extractor_); + ON_CALL(mock_extractor_, extract(_)).WillByDefault(Invoke([](const Http::HeaderMap&) { + return std::vector{}; + })); + } + void createSyncMockAuthsAndVerifier(const StatusMap& statuses) { + for (const auto& it : statuses) { + auto mock_auth = std::make_unique(); + EXPECT_CALL(*mock_auth.get(), doVerify(_, _, _)) + .WillOnce( + Invoke([status = it.second](Http::HeaderMap&, std::vector*, + AuthenticatorCallback callback) { callback(status); })); + EXPECT_CALL(*mock_auth.get(), onDestroy()).Times(1); + mock_auths_[it.first] = std::move(mock_auth); + } + createVerifier(); + } + + std::unordered_map + createAsyncMockAuthsAndVerifier(const std::vector& providers) { + std::unordered_map callbacks; + for (std::size_t i = 0; i < providers.size(); ++i) { + auto mock_auth = std::make_unique(); + EXPECT_CALL(*mock_auth.get(), doVerify(_, _, _)) + .WillOnce(Invoke([&callbacks, iss = providers[i]](Http::HeaderMap&, + std::vector*, + AuthenticatorCallback callback) { + callbacks[iss] = std::move(callback); + })); + EXPECT_CALL(*mock_auth.get(), onDestroy()).Times(1); + mock_auths_[providers[i]] = std::move(mock_auth); + } + createVerifier(); + return callbacks; + } + + JwtAuthentication proto_config_; + VerifierPtr verifier_; + MockVerifierCallbacks mock_cb_; + std::unordered_map> mock_auths_; + NiceMock mock_factory_; + ContextSharedPtr context_; + NiceMock mock_extractor_; +}; + +// Deeply nested anys that ends in provider name +TEST_F(GroupVerifierTest, DeeplyNestedAnys) { + const char config[] = R"( +providers: + example_provider: + issuer: https://example.com + audiences: + - example_service + - http://example_service1 + - https://example_service2/ + remote_jwks: + http_uri: + uri: https://pubkey_server/pubkey_path + cluster: pubkey_cluster + forward_payload_header: sec-istio-auth-userinfo + from_params: + - jwta + - jwtb + - jwtc +rules: +- match: { path: "/match" } + requires: + requires_any: + requirements: + - requires_any: + requirements: + - requires_any: + requirements: + - provider_name: "example_provider" +)"; + MessageUtil::loadFromYaml(config, proto_config_); + createSyncMockAuthsAndVerifier(StatusMap{{"example_provider", Status::Ok}}); + + EXPECT_CALL(mock_cb_, onComplete(Status::Ok)).Times(1); + auto headers = Http::TestHeaderMapImpl{ + {"sec-istio-auth-userinfo", ""}, + }; + context_ = Verifier::createContext(headers, &mock_cb_); + verifier_->verify(context_); + EXPECT_FALSE(headers.has("sec-istio-auth-userinfo")); +} + +// require alls that just ends +TEST_F(GroupVerifierTest, CanHandleUnexpectedEnd) { + const char config[] = R"( +providers: + example_provider: + issuer: https://example.com + audiences: + - example_service + - http://example_service1 + - https://example_service2/ + remote_jwks: + http_uri: + uri: https://pubkey_server/pubkey_path + cluster: pubkey_cluster + forward_payload_header: sec-istio-auth-userinfo +rules: +- match: { path: "/match" } + requires: + requires_all: + requirements: + - requires_all: +)"; + MessageUtil::loadFromYaml(config, proto_config_); + auto mock_auth = std::make_unique(); + EXPECT_CALL(*mock_auth.get(), doVerify(_, _, _)).Times(0); + mock_auths_["example_provider"] = std::move(mock_auth); + createVerifier(); + + EXPECT_CALL(mock_cb_, onComplete(Status::Ok)).Times(1); + auto headers = Http::TestHeaderMapImpl{}; + context_ = Verifier::createContext(headers, &mock_cb_); + verifier_->verify(context_); +} + +// test requires all with both auth returning OK +TEST_F(GroupVerifierTest, TestRequiresAll) { + MessageUtil::loadFromYaml(RequiresAllConfig, proto_config_); + createSyncMockAuthsAndVerifier( + StatusMap{{"example_provider", Status::Ok}, {"other_provider", Status::Ok}}); + + EXPECT_CALL(mock_cb_, onComplete(Status::Ok)).Times(1); + auto headers = Http::TestHeaderMapImpl{ + {"example-auth-userinfo", ""}, + {"other-auth-userinfo", ""}, + }; + context_ = Verifier::createContext(headers, &mock_cb_); + verifier_->verify(context_); + EXPECT_FALSE(headers.has("example-auth-userinfo")); + EXPECT_FALSE(headers.has("other-auth-userinfo")); +} + +// test requires all with first auth returning bad format +TEST_F(GroupVerifierTest, TestRequiresAllBadFormat) { + MessageUtil::loadFromYaml(RequiresAllConfig, proto_config_); + auto callbacks = createAsyncMockAuthsAndVerifier( + std::vector{"example_provider", "other_provider"}); + + EXPECT_CALL(mock_cb_, onComplete(Status::JwtBadFormat)).Times(1); + auto headers = Http::TestHeaderMapImpl{ + {"example-auth-userinfo", ""}, + {"other-auth-userinfo", ""}, + }; + context_ = Verifier::createContext(headers, &mock_cb_); + verifier_->verify(context_); + callbacks["example_provider"](Status::JwtBadFormat); + // can keep invoking callback + callbacks["other_provider"](Status::Ok); + callbacks["example_provider"](Status::Ok); + callbacks["other_provider"](Status::Ok); + EXPECT_FALSE(headers.has("example-auth-userinfo")); + EXPECT_FALSE(headers.has("other-auth-userinfo")); +} + +// test requires all with second auth returning missing jwt +TEST_F(GroupVerifierTest, TestRequiresAllMissing) { + MessageUtil::loadFromYaml(RequiresAllConfig, proto_config_); + auto callbacks = createAsyncMockAuthsAndVerifier( + std::vector{"example_provider", "other_provider"}); + + EXPECT_CALL(mock_cb_, onComplete(Status::JwtMissed)).Times(1); + auto headers = Http::TestHeaderMapImpl{ + {"example-auth-userinfo", ""}, + {"other-auth-userinfo", ""}, + }; + context_ = Verifier::createContext(headers, &mock_cb_); + verifier_->verify(context_); + callbacks["example_provider"](Status::Ok); + callbacks["other_provider"](Status::JwtMissed); + // can keep invoking callback + callbacks["example_provider"](Status::Ok); + callbacks["other_provider"](Status::Ok); + EXPECT_FALSE(headers.has("example-auth-userinfo")); + EXPECT_FALSE(headers.has("other-auth-userinfo")); +} + +// Test requrires all and mock auths simulate cache misses and async return of failure statuses. +TEST_F(GroupVerifierTest, TestRequiresAllBothFailed) { + MessageUtil::loadFromYaml(RequiresAllConfig, proto_config_); + auto callbacks = createAsyncMockAuthsAndVerifier( + std::vector{"example_provider", "other_provider"}); + + EXPECT_CALL(mock_cb_, onComplete(Status::JwtUnknownIssuer)).Times(1); + auto headers = Http::TestHeaderMapImpl{ + {"example-auth-userinfo", ""}, + {"other-auth-userinfo", ""}, + }; + context_ = Verifier::createContext(headers, &mock_cb_); + verifier_->verify(context_); + EXPECT_FALSE(headers.has("example-auth-userinfo")); + EXPECT_FALSE(headers.has("other-auth-userinfo")); + callbacks["example_provider"](Status::JwtUnknownIssuer); + callbacks["other_provider"](Status::JwtUnknownIssuer); +} + +// Test requires any with first auth returning OK. +TEST_F(GroupVerifierTest, TestRequiresAnyFirstAuthOK) { + MessageUtil::loadFromYaml(RequiresAnyConfig, proto_config_); + createSyncMockAuthsAndVerifier(StatusMap{{"example_provider", Status::Ok}}); + + EXPECT_CALL(mock_cb_, onComplete(Status::Ok)).Times(1); + auto headers = Http::TestHeaderMapImpl{ + {"example-auth-userinfo", ""}, + {"other-auth-userinfo", ""}, + }; + context_ = Verifier::createContext(headers, &mock_cb_); + verifier_->verify(context_); + EXPECT_FALSE(headers.has("example-auth-userinfo")); + EXPECT_TRUE(headers.has("other-auth-userinfo")); +} + +// Test requires any with last auth returning OK. +TEST_F(GroupVerifierTest, TestRequiresAnyLastAuthOk) { + MessageUtil::loadFromYaml(RequiresAnyConfig, proto_config_); + createSyncMockAuthsAndVerifier( + StatusMap{{"example_provider", Status::JwtUnknownIssuer}, {"other_provider", Status::Ok}}); + + EXPECT_CALL(mock_cb_, onComplete(Status::Ok)).Times(1); + auto headers = Http::TestHeaderMapImpl{ + {"example-auth-userinfo", ""}, + {"other-auth-userinfo", ""}, + }; + context_ = Verifier::createContext(headers, &mock_cb_); + verifier_->verify(context_); + EXPECT_FALSE(headers.has("example-auth-userinfo")); + EXPECT_FALSE(headers.has("other-auth-userinfo")); +} + +// Test requires any with both auth returning error. Requires any returns the error last recieved +// back to the caller. +TEST_F(GroupVerifierTest, TestRequiresAnyAllAuthFailed) { + MessageUtil::loadFromYaml(RequiresAnyConfig, proto_config_); + auto mock_auth = std::make_unique(); + createSyncMockAuthsAndVerifier(StatusMap{{"example_provider", Status::JwtHeaderBadKid}, + {"other_provider", Status::JwtUnknownIssuer}}); + + EXPECT_CALL(mock_cb_, onComplete(Status::JwtUnknownIssuer)).Times(1); + auto headers = Http::TestHeaderMapImpl{ + {"example-auth-userinfo", ""}, + {"other-auth-userinfo", ""}, + }; + context_ = Verifier::createContext(headers, &mock_cb_); + verifier_->verify(context_); + EXPECT_FALSE(headers.has("example-auth-userinfo")); + EXPECT_FALSE(headers.has("other-auth-userinfo")); +} + +// Test contains a 2 provider_name in a require any along with another provider_name in require all. +// Test simulates first require any is OK and proivder_name is OK. +TEST_F(GroupVerifierTest, TestAnyInAllFirstAnyIsOk) { + MessageUtil::loadFromYaml(AllWithAny, proto_config_); + createSyncMockAuthsAndVerifier(StatusMap{{"provider_1", Status::Ok}, {"provider_3", Status::Ok}}); + + EXPECT_CALL(mock_cb_, onComplete(Status::Ok)).Times(1); + auto headers = Http::TestHeaderMapImpl{}; + context_ = Verifier::createContext(headers, &mock_cb_); + verifier_->verify(context_); +} + +// Test contains a 2 provider_name in a require any along with another provider_name in require all. +// Test simulates first require any is OK and proivder_name is OK. +TEST_F(GroupVerifierTest, TestAnyInAllLastAnyIsOk) { + MessageUtil::loadFromYaml(AllWithAny, proto_config_); + createSyncMockAuthsAndVerifier(StatusMap{{"provider_1", Status::JwtUnknownIssuer}, + {"provider_2", Status::Ok}, + {"provider_3", Status::Ok}}); + + EXPECT_CALL(mock_cb_, onComplete(Status::Ok)).Times(1); + auto headers = Http::TestHeaderMapImpl{}; + context_ = Verifier::createContext(headers, &mock_cb_); + verifier_->verify(context_); +} + +// Test contains a 2 provider_name in a require any along with another provider_name in require all. +// Test simulates all require any OK and proivder_name is OK. +TEST_F(GroupVerifierTest, TestAnyInAllBothInRequireAnyIsOk) { + MessageUtil::loadFromYaml(AllWithAny, proto_config_); + auto callbacks = createAsyncMockAuthsAndVerifier( + std::vector{"provider_1", "provider_2", "provider_3"}); + + EXPECT_CALL(mock_cb_, onComplete(Status::Ok)).Times(1); + auto headers = Http::TestHeaderMapImpl{}; + context_ = Verifier::createContext(headers, &mock_cb_); + verifier_->verify(context_); + callbacks["provider_1"](Status::Ok); + callbacks["provider_2"](Status::Ok); + callbacks["provider_3"](Status::Ok); +} + +// Test contains a 2 provider_name in a require any along with another provider_name in require all. +// Test simulates all require any failed and proivder_name is OK. +TEST_F(GroupVerifierTest, TestAnyInAllBothInRequireAnyFailed) { + MessageUtil::loadFromYaml(AllWithAny, proto_config_); + auto callbacks = createAsyncMockAuthsAndVerifier( + std::vector{"provider_1", "provider_2", "provider_3"}); + + EXPECT_CALL(mock_cb_, onComplete(Status::JwksFetchFail)).Times(1); + auto headers = Http::TestHeaderMapImpl{}; + context_ = Verifier::createContext(headers, &mock_cb_); + verifier_->verify(context_); + callbacks["provider_1"](Status::JwksFetchFail); + callbacks["provider_2"](Status::JwksFetchFail); + callbacks["provider_3"](Status::Ok); +} + +// Test contains a requires any which in turn has 2 requires all. Mock auths simulate JWKs cache +// hits and inline return of errors. Requires any returns the error last recieved back to the +// caller. +TEST_F(GroupVerifierTest, TestAllInAnyBothRequireAllFailed) { + MessageUtil::loadFromYaml(AnyWithAll, proto_config_); + createSyncMockAuthsAndVerifier( + StatusMap{{"provider_1", Status::JwksFetchFail}, {"provider_3", Status::JwtExpired}}); + + EXPECT_CALL(mock_cb_, onComplete(Status::JwtExpired)).Times(1); + auto headers = Http::TestHeaderMapImpl{}; + context_ = Verifier::createContext(headers, &mock_cb_); + verifier_->verify(context_); +} + +// Test contains a requires any which in turn has 2 requires all. The first inner requires all is +// completed with OKs. Mock auths simulate JWKs cache misses and async return of OKs. +TEST_F(GroupVerifierTest, TestAllInAnyFirstAllIsOk) { + MessageUtil::loadFromYaml(AnyWithAll, proto_config_); + auto callbacks = createAsyncMockAuthsAndVerifier( + std::vector{"provider_1", "provider_2", "provider_3", "provider_4"}); + + EXPECT_CALL(mock_cb_, onComplete(Status::Ok)).Times(1); + auto headers = Http::TestHeaderMapImpl{}; + context_ = Verifier::createContext(headers, &mock_cb_); + verifier_->verify(context_); + callbacks["provider_2"](Status::Ok); + callbacks["provider_3"](Status::JwtMissed); + callbacks["provider_1"](Status::Ok); +} + +// Test contains a requires any which in turn has 2 requires all. The last inner requires all is +// completed with OKs. Mock auths simulate JWKs cache misses and async return of OKs. +TEST_F(GroupVerifierTest, TestAllInAnyLastAllIsOk) { + MessageUtil::loadFromYaml(AnyWithAll, proto_config_); + auto callbacks = createAsyncMockAuthsAndVerifier( + std::vector{"provider_1", "provider_2", "provider_3", "provider_4"}); + + EXPECT_CALL(mock_cb_, onComplete(Status::Ok)).Times(1); + auto headers = Http::TestHeaderMapImpl{}; + context_ = Verifier::createContext(headers, &mock_cb_); + verifier_->verify(context_); + callbacks["provider_3"](Status::Ok); + callbacks["provider_4"](Status::Ok); + callbacks["provider_2"](Status::JwtExpired); +} + +// Test contains a requires any which in turn has 2 requires all. The both inner requires all are +// completed with OKs. Mock auths simulate JWKs cache misses and async return of OKs. +TEST_F(GroupVerifierTest, TestAllInAnyBothRequireAllAreOk) { + MessageUtil::loadFromYaml(AnyWithAll, proto_config_); + auto callbacks = createAsyncMockAuthsAndVerifier( + std::vector{"provider_1", "provider_2", "provider_3", "provider_4"}); + + EXPECT_CALL(mock_cb_, onComplete(Status::Ok)).Times(1); + auto headers = Http::TestHeaderMapImpl{}; + context_ = Verifier::createContext(headers, &mock_cb_); + verifier_->verify(context_); + callbacks["provider_1"](Status::Ok); + callbacks["provider_2"](Status::Ok); + callbacks["provider_3"](Status::Ok); + callbacks["provider_4"](Status::Ok); +} + +// Test require any with additional allow all +TEST_F(GroupVerifierTest, TestRequiresAnyWithAllowAll) { + MessageUtil::loadFromYaml(RequiresAnyConfig, proto_config_); + proto_config_.mutable_rules(0) + ->mutable_requires() + ->mutable_requires_any() + ->add_requirements() + ->mutable_allow_missing_or_failed(); + + auto callbacks = createAsyncMockAuthsAndVerifier( + std::vector{"example_provider", "other_provider"}); + auto mock_auth = std::make_unique(); + EXPECT_CALL(*mock_auth.get(), doVerify(_, _, _)) + .WillOnce(Invoke( + [&](Http::HeaderMap&, std::vector*, AuthenticatorCallback callback) { + callbacks[allowfailed] = std::move(callback); + })); + EXPECT_CALL(*mock_auth.get(), onDestroy()).Times(1); + mock_auths_[allowfailed] = std::move(mock_auth); + EXPECT_CALL(mock_cb_, onComplete(Status::Ok)).Times(1); + + auto headers = Http::TestHeaderMapImpl{}; + context_ = Verifier::createContext(headers, &mock_cb_); + verifier_->verify(context_); + callbacks[allowfailed](Status::Ok); + // with requires any, if any inner verifier returns OK the whole any verifier should return OK. +} + +} // namespace +} // namespace JwtAuthn +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy 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 0c88776b6e8e5..fdec7c6ea2246 100644 --- a/test/extensions/filters/http/jwt_authn/jwks_cache_test.cc +++ b/test/extensions/filters/http/jwt_authn/jwks_cache_test.cc @@ -133,6 +133,12 @@ TEST_F(JwksCacheTest, TestAudiences) { EXPECT_FALSE(jwks->areAudiencesAllowed({"wrong-audience1", "wrong-audience2"})); } +// Test findByProvider +TEST_F(JwksCacheTest, TestFindByProvider) { + EXPECT_TRUE(cache_->findByProvider(ProviderName) != nullptr); + EXPECT_TRUE(cache_->findByProvider("other-provider") == nullptr); +} + } // namespace } // namespace JwtAuthn } // namespace HttpFilters diff --git a/test/extensions/filters/http/jwt_authn/matcher_test.cc b/test/extensions/filters/http/jwt_authn/matcher_test.cc new file mode 100644 index 0000000000000..0b11b632e8593 --- /dev/null +++ b/test/extensions/filters/http/jwt_authn/matcher_test.cc @@ -0,0 +1,167 @@ +#include "common/protobuf/utility.h" + +#include "extensions/filters/http/jwt_authn/matcher.h" + +#include "test/extensions/filters/http/jwt_authn/mock.h" +#include "test/extensions/filters/http/jwt_authn/test_common.h" +#include "test/test_common/utility.h" + +using ::envoy::api::v2::route::RouteMatch; +using ::envoy::config::filter::http::jwt_authn::v2alpha::JwtProvider; +using ::envoy::config::filter::http::jwt_authn::v2alpha::JwtRequirement; +using ::envoy::config::filter::http::jwt_authn::v2alpha::RequirementRule; +using ::Envoy::Http::TestHeaderMapImpl; +using ::testing::_; +using ::testing::Invoke; +using ::testing::NiceMock; + +using ::google::jwt_verify::Status; + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace JwtAuthn { +namespace { + +class MatcherTest : public ::testing::Test { +public: + NiceMock mock_factory_; + MockExtractor extractor_; +}; + +TEST_F(MatcherTest, TestMatchPrefix) { + const char config[] = R"(match: + prefix: "/match")"; + RequirementRule rule; + MessageUtil::loadFromYaml(config, rule); + MatcherConstSharedPtr matcher = Matcher::create( + rule, Protobuf::Map(), mock_factory_, extractor_); + auto headers = TestHeaderMapImpl{{":path", "/match/this"}}; + EXPECT_TRUE(matcher->matches(headers)); + headers = TestHeaderMapImpl{{":path", "/MATCH"}}; + EXPECT_FALSE(matcher->matches(headers)); + headers = TestHeaderMapImpl{{":path", "/matching"}}; + EXPECT_TRUE(matcher->matches(headers)); + headers = TestHeaderMapImpl{{":path", "/matc"}}; + EXPECT_FALSE(matcher->matches(headers)); + headers = TestHeaderMapImpl{{":path", "/no"}}; + EXPECT_FALSE(matcher->matches(headers)); + EXPECT_FALSE(matcher->verifier() == nullptr); +} + +TEST_F(MatcherTest, TestMatchRegex) { + const char config[] = R"(match: + regex: "/[^c][au]t")"; + RequirementRule rule; + MessageUtil::loadFromYaml(config, rule); + MatcherConstSharedPtr matcher = Matcher::create( + rule, Protobuf::Map(), mock_factory_, extractor_); + auto headers = TestHeaderMapImpl{{":path", "/but"}}; + EXPECT_TRUE(matcher->matches(headers)); + headers = TestHeaderMapImpl{{":path", "/mat?ok=bye"}}; + EXPECT_TRUE(matcher->matches(headers)); + headers = TestHeaderMapImpl{{":path", "/maut"}}; + EXPECT_FALSE(matcher->matches(headers)); + headers = TestHeaderMapImpl{{":path", "/cut"}}; + EXPECT_FALSE(matcher->matches(headers)); + headers = TestHeaderMapImpl{{":path", "/mut/"}}; + EXPECT_FALSE(matcher->matches(headers)); + EXPECT_FALSE(matcher->verifier() == nullptr); +} + +TEST_F(MatcherTest, TestMatchPath) { + const char config[] = R"(match: + path: "/match" + case_sensitive: false)"; + RequirementRule rule; + MessageUtil::loadFromYaml(config, rule); + MatcherConstSharedPtr matcher = Matcher::create( + rule, Protobuf::Map(), mock_factory_, extractor_); + auto headers = TestHeaderMapImpl{{":path", "/match"}}; + EXPECT_TRUE(matcher->matches(headers)); + headers = TestHeaderMapImpl{{":path", "/MATCH"}}; + EXPECT_TRUE(matcher->matches(headers)); + headers = TestHeaderMapImpl{{":path", "/match?ok=bye"}}; + EXPECT_TRUE(matcher->matches(headers)); + headers = TestHeaderMapImpl{{":path", "/matc"}}; + EXPECT_FALSE(matcher->matches(headers)); + headers = TestHeaderMapImpl{{":path", "/match/"}}; + EXPECT_FALSE(matcher->matches(headers)); + headers = TestHeaderMapImpl{{":path", "/matching"}}; + EXPECT_FALSE(matcher->matches(headers)); + EXPECT_FALSE(matcher->verifier() == nullptr); +} + +TEST_F(MatcherTest, TestMatchQuery) { + const char config[] = R"(match: + prefix: "/" + query_parameters: + - name: foo + value: bar)"; + RequirementRule rule; + MessageUtil::loadFromYaml(config, rule); + MatcherConstSharedPtr matcher = Matcher::create( + rule, Protobuf::Map(), mock_factory_, extractor_); + auto headers = TestHeaderMapImpl{{":path", "/boo?foo=bar"}}; + EXPECT_TRUE(matcher->matches(headers)); + headers = TestHeaderMapImpl{{":path", "/boo?ok=bye"}}; + EXPECT_FALSE(matcher->matches(headers)); + headers = TestHeaderMapImpl{{":path", "/foo?bar=bar"}}; + EXPECT_FALSE(matcher->matches(headers)); + headers = TestHeaderMapImpl{{":path", "/boo?foo"}}; + EXPECT_FALSE(matcher->matches(headers)); + headers = TestHeaderMapImpl{{":path", "/boo?bar=foo"}}; + EXPECT_FALSE(matcher->matches(headers)); + EXPECT_FALSE(matcher->verifier() == nullptr); +} + +TEST_F(MatcherTest, TestMatchHeader) { + const char config[] = R"(match: + prefix: "/" + headers: + - name: a)"; + RequirementRule rule; + MessageUtil::loadFromYaml(config, rule); + MatcherConstSharedPtr matcher = Matcher::create( + rule, Protobuf::Map(), mock_factory_, extractor_); + auto headers = TestHeaderMapImpl{{":path", "/"}, {"a", ""}}; + EXPECT_TRUE(matcher->matches(headers)); + headers = TestHeaderMapImpl{{":path", "/"}, {"a", "some"}, {"b", ""}}; + EXPECT_TRUE(matcher->matches(headers)); + headers = TestHeaderMapImpl{{":path", "/"}, {"aa", ""}}; + EXPECT_FALSE(matcher->matches(headers)); + headers = TestHeaderMapImpl{{":path", "/"}}; + EXPECT_FALSE(matcher->matches(headers)); + headers = TestHeaderMapImpl{{":path", "/"}, {"", ""}}; + EXPECT_FALSE(matcher->matches(headers)); + EXPECT_FALSE(matcher->verifier() == nullptr); +} + +TEST_F(MatcherTest, TestMatchPathAndHeader) { + const char config[] = R"(match: + path: "/boo" + query_parameters: + - name: foo + value: bar)"; + RequirementRule rule; + MessageUtil::loadFromYaml(config, rule); + MatcherConstSharedPtr matcher = Matcher::create( + rule, Protobuf::Map(), mock_factory_, extractor_); + auto headers = TestHeaderMapImpl{{":path", "/boo?foo=bar"}}; + EXPECT_TRUE(matcher->matches(headers)); + headers = TestHeaderMapImpl{{":path", "/boo?ok=bye"}}; + EXPECT_FALSE(matcher->matches(headers)); + headers = TestHeaderMapImpl{{":path", "/foo?bar=bar"}}; + EXPECT_FALSE(matcher->matches(headers)); + headers = TestHeaderMapImpl{{":path", "/boo?foo"}}; + EXPECT_FALSE(matcher->matches(headers)); + headers = TestHeaderMapImpl{{":path", "/boo?bar=foo"}}; + EXPECT_FALSE(matcher->matches(headers)); + EXPECT_FALSE(matcher->verifier() == nullptr); +} + +} // namespace +} // namespace JwtAuthn +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/jwt_authn/mock.h b/test/extensions/filters/http/jwt_authn/mock.h index 126b271e06095..ec437b721d652 100644 --- a/test/extensions/filters/http/jwt_authn/mock.h +++ b/test/extensions/filters/http/jwt_authn/mock.h @@ -1,24 +1,77 @@ #include "extensions/filters/http/jwt_authn/authenticator.h" +#include "test/mocks/upstream/mocks.h" + #include "gmock/gmock.h" +using ::google::jwt_verify::Status; + namespace Envoy { namespace Extensions { namespace HttpFilters { namespace JwtAuthn { -class MockAuthenticatorCallbacks : public Authenticator::Callbacks { +class MockAuthFactory : public AuthFactory { public: - MOCK_METHOD1(onComplete, void(const ::google::jwt_verify::Status& status)); + MOCK_CONST_METHOD3(create, AuthenticatorPtr(const ::google::jwt_verify::CheckAudience*, + const absl::optional&, bool)); }; class MockAuthenticator : public Authenticator { public: - MOCK_METHOD2(verify, void(Http::HeaderMap& headers, Authenticator::Callbacks* callback)); + MOCK_METHOD3(doVerify, void(Http::HeaderMap& headers, std::vector* tokens, + std::function callback)); + + void verify(Http::HeaderMap& headers, std::vector&& tokens, + AuthenticatorCallback callback) { + doVerify(headers, &tokens, std::move(callback)); + } + MOCK_METHOD0(onDestroy, void()); +}; + +class MockVerifierCallbacks : public Verifier::Callbacks { +public: + MOCK_METHOD1(onComplete, void(const Status& status)); +}; + +class MockVerifier : public Verifier { +public: + MOCK_CONST_METHOD1(verify, void(ContextSharedPtr context)); +}; + +class MockExtractor : public Extractor { +public: + MOCK_CONST_METHOD1(extract, std::vector(const Http::HeaderMap& headers)); MOCK_CONST_METHOD1(sanitizePayloadHeaders, void(Http::HeaderMap& headers)); }; +// A mock HTTP upstream with response body. +class MockUpstream { +public: + MockUpstream(Upstream::MockClusterManager& mock_cm, const std::string& response_body) + : request_(&mock_cm.async_client_), response_body_(response_body) { + ON_CALL(mock_cm.async_client_, send_(_, _, _)) + .WillByDefault(Invoke([this](Http::MessagePtr&, Http::AsyncClient::Callbacks& cb, + const absl::optional&) + -> Http::AsyncClient::Request* { + Http::MessagePtr response_message(new Http::ResponseMessageImpl( + Http::HeaderMapPtr{new Http::TestHeaderMapImpl{{":status", "200"}}})); + response_message->body().reset(new Buffer::OwnedImpl(response_body_)); + cb.onSuccess(std::move(response_message)); + called_count_++; + return &request_; + })); + } + + int called_count() const { return called_count_; } + +private: + Http::MockAsyncClientRequest request_; + std::string response_body_; + int called_count_{}; +}; + } // namespace JwtAuthn } // namespace HttpFilters } // namespace Extensions diff --git a/test/extensions/filters/http/jwt_authn/provider_verifier_test.cc b/test/extensions/filters/http/jwt_authn/provider_verifier_test.cc new file mode 100644 index 0000000000000..ddc454c547143 --- /dev/null +++ b/test/extensions/filters/http/jwt_authn/provider_verifier_test.cc @@ -0,0 +1,138 @@ +#include "extensions/filters/http/jwt_authn/filter_config.h" +#include "extensions/filters/http/jwt_authn/verifier.h" + +#include "test/extensions/filters/http/jwt_authn/mock.h" +#include "test/extensions/filters/http/jwt_authn/test_common.h" +#include "test/mocks/server/mocks.h" +#include "test/mocks/upstream/mocks.h" +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" + +using ::envoy::config::filter::http::jwt_authn::v2alpha::JwtAuthentication; +using ::google::jwt_verify::Status; +using ::testing::NiceMock; + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace JwtAuthn { + +class ProviderVerifierTest : public ::testing::Test { +public: + void createVerifier() { + filter_config_ = ::std::make_shared(proto_config_, "", mock_factory_ctx_); + verifier_ = Verifier::create(proto_config_.rules(0).requires(), proto_config_.providers(), + *filter_config_, filter_config_->getExtractor()); + } + + JwtAuthentication proto_config_; + FilterConfigSharedPtr filter_config_; + VerifierPtr verifier_; + NiceMock mock_factory_ctx_; + ContextSharedPtr context_; + MockVerifierCallbacks mock_cb_; +}; + +TEST_F(ProviderVerifierTest, TestOkJWT) { + MessageUtil::loadFromYaml(ExampleConfig, proto_config_); + createVerifier(); + MockUpstream mock_pubkey(mock_factory_ctx_.cluster_manager_, PublicKey); + + EXPECT_CALL(mock_cb_, onComplete(Status::Ok)).Times(1); + + auto headers = Http::TestHeaderMapImpl{ + {"Authorization", "Bearer " + std::string(GoodToken)}, + {"sec-istio-auth-userinfo", ""}, + }; + context_ = Verifier::createContext(headers, &mock_cb_); + verifier_->verify(context_); + EXPECT_EQ(ExpectedPayloadValue, headers.get_("sec-istio-auth-userinfo")); +} + +TEST_F(ProviderVerifierTest, TestMissedJWT) { + MessageUtil::loadFromYaml(ExampleConfig, proto_config_); + createVerifier(); + + EXPECT_CALL(mock_cb_, onComplete(Status::JwtMissed)).Times(1); + + auto headers = Http::TestHeaderMapImpl{{"sec-istio-auth-userinfo", ""}}; + context_ = Verifier::createContext(headers, &mock_cb_); + verifier_->verify(context_); + EXPECT_FALSE(headers.has("sec-istio-auth-userinfo")); +} + +// This test verifies that JWT must be issued by the provider specified in the requirement. +TEST_F(ProviderVerifierTest, TestTokenRequirementProviderMismatch) { + const char config[] = R"( +providers: + example_provider: + issuer: https://example.com + audiences: + - example_service + - http://example_service1 + - https://example_service2/ + remote_jwks: + http_uri: + uri: https://pubkey_server/pubkey_path + cluster: pubkey_cluster + forward_payload_header: example-auth-userinfo + other_provider: + issuer: other_issuer + forward_payload_header: other-auth-userinfo +rules: +- match: + path: "/" + requires: + provider_name: "other_provider" +)"; + MessageUtil::loadFromYaml(config, proto_config_); + createVerifier(); + + EXPECT_CALL(mock_cb_, onComplete(Status::JwtUnknownIssuer)).Times(1); + + auto headers = Http::TestHeaderMapImpl{ + {"Authorization", "Bearer " + std::string(GoodToken)}, + {"example-auth-userinfo", ""}, + {"other-auth-userinfo", ""}, + }; + context_ = Verifier::createContext(headers, &mock_cb_); + verifier_->verify(context_); + EXPECT_TRUE(headers.has("example-auth-userinfo")); + EXPECT_FALSE(headers.has("other-auth-userinfo")); +} + +// This test verifies that JWT requirement can override audiences +TEST_F(ProviderVerifierTest, TestRequiresProviderWithAudiences) { + MessageUtil::loadFromYaml(ExampleConfig, proto_config_); + auto* requires = + proto_config_.mutable_rules(0)->mutable_requires()->mutable_provider_and_audiences(); + requires->set_provider_name("example_provider"); + requires->add_audiences("invalid_service"); + createVerifier(); + MockUpstream mock_pubkey(mock_factory_ctx_.cluster_manager_, PublicKey); + + EXPECT_CALL(mock_cb_, onComplete(_)) + .WillOnce( + Invoke([](const Status& status) { ASSERT_EQ(status, Status::JwtAudienceNotAllowed); })) + .WillOnce(Invoke([](const Status& status) { ASSERT_EQ(status, Status::Ok); })); + + auto headers = Http::TestHeaderMapImpl{{"Authorization", "Bearer " + std::string(GoodToken)}}; + verifier_->verify(Verifier::createContext(headers, &mock_cb_)); + headers = Http::TestHeaderMapImpl{{"Authorization", "Bearer " + std::string(InvalidAudToken)}}; + verifier_->verify(Verifier::createContext(headers, &mock_cb_)); +} + +// This test verifies that requirement referencing nonexistent provider will throw exception +TEST_F(ProviderVerifierTest, TestRequiresNonexistentProvider) { + MessageUtil::loadFromYaml(ExampleConfig, proto_config_); + proto_config_.mutable_rules(0)->mutable_requires()->set_provider_name("nosuchprovider"); + + EXPECT_THROW(::std::make_shared(proto_config_, "", mock_factory_ctx_), + EnvoyException); +} + +} // namespace JwtAuthn +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/jwt_authn/test_common.h b/test/extensions/filters/http/jwt_authn/test_common.h index 8cad51b755085..a5156b079653e 100644 --- a/test/extensions/filters/http/jwt_authn/test_common.h +++ b/test/extensions/filters/http/jwt_authn/test_common.h @@ -5,6 +5,36 @@ namespace Extensions { namespace HttpFilters { namespace JwtAuthn { +// RS256 private key +//-----BEGIN PRIVATE KEY----- +// MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC6n3u6qsX0xY49 +// o+TBJoF64A8s6v0UpxpYZ1UQbNDh/dmrlYpVmjDH1MIHGYiY0nWqZSLXekHyi3Az +// +XmV9jUAUEzFVtAJRee0ui+ENqJK9injAYOMXNCJgD6lSryHoxRkGeGV5iuRTteU +// IHA1XI3yo0ySksDsoVljP7jzoadXY0gknH/gEZrcd0rBAbGLa2O5CxC9qjlbjGZJ +// VpoRaikHAzLZCaWFIVC49SlNrLBOpRxSr/pJ8AeFnggNr8XER3ZzbPyAUa1+y31x +// jeVFh/5z9l1uhjeao31K7f6PfPmvZIdaWEH8s0CPJaUEay9sY+VOoPOJhDBk3hoa +// ypUpBv1XAgMBAAECggEAc5HaJJIm/trsqD17pyV6X6arnyxyx7xn80Eii4ZnoNv8 +// VWbJARP4i3e1JIJqdgE3PutctUYP2u0A8h7XbcfHsMcJk9ecA3IX+HKohF71CCkD +// bYH9fgnoVo5lvSTYNcMHGKpyacrdRiImHKQt+M21VgJMpCRfdurAmVbX6YA9Sj6w +// SBFrZbWkBHiHg7w++xKr+VeTHW/8fXI5bvSPAm/XB6dDKAcSXYiJJJhIoaVR9cHn +// 1ePRDLpEwfDpBHeepd/S3qR37mIbHmo8SVytDY2xTUaIoaRfXRWGMYSyxl0y4RsZ +// Vo6Tp9Tj2fyohvB/S+lE34zhxnsHToK2JZvPeoyHCQKBgQDyEcjaUZiPdx7K63CT +// d57QNYC6DTjtKWnfO2q/vAVyAPwS30NcVuXj3/1yc0L+eExpctn8tcLfvDi1xZPY +// dW2L3SZKgRJXL+JHTCEkP8To/qNLhBqitcKYwp0gtpoZbUjZdZwn18QJx7Mw/nFC +// lJhSYRl+FjVolY3qBaS6eD7imwKBgQDFXNmeAV5FFF0FqGRsLYl0hhXTR6Hi/hKQ +// OyRALBW9LUKbsazwWEFGRlqbEWd1OcOF5SSV4d3u7wLQRTDeNELXUFvivok12GR3 +// gNl9nDJ5KKYGFmqxM0pzfbT5m3Lsrr2FTIq8gM9GBpQAOmzQIkEu62yELtt2rRf0 +// 1pTh+UbN9QKBgF88kAEUySjofLzpFElwbpML+bE5MoRcHsMs5Tq6BopryMDEBgR2 +// S8vzfAtjPaBQQ//Yp9q8yAauTsF1Ek2/JXI5d68oSMb0l9nlIcTZMedZB3XWa4RI +// bl8bciZEsSv/ywGDPASQ5xfR8bX85SKEw8jlWto4cprK/CJuRfj3BgaxAoGAAmQf +// ltR5aejXP6xMmyrqEWlWdlrV0UQ2wVyWEdj24nXb6rr6V2caU1mi22IYmMj8X3Dp +// Qo+b+rsWk6Ni9i436RfmJRcd3nMitHfxKp5r1h/x8vzuifsPGdsaCDQj7k4nqafF +// vobo+/Y0cNREYTkpBQKBLBDNQ+DQ+3xmDV7RxskCgYBCo6u2b/DZWFLoq3VpAm8u +// 1ZgL8qxY/bbyA02IKF84QPFczDM5wiLjDGbGnOcIYYMvTHf1LJU4FozzYkB0GicX +// Y0tBQIHaaLWbPk1RZdPfR9kAp16iwk8H+V4UVjLfsTP7ocEfNCzZztmds83h8mTL +// DSwE5aY76Cs8XLcF/GNJRQ== +//-----END PRIVATE KEY----- + // A good public key const char PublicKey[] = R"( { @@ -47,6 +77,11 @@ const char ExampleConfig[] = R"( cache_duration: seconds: 600 forward_payload_header: sec-istio-auth-userinfo +rules: +- match: + path: "/" + requires: + provider_name: "example_provider" )"; // The name of provider for above config. @@ -126,10 +161,97 @@ const char NonExistKidToken[] = "CNOnL0AjQKe9IGblJrMuouqYYS0zEWwmOVUWUSxQkoLpldQUVefcfjQeGjz8IlvktRa77FYe" "xfP590ACPyXrivtsxg"; +// {"iss":"https://other.com","sub":"test@other.com","aud":"other_service","exp":2001001001} +const char OtherGoodToken[] = + "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9." + "eyJpc3MiOiJodHRwczovL290aGVyLmNvbSIsInN1YiI6InRlc3RAb3RoZXIuY29tIiwiYXVkIjoib3RoZXJfc2VydmljZS" + "IsImV4cCI6MjAwMTAwMTAwMX0.R0GR2rnRTg_gWzDvuO-BXVMmw3-vyBspV_kUQ4zvIdO-_" + "1icaWzbioPTPEyoViWuErNYxaZ5YFBoD6Zk_hIe1YWoSJr9QRwxWA4CWcasJdBXPq2mMETt8VjAiXE_" + "aIrJOLIlP786GLjVgTsnvhaDUJyU7xUdoi9HRjEBYcdjNPvxJutoby8MypAkwdGxjl4H4Z01gomgWyUDRRy47OKI_" + "buwXk5M6d-" + "drRvLcvlT5gB4adOIOlmhm8xtXgYpvqrXfmMJCHbP9no7JATFaTEAkmA3OOxDsaOju4BFgMtRZtDM8p12QQG0rFl_FE-" + "2FqYX9qA4q41HJ4vxTSxgObeLGA"; + // Expected base64 payload value. const char ExpectedPayloadValue[] = "eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwic3ViIjoidGVzdEBleGFtcG" "xlLmNvbSIsImV4cCI6MjAwMTAwMTAwMSwiYXVkIjoiZXhhbXBsZV9zZXJ2" "aWNlIn0"; +// Config with requires_all requirement +const char RequiresAllConfig[] = R"( +providers: + example_provider: + issuer: https://example.com + audiences: + - example_service + - http://example_service1 + - https://example_service2/ + remote_jwks: + http_uri: + uri: https://pubkey_server/pubkey_path + cluster: pubkey_cluster + from_params: ["jwt_a"] + forward_payload_header: example-auth-userinfo + other_provider: + issuer: https://other.com + audiences: + - other_service + remote_jwks: + http_uri: + uri: https://pubkey_server/pubkey_path + cluster: pubkey_cluster + from_params: ["jwt_b"] + forward_payload_header: other-auth-userinfo +rules: +- match: + path: "/requires-all" + requires: + requires_all: + requirements: + - provider_name: "example_provider" + - provider_name: "other_provider" +)"; +// Config with requires_any requirement +const char RequiresAnyConfig[] = R"( +providers: + example_provider: + issuer: https://example.com + audiences: + - example_service + - http://example_service1 + - https://example_service2/ + remote_jwks: + http_uri: + uri: https://pubkey_server/pubkey_path + cluster: pubkey_cluster + from_headers: + - name: a + value_prefix: "Bearer " + - name: b + value_prefix: "Bearer " + forward_payload_header: example-auth-userinfo + other_provider: + issuer: https://other.com + audiences: + - other_service + remote_jwks: + http_uri: + uri: https://pubkey_server/pubkey_path + cluster: pubkey_cluster + from_headers: + - name: a + value_prefix: "Bearer " + - name: b + value_prefix: "Bearer " + forward_payload_header: other-auth-userinfo +rules: +- match: + path: "/requires-any" + requires: + requires_any: + requirements: + - provider_name: "example_provider" + - provider_name: "other_provider" +)"; } // namespace JwtAuthn } // namespace HttpFilters