diff --git a/api/envoy/extensions/filters/http/jwt_authn/v3/config.proto b/api/envoy/extensions/filters/http/jwt_authn/v3/config.proto index 1aabe1bd4390..a9456d557342 100644 --- a/api/envoy/extensions/filters/http/jwt_authn/v3/config.proto +++ b/api/envoy/extensions/filters/http/jwt_authn/v3/config.proto @@ -51,7 +51,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // cache_duration: // seconds: 300 // -// [#next-free-field: 10] +// [#next-free-field: 12] message JwtProvider { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.http.jwt_authn.v2alpha.JwtProvider"; @@ -190,6 +190,18 @@ message JwtProvider { // exp: 1501281058 // string payload_in_metadata = 9; + + // The two below fields define the amount of slack in seconds that will be used + // when determining if a JWT is valid or has expired. Validity is determined by + // the formula: VALID_FROM("iat") + nbf_slack <= NOW <= VALID_TO("exp") + exp_slack + // which aims to provide a remedy in the following case: + // Some load balancer performinc OIDC authentication receives a request and determines that the + // existing access token is valid (for another 1 second). It forwards the request to Istio Ingressgateway + // and subsequently to some pod with an envoy sidecar. Meanwhile, 1 second has passed and when envoy checks + // the token it finds that it has expired. + uint32 nbf_slack = 10; + + uint32 exp_slack = 11; } // This message specifies how to fetch JWKS from remote and how to cache it. diff --git a/bazel/repository_locations.bzl b/bazel/repository_locations.bzl index d89b139ee5f8..9e48edfb94b0 100644 --- a/bazel/repository_locations.bzl +++ b/bazel/repository_locations.bzl @@ -183,10 +183,10 @@ REPOSITORY_LOCATIONS = dict( urls = ["https://github.com/msgpack/msgpack-c/releases/download/cpp-3.2.1/msgpack-3.2.1.tar.gz"], ), com_github_google_jwt_verify = dict( - sha256 = "d422a6eadd4bcdd0f9b122cd843a4015f8b18aebea6e1deb004bd4d401a8ef92", - strip_prefix = "jwt_verify_lib-40e2cc938f4bcd059a97dc6c73f59ecfa5a71bac", - # 2020-02-11 - urls = ["https://github.com/google/jwt_verify_lib/archive/40e2cc938f4bcd059a97dc6c73f59ecfa5a71bac.tar.gz"], + sha256 = "21b9fd9fb8714cb199a823a4c01cf6665bdd42b62137348707dee51714797dfc", + strip_prefix = "jwt_verify_lib-b5b3b4ed8611b1eea8764845381e60becc7b0b43", + # 2020-04-17 + urls = ["https://github.com/google/jwt_verify_lib/archive/b5b3b4ed8611b1eea8764845381e60becc7b0b43.tar.gz"], ), com_github_nodejs_http_parser = dict( sha256 = "8fa0ab8770fd8425a9b431fdbf91623c4d7a9cdb842b9339289bd2b0b01b0d3d", diff --git a/generated_api_shadow/envoy/extensions/filters/http/jwt_authn/v3/config.proto b/generated_api_shadow/envoy/extensions/filters/http/jwt_authn/v3/config.proto index 802a582a572a..d016d5f5d0be 100644 --- a/generated_api_shadow/envoy/extensions/filters/http/jwt_authn/v3/config.proto +++ b/generated_api_shadow/envoy/extensions/filters/http/jwt_authn/v3/config.proto @@ -51,7 +51,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // cache_duration: // seconds: 300 // -// [#next-free-field: 10] +// [#next-free-field: 12] message JwtProvider { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.http.jwt_authn.v2alpha.JwtProvider"; @@ -159,35 +159,47 @@ message JwtProvider { // string payload_in_metadata = 9; + // This field specifies the header name to forward a successfully verified JWT payload to the + // backend. The forwarded data is:: + // + // base64url_encoded(jwt_payload_in_JSON) + // + // If it is not specified, the payload will not be forwarded. + uint32 nbf_slack = 10; + + // If non empty, successfully verified JWT payloads will be written to StreamInfo DynamicMetadata + // in the format as: *namespace* is the jwt_authn filter name as **envoy.filters.http.jwt_authn** + // The value is the *protobuf::Struct*. The value of this field will be the key for its *fields* + // and the value is the *protobuf::Struct* converted from JWT JSON payload. + // + // For example, if payload_in_metadata is *my_payload*: + // + // .. code-block:: yaml + // + // envoy.filters.http.jwt_authn: + // my_payload: + // iss: https://example.com + // sub: test@example.com + // aud: https://example.com + // exp: 1501281058 + // + uint32 exp_slack = 11; + // `JSON Web Key Set (JWKS) `_ is needed to // validate signature of a JWT. This field specifies where to fetch JWKS. oneof jwks_source_specifier { option (validate.required) = true; - // This field specifies the header name to forward a successfully verified JWT payload to the - // backend. The forwarded data is:: - // - // base64url_encoded(jwt_payload_in_JSON) - // - // If it is not specified, the payload will not be forwarded. + // The two below fields define the amount of slack in seconds that will be used + // when determining if a JWT is valid or has expired. Validity is determined by + // the formula: VALID_FROM("iat") + nbf_slack <= NOW <= VALID_TO("exp") + exp_slack + // which aims to provide a remedy in the following case: + // Some load balancer performinc OIDC authentication receives a request and determines that the + // existing access token is valid (for another 1 second). It forwards the request to Istio Ingressgateway + // and subsequently to some pod with an envoy sidecar. Meanwhile, 1 second has passed and when envoy checks + // the token it finds that it has expired. RemoteJwks remote_jwks = 3; - // If non empty, successfully verified JWT payloads will be written to StreamInfo DynamicMetadata - // in the format as: *namespace* is the jwt_authn filter name as **envoy.filters.http.jwt_authn** - // The value is the *protobuf::Struct*. The value of this field will be the key for its *fields* - // and the value is the *protobuf::Struct* converted from JWT JSON payload. - // - // For example, if payload_in_metadata is *my_payload*: - // - // .. code-block:: yaml - // - // envoy.filters.http.jwt_authn: - // my_payload: - // iss: https://example.com - // sub: test@example.com - // aud: https://example.com - // exp: 1501281058 - // config.core.v3.DataSource local_jwks = 4; } } diff --git a/source/extensions/filters/http/jwt_authn/authenticator.cc b/source/extensions/filters/http/jwt_authn/authenticator.cc index 3837b2c4c034..14a3dbf4acef 100644 --- a/source/extensions/filters/http/jwt_authn/authenticator.cc +++ b/source/extensions/filters/http/jwt_authn/authenticator.cc @@ -153,33 +153,41 @@ void AuthenticatorImpl::startVerify() { return; } - // TODO(qiwzhang): Cross-platform-wise the below unix_timestamp code is wrong as the - // epoch is not guaranteed to be defined as the unix epoch. We should use - // the abseil time functionality instead or use the jwt_verify_lib to check - // the validity of a JWT. + // Check the issuer is configured or not. + 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 "exp" claim. - const uint64_t unix_timestamp = - std::chrono::duration_cast(timeSource().systemTime().time_since_epoch()) - .count(); + // The two below fields define the amount of slack in seconds that will be used + // when determining if a JWT is valid or has expired. Validity is determined by + // the formula: VALID_FROM("iat") + nbf_slack <= NOW <= VALID_TO("exp") + exp_slack + // which aims to provide a remedy in the following case: + // Some load balancer performinc OIDC authentication receives a request and determines that the + // existing access token is valid (for another 1 second). It forwards the request to Istio + // Ingressgateway and subsequently to some pod with an envoy sidecar. Meanwhile, 1 second has + // passed and when envoy checks the token it finds that it has expired. + const uint64_t now = std::chrono::time_point_cast(timeSource().systemTime()) + .time_since_epoch() + .count(); + + const uint32_t nbf_slack = jwks_data_->getJwtProvider().nbf_slack(); + const uint32_t exp_slack = jwks_data_->getJwtProvider().exp_slack(); + // If the nbf claim does *not* appear in the JWT, then the nbf field is defaulted // to 0. - if (jwt_->nbf_ > unix_timestamp) { + if (now < jwt_->nbf_ + nbf_slack) { 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 (0 < jwt_->exp_ && now > jwt_->exp_ + exp_slack) { doneWithStatus(Status::JwtExpired); return; } - // Check the issuer is configured or not. - 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 bool is_allowed = check_audience_ ? check_audience_->areAudiencesAllowed(jwt_->audiences_) : jwks_data_->areAudiencesAllowed(jwt_->audiences_); @@ -237,7 +245,8 @@ 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::verifyJwtWithoutTimeChecking(*jwt_, *jwks_data_->getJwksObj()); if (status != Status::Ok) { doneWithStatus(status); return; diff --git a/source/extensions/filters/http/jwt_authn/authenticator.h b/source/extensions/filters/http/jwt_authn/authenticator.h index 928a8045b843..a5561b22d887 100644 --- a/source/extensions/filters/http/jwt_authn/authenticator.h +++ b/source/extensions/filters/http/jwt_authn/authenticator.h @@ -6,6 +6,7 @@ #include "extensions/filters/http/jwt_authn/extractor.h" #include "extensions/filters/http/jwt_authn/jwks_cache.h" +#include "absl/time/clock.h" #include "jwt_verify_lib/check_audience.h" #include "jwt_verify_lib/status.h"