Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions api/envoy/config/filter/http/jwt_authn/v2alpha/config.proto
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,12 @@ message JwtProvider {
//
// If it is not specified, the payload will not be forwarded.
string forward_payload_header = 8;

// If true, 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*. Its *fields* contains the JWT **issuer** as the key and
// the **jwt_payload** as string value for each verified token.
bool payload_in_metadata = 9;
}

// This message specifies how to fetch JWKS from remote and how to cache it.
Expand Down
12 changes: 12 additions & 0 deletions source/common/protobuf/utility.cc
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,18 @@ ProtobufWkt::Struct MessageUtil::keyValueStruct(const std::string& key, const st
return struct_obj;
}

ProtobufWkt::Struct
MessageUtil::stringPairStruct(const std::vector<std::pair<std::string, std::string>>& pairs) {
ProtobufWkt::Struct struct_obj;
auto fields = struct_obj.mutable_fields();
for (const auto& pair : pairs) {
ProtobufWkt::Value val;
val.set_string_value(pair.second);
(*fields)[pair.first] = val;
}
return struct_obj;
}

bool ValueUtil::equal(const ProtobufWkt::Value& v1, const ProtobufWkt::Value& v2) {
ProtobufWkt::Value::KindCase kind = v1.kind_case();
if (kind != v2.kind_case()) {
Expand Down
8 changes: 8 additions & 0 deletions source/common/protobuf/utility.h
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,14 @@ class MessageUtil {
* @param value the string value to associate with the key
*/
static ProtobufWkt::Struct keyValueStruct(const std::string& key, const std::string& value);

/**
* Utility method to create a Struct containing a vector of key/value pairs.
*
* @param pairs a vector of string pairs
*/
static ProtobufWkt::Struct
stringPairStruct(const std::vector<std::pair<std::string, std::string>>& pairs);
};

class ValueUtil {
Expand Down
10 changes: 8 additions & 2 deletions source/extensions/filters/http/jwt_authn/authenticator.cc
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class AuthenticatorImpl : public Logger::Loggable<Logger::Id::filter>,
void onJwksError(Failure reason) override;
// Following functions are for Authenticator interface
void verify(Http::HeaderMap& headers, std::vector<JwtLocationConstPtr>&& tokens,
AuthenticatorCallback callback) override;
SetPayloadCallback set_payload_cb, AuthenticatorCallback callback) override;
void onDestroy() override;

TimeSource& timeSource() { return time_source_; }
Expand Down Expand Up @@ -78,6 +78,8 @@ class AuthenticatorImpl : public Logger::Loggable<Logger::Id::filter>,

// The HTTP request headers
Http::HeaderMap* headers_{};
// the callback function to set payload
SetPayloadCallback set_payload_cb_;
// The on_done function.
AuthenticatorCallback callback_;
// check audience object.
Expand All @@ -89,10 +91,11 @@ class AuthenticatorImpl : public Logger::Loggable<Logger::Id::filter>,
};

void AuthenticatorImpl::verify(Http::HeaderMap& headers, std::vector<JwtLocationConstPtr>&& tokens,
AuthenticatorCallback callback) {
SetPayloadCallback set_payload_cb, AuthenticatorCallback callback) {
ASSERT(!callback_);
headers_ = &headers;
tokens_ = std::move(tokens);
set_payload_cb_ = std::move(set_payload_cb);
callback_ = std::move(callback);

ENVOY_LOG(debug, "Jwt authentication starts");
Expand Down Expand Up @@ -224,6 +227,9 @@ void AuthenticatorImpl::verifyKey() {
// Remove JWT from headers.
curr_token_->removeJwt(*headers_);
}
if (set_payload_cb_ && provider.payload_in_metadata()) {
set_payload_cb_(provider.issuer(), jwt_->payload_str_base64url_);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we set the non-decoded version? It save a call to base64 decode in the consumer.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually can we parse the JSON payload into a ProtobufWkt::Struct? otherwise you won't be able to match each claim in RBAC.

Also rather than issuer as key, provider_name would be better.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For allow_missing_or_fail rule, it doesn't need to specify provider, so there is not provider_name. In theory, there could be two ProviderInfo with same same issuer. It is a useful feature for normal cases. But for allow_missing_or_fail case, we choose the first one matched with the issuer from the JWT token, it is better not to have two providers points to the same issuer in the config.
Overall, writing issuer works for all cases, but not provider name.

}

doneWithStatus(Status::Ok);
}
Expand Down
4 changes: 3 additions & 1 deletion source/extensions/filters/http/jwt_authn/authenticator.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ typedef std::unique_ptr<Authenticator> AuthenticatorPtr;

typedef std::function<void(const ::google::jwt_verify::Status& status)> AuthenticatorCallback;

typedef std::function<void(const std::string&, const std::string&)> SetPayloadCallback;

/**
* CreateJwksFetcherCb is a callback interface for creating a JwksFetcher instance.
*/
Expand All @@ -34,7 +36,7 @@ class Authenticator {
// Verify if headers satisfyies the JWT requirements. Can be limited to single provider with
// extract_param.
virtual void verify(Http::HeaderMap& headers, std::vector<JwtLocationConstPtr>&& tokens,
AuthenticatorCallback callback) PURE;
SetPayloadCallback set_payload_cb, AuthenticatorCallback callback) PURE;

// Called when the object is about to be destroyed.
virtual void onDestroy() PURE;
Expand Down
6 changes: 6 additions & 0 deletions source/extensions/filters/http/jwt_authn/filter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

#include "common/http/utility.h"

#include "extensions/filters/http/well_known_names.h"

using ::google::jwt_verify::Status;

namespace Envoy {
Expand Down Expand Up @@ -40,6 +42,10 @@ Http::FilterHeadersStatus Filter::decodeHeaders(Http::HeaderMap& headers, bool)
return Http::FilterHeadersStatus::StopIteration;
}

void Filter::setPayload(const ProtobufWkt::Struct& payload) {
decoder_callbacks_->streamInfo().setDynamicMetadata(HttpFilterNames::get().JwtAuthn, payload);
}

void Filter::onComplete(const Status& status) {
ENVOY_LOG(debug, "Called Filter : check complete {}", int(status));
// This stream has been reset, abort the callback.
Expand Down
4 changes: 3 additions & 1 deletion source/extensions/filters/http/jwt_authn/filter.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ class Filter : public Http::StreamDecoderFilter,
void setDecoderFilterCallbacks(Http::StreamDecoderFilterCallbacks& callbacks) override;

private:
// the function for Verifier::Callbacks interface.
// Following two functions are for Verifier::Callbacks interface.
// Pass the payload as Struct.
void setPayload(const ProtobufWkt::Struct& payload) override;
// It will be called when its verify() call is completed.
void onComplete(const ::google::jwt_verify::Status& status) override;

Expand Down
20 changes: 20 additions & 0 deletions source/extensions/filters/http/jwt_authn/verifier.cc
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,21 @@ class ContextImpl : public Verifier::Context {
// Stores an authenticator object for this request.
void storeAuth(AuthenticatorPtr&& auth) { auths_.emplace_back(std::move(auth)); }

// Add a pair of (issuer, payload), called by Authenticator
void addPaylod(const std::string& issuer, const std::string& payload) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addPayload

payload_pairs_.push_back({issuer, payload});
}

bool hasPayload() const { return !payload_pairs_.empty(); }

ProtobufWkt::Struct getPayload() const { return MessageUtil::stringPairStruct(payload_pairs_); }

private:
Http::HeaderMap& headers_;
Verifier::Callbacks* callback_;
std::unordered_map<const Verifier*, CompletionState> completion_states_;
std::vector<AuthenticatorPtr> auths_;
std::vector<std::pair<std::string, std::string>> payload_pairs_;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just use ProbotufWkt::Struct here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am looking for a utility code to convert JSON to protobuf::Struct. Do you know any?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about just use MessageUtil::loadFromJson() to convert it to Struct. Not sure what it will look like?

};

// base verifier for provider_name, provider_and_audiences, and allow_missing_or_failed.
Expand All @@ -65,6 +75,10 @@ class BaseVerifierImpl : public Verifier {
return parent_->onComplete(status, context);
}

if (Status::Ok == status && context.hasPayload()) {
context.callback()->setPayload(context.getPayload());
}

context.callback()->onComplete(status);
context.cancel();
}
Expand Down Expand Up @@ -97,6 +111,9 @@ class ProviderVerifierImpl : public BaseVerifierImpl {
auto auth = auth_factory_.create(getAudienceChecker(), provider_name_, false);
extractor_->sanitizePayloadHeaders(ctximpl.headers());
auth->verify(ctximpl.headers(), extractor_->extract(ctximpl.headers()),
[&ctximpl](const std::string& issuer, const std::string& payload) {
ctximpl.addPaylod(issuer, payload);
},
[this, context](const Status& status) {
onComplete(status, static_cast<ContextImpl&>(*context));
});
Expand Down Expand Up @@ -143,6 +160,9 @@ class AllowFailedVerifierImpl : public BaseVerifierImpl {
auto auth = auth_factory_.create(nullptr, absl::nullopt, true);
extractor_.sanitizePayloadHeaders(ctximpl.headers());
auth->verify(ctximpl.headers(), extractor_.extract(ctximpl.headers()),
[&ctximpl](const std::string& issuer, const std::string& payload) {
ctximpl.addPaylod(issuer, payload);
},
[this, context](const Status& status) {
onComplete(status, static_cast<ContextImpl&>(*context));
});
Expand Down
8 changes: 8 additions & 0 deletions source/extensions/filters/http/jwt_authn/verifier.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ class Verifier {
public:
virtual ~Callbacks() {}

/**
* Successfully verified JWT payload are stored in the struct with its
* *fields* containing **issuer** as keys and **payload** as string values
* This function is called before onComplete() function.
* It will not be called if no payload to write.
*/
virtual void setPayload(const ProtobufWkt::Struct& payload) PURE;

/**
* Called on completion of request.
*
Expand Down
35 changes: 33 additions & 2 deletions test/extensions/filters/http/jwt_authn/authenticator_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,12 @@ class AuthenticatorTest : public ::testing::Test {
std::function<void(const Status&)> on_complete_cb = [&expected_status](const Status& status) {
ASSERT_EQ(status, expected_status);
};
auto set_payload_cb = [this](const std::string& issuer, const std::string& payload) {
out_issuer_ = issuer;
out_payload_ = payload;
};
auto tokens = filter_config_->getExtractor().extract(headers);
auth_->verify(headers, std::move(tokens), std::move(on_complete_cb));
auth_->verify(headers, std::move(tokens), std::move(set_payload_cb), std::move(on_complete_cb));
}

JwtAuthentication proto_config_;
Expand All @@ -64,6 +68,8 @@ class AuthenticatorTest : public ::testing::Test {
AuthenticatorPtr auth_;
::google::jwt_verify::JwksPtr jwks_;
NiceMock<Server::Configuration::MockFactoryContext> mock_factory_ctx_;
std::string out_issuer_;
std::string out_payload_;
};

// This test validates a good JWT authentication with a remote Jwks.
Expand Down Expand Up @@ -105,6 +111,31 @@ TEST_F(AuthenticatorTest, TestForwardJwt) {

// Verify the token is NOT removed.
EXPECT_TRUE(headers.Authorization());

// Payload not set by default
EXPECT_EQ(out_issuer_, "");
EXPECT_EQ(out_payload_, "");
}

// This test verifies the Jwt payload is set.
TEST_F(AuthenticatorTest, TestSetPayload) {
// Confit payload_in_metadata flag
(*proto_config_.mutable_providers())[std::string(ProviderName)].set_payload_in_metadata(true);
CreateAuthenticator();
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(GoodToken)}};

expectVerifyStatus(Status::Ok, headers);

// Payload is set
EXPECT_EQ(out_issuer_, "https://example.com");
EXPECT_EQ(out_payload_, ExpectedPayloadValue);
}

// This test verifies the Jwt with non existing kid
Expand Down Expand Up @@ -238,7 +269,7 @@ TEST_F(AuthenticatorTest, TestOnDestroy) {
auto tokens = filter_config_->getExtractor().extract(headers);
// callback should not be called.
std::function<void(const Status&)> on_complete_cb = [](const Status&) { FAIL(); };
auth_->verify(headers, std::move(tokens), std::move(on_complete_cb));
auth_->verify(headers, std::move(tokens), nullptr, std::move(on_complete_cb));

// Destroy the authenticating process.
auth_->onDestroy();
Expand Down
27 changes: 27 additions & 0 deletions test/extensions/filters/http/jwt_authn/filter_test.cc
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#include "extensions/filters/http/jwt_authn/filter.h"
#include "extensions/filters/http/well_known_names.h"

#include "test/extensions/filters/http/jwt_authn/mock.h"
#include "test/mocks/server/mocks.h"
#include "test/test_common/utility.h"

#include "gmock/gmock.h"
#include "gtest/gtest.h"
Expand Down Expand Up @@ -85,6 +87,31 @@ TEST_F(FilterTest, InlineOK) {
EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(headers));
}

// This test verifies the setPayload call is handled correctly
TEST_F(FilterTest, TestSetPayloadCall) {
setupMockConfig();
ProtobufWkt::Struct payload;
// A successful authentication completed inline: callback is called inside verify().
EXPECT_CALL(*raw_mock_verifier_, verify(_)).WillOnce(Invoke([&payload](ContextSharedPtr context) {
context->callback()->setPayload(payload);
context->callback()->onComplete(Status::Ok);
}));

EXPECT_CALL(filter_callbacks_.stream_info_, setDynamicMetadata(_, _))
.WillOnce(Invoke([&payload](const std::string& ns, const ProtobufWkt::Struct& out_payload) {
EXPECT_EQ(ns, HttpFilterNames::get().JwtAuthn);
EXPECT_TRUE(TestUtility::protoEqual(out_payload, payload));
}));

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));
}

// 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) {
Expand Down
Loading