From 0d827df8990e1616a04d192a783e573395e64b41 Mon Sep 17 00:00:00 2001 From: Kuat Yessenov Date: Fri, 27 Oct 2023 23:59:38 +0000 Subject: [PATCH] implement jwt normalize claims Signed-off-by: Kuat Yessenov --- .../filters/http/jwt_authn/v3/config.proto | 1 - .../filters/http/jwt_authn/authenticator.cc | 28 ++++++++++++++-- .../http/jwt_authn/authenticator_test.cc | 32 ++++++++++++++++++ .../filters/http/jwt_authn/test_common.h | 33 +++++++++++++++++++ 4 files changed, 91 insertions(+), 3 deletions(-) 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 c2ad8f0f26f0..1bb1b111303a 100644 --- a/api/envoy/extensions/filters/http/jwt_authn/v3/config.proto +++ b/api/envoy/extensions/filters/http/jwt_authn/v3/config.proto @@ -242,7 +242,6 @@ message JwtProvider { // string payload_in_metadata = 9; - // [#not-implemented-hide:] // Normalizes the payload representation in the request metadata. NormalizePayload normalize_payload_in_metadata = 18; diff --git a/source/extensions/filters/http/jwt_authn/authenticator.cc b/source/extensions/filters/http/jwt_authn/authenticator.cc index cb380b905eb0..9587ad3c1435 100644 --- a/source/extensions/filters/http/jwt_authn/authenticator.cc +++ b/source/extensions/filters/http/jwt_authn/authenticator.cc @@ -12,6 +12,7 @@ #include "source/common/protobuf/protobuf.h" #include "source/common/tracing/http_tracer_impl.h" +#include "absl/strings/str_split.h" #include "jwt_verify_lib/jwt.h" #include "jwt_verify_lib/struct_utils.h" #include "jwt_verify_lib/verify.h" @@ -76,6 +77,9 @@ class AuthenticatorImpl : public Logger::Loggable, // Handle Good Jwt either Cache JWT or verified public key. void handleGoodJwt(bool cache_hit); + // Normalize and set the payload metadata. + void setPayloadMetadata(const ProtobufWkt::Struct& jwt_payload); + // Calls the callback with status. void doneWithStatus(const Status& status); @@ -373,9 +377,8 @@ void AuthenticatorImpl::handleGoodJwt(bool cache_hit) { if (!provider.header_in_metadata().empty()) { set_extracted_jwt_data_cb_(provider.header_in_metadata(), jwt_->header_pb_); } - if (!provider.payload_in_metadata().empty()) { - set_extracted_jwt_data_cb_(provider.payload_in_metadata(), jwt_->payload_pb_); + setPayloadMetadata(jwt_->payload_pb_); } } if (provider_ && !cache_hit) { @@ -385,6 +388,27 @@ void AuthenticatorImpl::handleGoodJwt(bool cache_hit) { doneWithStatus(Status::Ok); } +void AuthenticatorImpl::setPayloadMetadata(const ProtobufWkt::Struct& jwt_payload) { + const auto& provider = jwks_data_->getJwtProvider(); + const auto& normalize = provider.normalize_payload_in_metadata(); + if (normalize.space_delimited_claims().size() == 0) { + set_extracted_jwt_data_cb_(provider.payload_in_metadata(), jwt_payload); + } + // Make a temporary copy to normalize the JWT struct. + ProtobufWkt::Struct out_payload = jwt_payload; + for (const auto& claim : normalize.space_delimited_claims()) { + const auto& it = jwt_payload.fields().find(claim); + if (it != jwt_payload.fields().end() && it->second.has_string_value()) { + const auto list = absl::StrSplit(it->second.string_value(), ' ', absl::SkipEmpty()); + for (const auto& elt : list) { + (*out_payload.mutable_fields())[claim].mutable_list_value()->add_values()->set_string_value( + elt); + } + } + } + set_extracted_jwt_data_cb_(provider.payload_in_metadata(), out_payload); +} + void AuthenticatorImpl::doneWithStatus(const Status& status) { ENVOY_LOG(debug, "{}: JWT token verification completed with: {}", name(), ::google::jwt_verify::getStatusString(status)); diff --git a/test/extensions/filters/http/jwt_authn/authenticator_test.cc b/test/extensions/filters/http/jwt_authn/authenticator_test.cc index 70b123c3807f..d07c1b60d09d 100644 --- a/test/extensions/filters/http/jwt_authn/authenticator_test.cc +++ b/test/extensions/filters/http/jwt_authn/authenticator_test.cc @@ -232,6 +232,38 @@ TEST_F(AuthenticatorTest, TestSetPayload) { TestUtility::protoEqual(expected_payload, out_extracted_data_.fields().at("my_payload"))); } +// This test verifies the JWT payload is set. +TEST_F(AuthenticatorTest, TestSetPayloadWithSpaces) { + // Config payload_in_metadata flag + (*proto_config_.mutable_providers())[std::string(ProviderName)].set_payload_in_metadata( + "my_payload"); + auto* normalize_payload = (*proto_config_.mutable_providers())[std::string(ProviderName)] + .mutable_normalize_payload_in_metadata(); + normalize_payload->add_space_delimited_claims("scope"); + normalize_payload->add_space_delimited_claims("test_string"); + normalize_payload->add_space_delimited_claims("test_num"); + + createAuthenticator(); + EXPECT_CALL(*raw_fetcher_, fetch(_, _)) + .WillOnce(Invoke([this](Tracing::Span&, JwksFetcher::JwksReceiver& receiver) { + receiver.onJwksSuccess(std::move(jwks_)); + })); + + // Test OK pubkey and its cache + Http::TestRequestHeaderMapImpl headers{ + {"Authorization", "Bearer " + std::string(GoodTokenWithSpaces)}}; + + expectVerifyStatus(Status::Ok, headers); + + // Only one field is set. + EXPECT_EQ(1, out_extracted_data_.fields().size()); + + ProtobufWkt::Value expected_payload; + TestUtility::loadFromJson(ExpectedPayloadJSONWithSpaces, expected_payload); + EXPECT_TRUE( + TestUtility::protoEqual(expected_payload, out_extracted_data_.fields().at("my_payload"))); +} + // This test verifies setting only the extracted header to metadata. TEST_F(AuthenticatorTest, TestSetHeader) { // Set the extracted header to metadata. diff --git a/test/extensions/filters/http/jwt_authn/test_common.h b/test/extensions/filters/http/jwt_authn/test_common.h index eb696a7a2509..71364cbfff3d 100644 --- a/test/extensions/filters/http/jwt_authn/test_common.h +++ b/test/extensions/filters/http/jwt_authn/test_common.h @@ -151,6 +151,26 @@ const char GoodToken[] = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwc "EprqSZUzi_ZzzYzqBNVhIJujcNWij7JRra2sXXiSAfKjtxHQoxrX8n4V1ySWJ3_1T" "H_cJcdfS_RKP7YgXRWC0L16PNF5K7iqRqmjKALNe83ZFnFIw"; +// Payload: +// { +// "iss": "https://example.com", +// "sub": "test@example.com", +// "exp": 2001001001, +// "aud": "example_service", +// "scope": "read write", +// "test_string": "test_value", +// "test_num": 1337 +// } +const char GoodTokenWithSpaces[] = + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9." + "eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwic3ViIjoidGVzdEBleGFtcGxlLmNvbSIsImV4cCI6MjAwMTAwMTAwMS" + "wiYXVkIjoiZXhhbXBsZV9zZXJ2aWNlIiwic2NvcGUiOiJyZWFkIHdyaXRlIiwidGVzdF9zdHJpbmciOiJ0ZXN0X3ZhbHVl" + "IiwidGVzdF9udW0iOjEzMzd9.cKTwWSJgS0TZ3Ajc9QrAA50Me7j1zVv9YzDT_" + "2UE5jlCs5vWkdWjUb2r7MYaqximXj3affDZdDsUxMaqqR7lWT2EbxOoEceBkCMmakgSs8tjZ210w0YTU0OyhrrxsyxUpsp" + "PeRzPIHQTUdN7zU_KkMcUU1yDSlnJxqlYXyTL9E-DhTnLwoOdgFGiQs-md_QJfdOFgXQqU71EZ-" + "Ofxen8EFl10wbzHubMHGLJqVfFzK-iuVr2P0OZ0ymWvPGwQdlVMojHx3P0Yb8MRbhdW04hCJq-_" + "fTE1RNb6ja1JBFQbyGcQTtWVSdkHZ_C8syd8s-aK4C8_VhwNEDviOVrHPbztw"; + // Payload: // {"iss":"https://example.com","sub":"test@example.com","exp":null} const char NonExpiringToken[] = @@ -271,6 +291,19 @@ const char ExpectedPayloadJSON[] = R"( } )"; +// Base64 decoded Payload with space-delimited claims JSON +const char ExpectedPayloadJSONWithSpaces[] = R"( +{ + "iss":"https://example.com", + "sub":"test@example.com", + "exp":2001001001, + "aud":"example_service", + "scope":["read","write"], + "test_string":["test_value"], + "test_num":1337 +} +)"; + const char ExpectedHeaderJSON[] = R"( { "alg": "RS256",