Skip to content
Merged
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: 4 additions & 2 deletions WORKSPACE
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ bind(
)

# When updating envoy sha manually please update the sha in istio.deps file also
ENVOY_SHA = "48b161ee02f4ca77b8c26959c4058456ad3f197f"
ENVOY_SHA256 = "ae044661f49db58cfb79fa65f1213a2e07758e3fac1e3e4088fd9edafa8e590d"
#
# Determine SHA256 `wget https://github.com/envoyproxy/envoy/archive/COMMIT.zip && sha256sum COMMIT.zip`
ENVOY_SHA = "2a2ad48a7d4b57512bc10a9593e852fe950b1c8d"
ENVOY_SHA256 = "a86dd396bd3db8401d45f9d387d3177ba1eb8298520ef684c1deaf7b91a1af1d"

http_archive(
name = "envoy",
Expand Down
4 changes: 2 additions & 2 deletions istio.deps
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
"name": "ISTIO_API",
"repoName": "api",
"file": "repositories.bzl",
"lastStableSHA": "6b9e3a501e6ef254958bf82f7b74c37d64a57a15"
"lastStableSHA": "1a7788d738d2c6b07ba22106fca19bfef3843fa1"
},
{
"_comment": "",
"name": "ENVOY_SHA",
"repoName": "envoyproxy/envoy",
"file": "WORKSPACE",
"lastStableSHA": "48b161ee02f4ca77b8c26959c4058456ad3f197f"
"lastStableSHA": "2a2ad48a7d4b57512bc10a9593e852fe950b1c8d"
}
]
4 changes: 2 additions & 2 deletions repositories.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ cc_library(
actual = "@googletest_git//:googletest_prod",
)

ISTIO_API = "6b9e3a501e6ef254958bf82f7b74c37d64a57a15"
ISTIO_API_SHA256 = "ce43fcc51bd7c653d39b810e50df68e32ed95991919c1d1f2f56b331d79e674e"
ISTIO_API = "056eb85d96f09441775d79283c149d93fcbd0982"
ISTIO_API_SHA256 = "df491c399f0a06bb2b85f43f5328c880c8e5cb5b3ce972efbd1ce137f83ebc52"

def mixerapi_repositories(bind=True):
BUILD = """
Expand Down
3 changes: 2 additions & 1 deletion src/envoy/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ envoy_cc_binary(
"//src/envoy/http/authn:filter_lib",
"//src/envoy/http/jwt_auth:http_filter_factory",
"//src/envoy/http/mixer:filter_lib",
"//src/envoy/tcp/forward_downstream_sni:config_lib",
"//src/envoy/tcp/mixer:filter_lib",
"//src/envoy/tcp/sni_verifier:config_lib",
"//src/envoy/tcp/tcp_cluster_rewrite:config_lib",
"//src/envoy/tcp/forward_downstream_sni:config_lib",
"@envoy//source/exe:envoy_main_entry_lib",
],
)
Expand Down
35 changes: 34 additions & 1 deletion src/envoy/http/authn/authenticator_base.cc
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@ namespace Http {
namespace Istio {
namespace AuthN {

namespace {
// The default header name for an exchanged token
static const std::string kExchangedTokenHeaderName = "ingress-authorization";

// Returns whether the header for an exchanged token is found
bool FindHeaderOfExchangedToken(const iaapi::Jwt& jwt) {
return (jwt.jwt_headers_size() == 1 &&
LowerCaseString(kExchangedTokenHeaderName) ==
LowerCaseString(jwt.jwt_headers(0)));
}

} // namespace

AuthenticatorBase::AuthenticatorBase(FilterContext* filter_context)
: filter_context_(*filter_context) {}

Expand Down Expand Up @@ -68,7 +81,27 @@ bool AuthenticatorBase::validateX509(const iaapi::MutualTls& mtls,
bool AuthenticatorBase::validateJwt(const iaapi::Jwt& jwt, Payload* payload) {
std::string jwt_payload;
if (filter_context()->getJwtPayload(jwt.issuer(), &jwt_payload)) {
return AuthnUtils::ProcessJwtPayload(jwt_payload, payload->mutable_jwt());
std::string payload_to_process = jwt_payload;
std::string original_payload;
if (FindHeaderOfExchangedToken(jwt)) {
if (AuthnUtils::ExtractOriginalPayload(jwt_payload, &original_payload)) {
// When the header of an exchanged token is found and the token
// contains the claim of the original payload, the original payload
// is extracted and used as the token payload.
payload_to_process = original_payload;
} else {
// When the header of an exchanged token is found but the token
// does not contain the claim of the original payload, it
// is regarded as an invalid exchanged token.
ENVOY_LOG(
error,
"Expect exchanged-token with original payload claim. Received: {}",
jwt_payload);
return false;
}
}
return AuthnUtils::ProcessJwtPayload(payload_to_process,
payload->mutable_jwt());
}
return false;
}
Expand Down
105 changes: 105 additions & 0 deletions src/envoy/http/authn/authenticator_base_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,31 @@ const std::string kSecIstioAuthUserinfoHeaderValue =
}
)";

const std::string kExchangedTokenHeaderName = "ingress-authorization";

const std::string kExchangedTokenPayload =
R"(
{
"iss": "token-service",
"sub": "subject",
"aud": ["aud1", "aud2"],
"original_claims": {
"iss": "https://accounts.example.com",
"sub": "example-subject",
"email": "user@example.com"
}
}
)";

const std::string kExchangedTokenPayloadNoOriginalClaims =
R"(
{
"iss": "token-service",
"sub": "subject",
"aud": ["aud1", "aud2"]
}
)";

class MockAuthenticatorBase : public AuthenticatorBase {
public:
MockAuthenticatorBase(FilterContext* filter_context)
Expand Down Expand Up @@ -293,6 +318,86 @@ TEST_F(ValidateJwtTest, JwtPayloadAvailable) {
EXPECT_TRUE(MessageDifferencer::Equals(expected_payload, *payload_));
}

TEST_F(ValidateJwtTest, OriginalPayloadOfExchangedToken) {
jwt_.set_issuer("token-service");
jwt_.add_jwt_headers(kExchangedTokenHeaderName);

(*dynamic_metadata_.mutable_filter_metadata())[Utils::IstioFilterName::kJwt]
.MergeFrom(
MessageUtil::keyValueStruct("token-service", kExchangedTokenPayload));

Payload expected_payload;
JsonStringToMessage(
R"({
"jwt": {
"user": "https://accounts.example.com/example-subject",
"claims": {
"iss": ["https://accounts.example.com"],
"sub": ["example-subject"],
"email": ["user@example.com"]
},
"raw_claims": "{\"email\":\"user@example.com\",\"iss\":\"https://accounts.example.com\",\"sub\":\"example-subject\"}"
}
}
)",
&expected_payload, google::protobuf::util::JsonParseOptions{});

EXPECT_TRUE(authenticator_.validateJwt(jwt_, payload_));
// On different platforms, the order of fields in raw_claims may be
// different. E.g., on MacOs, the raw_claims in the payload_ can be:
// raw_claims:
// "{\"email\":\"user@example.com\",\"sub\":\"example-subject\",\"iss\":\"https://accounts.example.com\"}"
// Therefore, raw_claims is skipped to avoid a flaky test.
MessageDifferencer diff;
const google::protobuf::FieldDescriptor* field =
expected_payload.jwt().GetDescriptor()->FindFieldByName("raw_claims");
diff.IgnoreField(field);
EXPECT_TRUE(diff.Compare(expected_payload, *payload_));
}

TEST_F(ValidateJwtTest, OriginalPayloadOfExchangedTokenMissing) {
jwt_.set_issuer("token-service");
jwt_.add_jwt_headers(kExchangedTokenHeaderName);

(*dynamic_metadata_.mutable_filter_metadata())[Utils::IstioFilterName::kJwt]
.MergeFrom(MessageUtil::keyValueStruct(
"token-service", kExchangedTokenPayloadNoOriginalClaims));

// When no original_claims in an exchanged token, the token
// is treated as invalid.
EXPECT_FALSE(authenticator_.validateJwt(jwt_, payload_));
}

TEST_F(ValidateJwtTest, OriginalPayloadOfExchangedTokenNotInIntendedHeader) {
jwt_.set_issuer("token-service");

(*dynamic_metadata_.mutable_filter_metadata())[Utils::IstioFilterName::kJwt]
.MergeFrom(
MessageUtil::keyValueStruct("token-service", kExchangedTokenPayload));

Payload expected_payload;
JsonStringToMessage(
R"({
"jwt": {
"user": "token-service/subject",
"audiences": ["aud1", "aud2"],
"claims": {
"iss": ["token-service"],
"sub": ["subject"],
"aud": ["aud1", "aud2"]
},
"raw_claims":"\n {\n \"iss\": \"token-service\",\n \"sub\": \"subject\",\n \"aud\": [\"aud1\", \"aud2\"],\n \"original_claims\": {\n \"iss\": \"https://accounts.example.com\",\n \"sub\": \"example-subject\",\n \"email\": \"user@example.com\"\n }\n }\n "
}
}
)",
&expected_payload, google::protobuf::util::JsonParseOptions{});

// When an exchanged token is not in the intended header, the token
// is treated as a normal token with its claims extracted.
EXPECT_TRUE(authenticator_.validateJwt(jwt_, payload_));
EXPECT_TRUE(MessageDifferencer::Equals(expected_payload, *payload_));
}

} // namespace
} // namespace AuthN
} // namespace Istio
Expand Down
39 changes: 37 additions & 2 deletions src/envoy/http/authn/authn_utils.cc
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

#include <regex>

#include "absl/strings/match.h"
#include "authn_utils.h"
#include "common/json/json_loader.h"
#include "google/protobuf/struct.pb.h"
Expand All @@ -27,6 +28,10 @@ namespace AuthN {
namespace {
// The JWT audience key name
static const std::string kJwtAudienceKey = "aud";
// The JWT issuer key name
static const std::string kJwtIssuerKey = "iss";
// The key name for the original claims in an exchanged token
static const std::string kExchangedTokenOriginalPayload = "original_claims";

// Extract JWT claim as a string list.
// This function only extracts string and string list claims.
Expand Down Expand Up @@ -100,6 +105,36 @@ bool AuthnUtils::ProcessJwtPayload(const std::string& payload_str,
return true;
}

bool AuthnUtils::ExtractOriginalPayload(const std::string& token,
std::string* original_payload) {
Envoy::Json::ObjectSharedPtr json_obj;
try {
json_obj = Json::Factory::loadFromString(token);
} catch (...) {
return false;
}

if (json_obj->hasObject(kExchangedTokenOriginalPayload) == false) {
return false;
}

Envoy::Json::ObjectSharedPtr original_payload_obj;
try {
auto original_payload_obj =
json_obj->getObject(kExchangedTokenOriginalPayload);
*original_payload = original_payload_obj->asJsonString();
ENVOY_LOG(debug, "{}: the original payload in exchanged token is {}",
__FUNCTION__, *original_payload);
} catch (...) {
ENVOY_LOG(debug,
"{}: original_payload in exchanged token is of invalid format.",
__FUNCTION__);
return false;
}

return true;
}

bool AuthnUtils::MatchString(const char* const str,
const iaapi::StringMatch& match) {
if (str == nullptr) {
Expand All @@ -110,10 +145,10 @@ bool AuthnUtils::MatchString(const char* const str,
return match.exact().compare(str) == 0;
}
case iaapi::StringMatch::kPrefix: {
return StringUtil::startsWith(str, match.prefix());
return absl::StartsWith(str, match.prefix());
}
case iaapi::StringMatch::kSuffix: {
return StringUtil::endsWith(str, match.suffix());
return absl::EndsWith(str, match.suffix());
}
case iaapi::StringMatch::kRegex: {
return std::regex_match(str, std::regex(match.regex()));
Expand Down
6 changes: 6 additions & 0 deletions src/envoy/http/authn/authn_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ class AuthnUtils : public Logger::Loggable<Logger::Id::filter> {
static bool ProcessJwtPayload(const std::string& jwt_payload_str,
istio::authn::JwtPayload* payload);

// Parses the original_payload in an exchanged JWT.
// Returns true if original_payload can be
// parsed successfully. Otherwise, returns false.
static bool ExtractOriginalPayload(const std::string& token,
std::string* original_payload);

// Returns true if str is matched to match.
static bool MatchString(const char* const str,
const iaapi::StringMatch& match);
Expand Down
1 change: 1 addition & 0 deletions src/envoy/http/authn/sample/APToken/APToken-example1.jwt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
eyJhbGciOiJSUzI1NiIsImtpZCI6IkRIRmJwb0lVcXJZOHQyenBBMnFYZkNtcjVWTzVaRXI0UnpIVV8tZW52dlEiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJleGFtcGxlLWF1ZGllbmNlIiwiZW1haWwiOiJmb29AZ29vZ2xlLmNvbSIsImV4cCI6NDY5ODM2MTUwOCwiaWF0IjoxNTQ0NzYxNTA4LCJpc3MiOiJodHRwczovL2V4YW1wbGUudG9rZW5fc2VydmljZS5jb20iLCJpc3Rpb19hdHRyaWJ1dGVzIjpbeyJzb3VyY2UuaXAiOiIxMjcuMC4wLjEifV0sImtleTEiOlsidmFsMiIsInZhbDMiXSwib3JpZ2luYWxfY2xhaW1zIjp7ImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlzcyI6Imh0dHBzOi8vYWNjb3VudHMuZXhhbXBsZS5jb20iLCJzdWIiOiJleGFtcGxlLXN1YmplY3QifSwic3ViIjoiaHR0cHM6Ly9hY2NvdW50cy5leGFtcGxlLmNvbS8xMjM0NTU2Nzg5MCJ9.mLm9Gmcd748anwybiPxGPEuYgJBChqoHkVOvRhQN-H9jMqVKyF-7ynud1CJp5n72VeMB1FzvKAV0ErzSyWQc0iofQywG6whYXP6zL-Oc0igUrLDvzb6PuBDkbWOcZrvHkHM4tIYAkF4j880GqMWEP3gGrykziIEY9g4povquCFSdkLjjyol2-Ge_6MFdayYoeWLLOaMP7tHiPTm_ajioQ4jcz5whBWu3DZWx4IuU5UIBYlHG_miJZv5zmwwQ60T1_p_sW7zkABJgDhCvu6cHh6g-hZdQvZbATFwMfN8VDzttTjRG8wuLlkQ1TTOCx5PDv-_gHfQfRWt8Z94HrIJPuQ
Loading