-
Notifications
You must be signed in to change notification settings - Fork 5.5k
Creating JwksFetcher interface and impl v2 #4242
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
7914794
d3fbad5
c90ebf7
1b2e6c7
6dbc828
6ca0471
498c858
af9175e
51c4d1b
f477905
8fa1dc8
2d18a01
1d9281e
9d277be
4c77200
4bb61d2
8c1c294
489c52d
17ddf46
8038924
1457fb1
f95e51e
18b6b9d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| #include "extensions/filters/http/common/jwks_fetcher.h" | ||
|
|
||
| #include "common/common/enum_to_int.h" | ||
| #include "common/http/headers.h" | ||
| #include "common/http/utility.h" | ||
|
|
||
| #include "jwt_verify_lib/status.h" | ||
|
|
||
| namespace Envoy { | ||
| namespace Extensions { | ||
| namespace HttpFilters { | ||
| namespace Common { | ||
| namespace { | ||
| class JwksFetcherImpl : public JwksFetcher, | ||
|
nickrmc83 marked this conversation as resolved.
|
||
| public Logger::Loggable<Logger::Id::filter>, | ||
| public Http::AsyncClient::Callbacks { | ||
| private: | ||
|
nickrmc83 marked this conversation as resolved.
Outdated
|
||
| Upstream::ClusterManager& cm_; | ||
| JwksFetcher::JwksReceiver* receiver_ = nullptr; | ||
|
nickrmc83 marked this conversation as resolved.
Outdated
|
||
| const ::envoy::api::v2::core::HttpUri* uri_ = nullptr; | ||
|
nickrmc83 marked this conversation as resolved.
Outdated
|
||
| Http::AsyncClient::Request* request_ = nullptr; | ||
|
|
||
| public: | ||
| JwksFetcherImpl(Upstream::ClusterManager& cm) : cm_(cm) { ENVOY_LOG(trace, "{}", __func__); } | ||
|
|
||
| void close() { | ||
| if (request_) { | ||
| request_->cancel(); | ||
| request_ = nullptr; | ||
| ENVOY_LOG(debug, "fetch pubkey [uri = {}]: canceled", uri_->uri()); | ||
| } | ||
| } | ||
|
|
||
| void fetch(const ::envoy::api::v2::core::HttpUri& uri, JwksFetcher::JwksReceiver* receiver) { | ||
| ENVOY_LOG(trace, "{}", __func__); | ||
| receiver_ = receiver; | ||
| uri_ = &uri; | ||
| Http::MessagePtr message = Http::Utility::prepareHeaders(uri); | ||
| message->headers().insertMethod().value().setReference(Http::Headers::get().MethodValues.Get); | ||
| ENVOY_LOG(debug, "fetch pubkey from [uri = {}]: start", uri_->uri()); | ||
| request_ = | ||
| cm_.httpAsyncClientForCluster(uri.cluster()) | ||
| .send(std::move(message), *this, | ||
| std::chrono::milliseconds(DurationUtil::durationToMilliseconds(uri.timeout()))); | ||
| } | ||
|
|
||
| // HTTP async receive methods | ||
| void onSuccess(Http::MessagePtr&& response) { | ||
| ENVOY_LOG(trace, "{}", __func__); | ||
| request_ = nullptr; | ||
| const uint64_t status_code = Http::Utility::getResponseStatus(response->headers()); | ||
| if (status_code == enumToInt(Http::Code::OK)) { | ||
| ENVOY_LOG(debug, "{}: fetch pubkey [uri = {}]: success", __func__, uri_->uri()); | ||
| if (response->body()) { | ||
| const auto len = response->body()->length(); | ||
| const auto body = std::string(static_cast<char*>(response->body()->linearize(len)), len); | ||
| auto jwks = | ||
| google::jwt_verify::Jwks::createFrom(body, google::jwt_verify::Jwks::Type::JWKS); | ||
| if (jwks->getStatus() == google::jwt_verify::Status::Ok) { | ||
| ENVOY_LOG(debug, "{}: fetch pubkey [uri = {}]: succeeded", __func__, uri_->uri()); | ||
| receiver_->onJwksSuccess(std::move(jwks)); | ||
| } else { | ||
| ENVOY_LOG(debug, "{}: fetch pubkey [uri = {}]: invalid jwks", __func__, uri_->uri()); | ||
| receiver_->onJwksError(JwksFetcher::JwksReceiver::Failure::invalid_jwks); | ||
| } | ||
| } else { | ||
| ENVOY_LOG(debug, "{}: fetch pubkey [uri = {}]: body is empty", __func__, uri_->uri()); | ||
| receiver_->onJwksError(JwksFetcher::JwksReceiver::Failure::network); | ||
| } | ||
| } else { | ||
| ENVOY_LOG(debug, "{}: fetch pubkey [uri = {}]: response status code {}", __func__, | ||
| uri_->uri(), status_code); | ||
| receiver_->onJwksError(JwksFetcher::JwksReceiver::Failure::network); | ||
| } | ||
| } | ||
|
|
||
| void onFailure(Http::AsyncClient::FailureReason reason) { | ||
|
nickrmc83 marked this conversation as resolved.
|
||
| ENVOY_LOG(debug, "{}: fetch pubkey [uri = {}]: network error {}", __func__, uri_->uri(), | ||
| enumToInt(reason)); | ||
| receiver_->onJwksError(JwksFetcher::JwksReceiver::Failure::network); | ||
| } | ||
| }; | ||
| } // namespace | ||
|
|
||
| JwksFetcher::JwksFetcherPtr JwksFetcher::create(Upstream::ClusterManager& cm) { | ||
| return JwksFetcherPtr(new JwksFetcherImpl(cm)); | ||
|
nickrmc83 marked this conversation as resolved.
Outdated
|
||
| } | ||
| } // namespace Common | ||
| } // namespace HttpFilters | ||
| } // namespace Extensions | ||
| } // namespace Envoy | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| #pragma once | ||
|
|
||
| #include "envoy/api/v2/core/http_uri.pb.h" | ||
| #include "envoy/common/pure.h" | ||
| #include "envoy/upstream/cluster_manager.h" | ||
|
|
||
| #include "jwt_verify_lib/jwks.h" | ||
|
|
||
| namespace Envoy { | ||
| namespace Extensions { | ||
| namespace HttpFilters { | ||
| namespace Common { | ||
|
|
||
| class JwksFetcher { | ||
|
nickrmc83 marked this conversation as resolved.
|
||
| public: | ||
| typedef std::unique_ptr<JwksFetcher> JwksFetcherPtr; | ||
|
nickrmc83 marked this conversation as resolved.
Outdated
|
||
|
|
||
| class JwksReceiver { | ||
| public: | ||
| enum class Failure { | ||
| unknown, | ||
|
nickrmc83 marked this conversation as resolved.
Outdated
|
||
| network, | ||
| invalid_jwks, | ||
| }; | ||
|
|
||
| virtual ~JwksReceiver(){}; | ||
| /* | ||
| * Successful retrieval callback. | ||
| * of the returned JWKS object. | ||
| * @param jwks the JWKS object retrieved. | ||
| */ | ||
| virtual void onJwksSuccess(google::jwt_verify::JwksPtr&& jwks) PURE; | ||
| /* | ||
| * Retrieval error callback. | ||
| * * @param reason the failure reason. | ||
| */ | ||
| virtual void onJwksError(Failure reason) PURE; | ||
| }; | ||
|
|
||
| virtual ~JwksFetcher(){}; | ||
|
|
||
| /* | ||
| * Close/stop any inflight request. | ||
| */ | ||
| virtual void close() PURE; | ||
|
nickrmc83 marked this conversation as resolved.
Outdated
|
||
|
|
||
| /* | ||
| * Retrieve a JWKS resource from a remote HTTP host. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in 2d18a01 |
||
| * @param uri the uri to retrieve the jwks from. | ||
|
nickrmc83 marked this conversation as resolved.
|
||
| */ | ||
| virtual void fetch(const ::envoy::api::v2::core::HttpUri& uri, JwksReceiver* receiver) PURE; | ||
|
nickrmc83 marked this conversation as resolved.
Outdated
|
||
|
|
||
| /* | ||
| * Factory method for creating a JwksFetcher. | ||
| * @param cm the cluster manager to use during Jwks retrieval | ||
| * @return a JwksFetcher instance | ||
| */ | ||
| static JwksFetcherPtr create(Upstream::ClusterManager& cm); | ||
| }; | ||
| } // namespace Common | ||
| } // namespace HttpFilters | ||
| } // namespace Extensions | ||
| } // namespace Envoy | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,29 +24,23 @@ namespace { | |
| */ | ||
| class AuthenticatorImpl : public Logger::Loggable<Logger::Id::filter>, | ||
| public Authenticator, | ||
| public Http::AsyncClient::Callbacks { | ||
| public Common::JwksFetcher::JwksReceiver { | ||
| public: | ||
| AuthenticatorImpl(FilterConfigSharedPtr config) : config_(config) {} | ||
| AuthenticatorImpl(FilterConfigSharedPtr config, Common::JwksFetcher::JwksFetcherPtr& fetcher) | ||
| : config_(config), fetcher_(std::move(fetcher)) {} | ||
|
|
||
| // 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 onDestroy() override; | ||
| void sanitizePayloadHeaders(Http::HeaderMap& headers) const override; | ||
|
|
||
| private: | ||
| // Fetch a remote public key. | ||
| void fetchRemoteJwks(); | ||
|
|
||
| // Following two functions are for AyncClient::Callbacks | ||
| void onSuccess(Http::MessagePtr&& response) override; | ||
| void onFailure(Http::AsyncClient::FailureReason) override; | ||
|
|
||
| // Verify with a specific public key. | ||
| void verifyKey(); | ||
|
|
||
| // Handle the public key fetch done event. | ||
| void onFetchRemoteJwksDone(const std::string& jwks_str); | ||
|
|
||
| // Calls the callback with status. | ||
| void doneWithStatus(const Status& status); | ||
|
|
||
|
|
@@ -56,6 +50,9 @@ class AuthenticatorImpl : public Logger::Loggable<Logger::Id::filter>, | |
| // The config object. | ||
| FilterConfigSharedPtr config_; | ||
|
|
||
| // The Jwks fetcher object | ||
| Common::JwksFetcher::JwksFetcherPtr fetcher_; | ||
|
|
||
| // The token data | ||
| JwtLocationConstPtr token_; | ||
| // The JWT object. | ||
|
|
@@ -115,13 +112,19 @@ void AuthenticatorImpl::verify(Http::HeaderMap& headers, Authenticator::Callback | |
| return; | ||
| } | ||
|
|
||
| // TODO: Cross-platform-wise the below unix_timestamp code is wrong as the | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @qiwzhang note comment I've added here. |
||
| // epoch is not guaranteed to be defined as the unix epoch. We should use | ||
| // the abseil time functionality instead. | ||
| // TODO: We should use the jwt_verify_lib to check the validity of a JWT. | ||
| // Check "exp" claim. | ||
| const auto unix_timestamp = std::chrono::duration_cast<std::chrono::seconds>( | ||
| std::chrono::system_clock::now().time_since_epoch()) | ||
| .count(); | ||
| // NOTE: Service account tokens generally don't have an expiration time (due to being long lived) | ||
| // and defaulted to 0 by google::jwt_verify library but are still valid. | ||
| if (jwt_.exp_ > 0 && jwt_.exp_ < unix_timestamp) { | ||
| if (jwt_.nbf_ > unix_timestamp) { | ||
|
nickrmc83 marked this conversation as resolved.
|
||
| doneWithStatus(Status::JwtNotYetValid); | ||
| return; | ||
| } | ||
| if (jwt_.exp && jwt_.exp_ < unix_timestamp) { | ||
| doneWithStatus(Status::JwtExpired); | ||
| return; | ||
| } | ||
|
|
@@ -137,7 +140,8 @@ void AuthenticatorImpl::verify(Http::HeaderMap& headers, Authenticator::Callback | |
| return; | ||
| } | ||
|
|
||
| if (jwks_data_->getJwksObj() != nullptr && !jwks_data_->isExpired()) { | ||
| auto jwks_obj = jwks_data_->getJwksObj(); | ||
| if (jwks_obj != nullptr && !jwks_data_->isExpired()) { | ||
| verifyKey(); | ||
| return; | ||
| } | ||
|
|
@@ -146,72 +150,28 @@ void AuthenticatorImpl::verify(Http::HeaderMap& headers, Authenticator::Callback | |
| // 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. | ||
| fetchRemoteJwks(); | ||
| } | ||
|
|
||
| void AuthenticatorImpl::fetchRemoteJwks() { | ||
| const auto& http_uri = jwks_data_->getJwtProvider().remote_jwks().http_uri(); | ||
|
|
||
| Http::MessagePtr message = Http::Utility::prepareHeaders(http_uri); | ||
| message->headers().insertMethod().value().setReference(Http::Headers::get().MethodValues.Get); | ||
|
|
||
| if (config_->cm().get(http_uri.cluster()) == nullptr) { | ||
| doneWithStatus(Status::JwksFetchFail); | ||
| return; | ||
| } | ||
|
|
||
| uri_ = http_uri.uri(); | ||
| ENVOY_LOG(debug, "fetch pubkey from [uri = {}]: start", uri_); | ||
| request_ = config_->cm() | ||
| .httpAsyncClientForCluster(http_uri.cluster()) | ||
| .send(std::move(message), *this, | ||
| std::chrono::milliseconds( | ||
| DurationUtil::durationToMilliseconds(http_uri.timeout()))); | ||
| } | ||
|
|
||
| void AuthenticatorImpl::onSuccess(Http::MessagePtr&& response) { | ||
| request_ = nullptr; | ||
| const uint64_t status_code = Http::Utility::getResponseStatus(response->headers()); | ||
| if (status_code == enumToInt(Http::Code::OK)) { | ||
| ENVOY_LOG(debug, "fetch pubkey [uri = {}]: success", uri_); | ||
| if (response->body()) { | ||
| const auto len = response->body()->length(); | ||
| const auto body = std::string(static_cast<char*>(response->body()->linearize(len)), len); | ||
| onFetchRemoteJwksDone(body); | ||
| return; | ||
| } else { | ||
| ENVOY_LOG(debug, "fetch pubkey [uri = {}]: body is empty", uri_); | ||
| } | ||
| if (jwks_data_->getJwtProvider().has_remote_jwks()) { | ||
| fetcher_->fetch(jwks_data_->getJwtProvider().remote_jwks().http_uri(), this); | ||
| } else { | ||
| ENVOY_LOG(debug, "fetch pubkey [uri = {}]: response status code {}", uri_, status_code); | ||
| } | ||
| doneWithStatus(Status::JwksFetchFail); | ||
| } | ||
|
|
||
| void AuthenticatorImpl::onFailure(Http::AsyncClient::FailureReason) { | ||
| request_ = nullptr; | ||
| ENVOY_LOG(debug, "fetch pubkey [uri = {}]: failed", uri_); | ||
| doneWithStatus(Status::JwksFetchFail); | ||
| } | ||
|
|
||
| void AuthenticatorImpl::onDestroy() { | ||
| if (request_ != nullptr) { | ||
| request_->cancel(); | ||
| request_ = nullptr; | ||
| ENVOY_LOG(debug, "fetch pubkey [uri = {}]: canceled", uri_); | ||
| // No valid keys for this issuer. This may happen as a result of incorrect local | ||
| // JWKS configuration. | ||
| doneWithStatus(Status::JwksNoValidKeys); | ||
| } | ||
| } | ||
|
|
||
| // Handle the public key fetch done event. | ||
| void AuthenticatorImpl::onFetchRemoteJwksDone(const std::string& jwks_str) { | ||
| const Status status = jwks_data_->setRemoteJwks(jwks_str); | ||
| void AuthenticatorImpl::onJwksSuccess(google::jwt_verify::JwksPtr&& jwks) { | ||
| const Status status = jwks_data_->setRemoteJwks(std::move(jwks))->getStatus(); | ||
| if (status != Status::Ok) { | ||
| doneWithStatus(status); | ||
| } else { | ||
| verifyKey(); | ||
| } | ||
| } | ||
|
|
||
| void AuthenticatorImpl::onJwksError(Failure) { doneWithStatus(Status::JwksFetchFail); } | ||
|
|
||
| void AuthenticatorImpl::onDestroy() { fetcher_->close(); } | ||
|
|
||
| // Verify with a specific public key. | ||
| void AuthenticatorImpl::verifyKey() { | ||
| const Status status = ::google::jwt_verify::verifyJwt(jwt_, *jwks_data_->getJwksObj()); | ||
|
|
@@ -249,8 +209,9 @@ void AuthenticatorImpl::doneWithStatus(const Status& status) { | |
|
|
||
| } // namespace | ||
|
|
||
| AuthenticatorPtr Authenticator::create(FilterConfigSharedPtr config) { | ||
| return std::make_unique<AuthenticatorImpl>(config); | ||
| AuthenticatorPtr Authenticator::create(FilterConfigSharedPtr config, | ||
|
nickrmc83 marked this conversation as resolved.
|
||
| Common::JwksFetcher::JwksFetcherPtr& fetcher) { | ||
| return std::make_unique<AuthenticatorImpl>(config, fetcher); | ||
|
nickrmc83 marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| } // namespace JwtAuthn | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.