Skip to content
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

Authenticate an exchanged token #2070

Merged
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": "[email protected]"
}
}
)";

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": ["[email protected]"]
},
"raw_claims": "{\"email\":\"[email protected]\",\"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\":\"[email protected]\",\"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\": \"[email protected]\"\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
34 changes: 34 additions & 0 deletions src/envoy/http/authn/authn_utils.cc
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,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 +104,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 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
118 changes: 118 additions & 0 deletions src/envoy/http/authn/sample/APToken/aptoken-envoy.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
{
"admin": {
"access_log_path": "/dev/stdout",
"address": {
"socket_address": {
"address": "0.0.0.0",
"port_value": 9001
}
}
},
"static_resources": {
"clusters": [
{
"name": "service1",
"connect_timeout": "5s",
"type": "STATIC",
"hosts": [
{
"socket_address": {
"address": "0.0.0.0",
"port_value": 8080
}
}
]
}
],
"listeners": [
{
"name": "server",
"address": {
"socket_address": {
"address": "0.0.0.0",
"port_value": 9090
}
},
"filter_chains": [
{
"filters": [
{
"name": "envoy.http_connection_manager",
"config": {
"codec_type": "AUTO",
"stat_prefix": "inbound_http",
"access_log": [
{
"name": "envoy.file_access_log",
"config": {
"path": "/tmp/envoy-access.log"
}
}
],
"http_filters": [
{
"name": "jwt-auth",
"config": {
"rules": [
{
"issuer": "https://example.token_service.com",
"local_jwks": {
"inline_string": "{ \"keys\":[ {\"e\":\"AQAB\",\"kid\":\"DHFbpoIUqrY8t2zpA2qXfCmr5VO5ZEr4RzHU_-envvQ\",\"kty\":\"RSA\",\"n\":\"xAE7eB6qugXyCAG3yhh7pkDkT65pHymX-P7KfIupjf59vsdo91bSP9C8H07pSAGQO1MV_xFj9VswgsCg4R6otmg5PV2He95lZdHtOcU5DXIg_pbhLdKXbi66GlVeK6ABZOUW3WYtnNHD-91gVuoeJT_DwtGGcp4ignkgXfkiEm4sw-4sfb4qdt5oLbyVpmW6x9cfa7vs2WTfURiCrBoUqgBo_-4WTiULmmHSGZHOjzwa8WtrtOQGsAFjIbno85jp6MnGGGZPYZbDAa_b3y5u-YpW7ypZrvD8BgtKVjgtQgZhLAGezMt0ua3DRrWnKqTZ0BJ_EyxOGuHJrLsn00fnMQ\"}]}",
},
"from_headers": [{"name": "ingress-authorization"}],
"forward_payload_header": "test-jwt-payload-output"
}
]
}
},
{
"name":"istio_authn",
"config":{
"policy":{
"origins":[
{
"jwt":{
"issuer":"https://example.token_service.com",
"jwt_headers":["ingress-authorization"]
}
}
],
"principal_binding":1
}
}
},
{
"name": "envoy.router"
}
],
"route_config": {
"name": "backend",
"virtual_hosts": [
{
"name": "backend",
"domains": [
"*"
],
"routes": [
{
"match": {
"prefix": "/"
},
"route": {
"cluster": "service1",
"timeout": "0s"
}
}
]
}
]
}
}
}
]
}
]
}
]
}
}
18 changes: 18 additions & 0 deletions src/envoy/http/authn/sample/APToken/guide.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
This is a guide of sending an example exchanged token to
the jwt-authn filter and the Istio authn filter, and observing
that the example backend echoes back the request when
the authentication succeeds.

1. Open a terminal, go to the root directory of the istio-proxy repository.
Start the example backend:
go run test/backend/echo/echo.go

2. Build the Istio proxy and run the proxy with the config for authenticating
an example exchanged token.
bazel build //src/envoy:envoy
bazel-bin/src/envoy/envoy -l debug -c src/envoy/http/jwt_auth/sample/APToken/aptoken-envoy.conf

3. Open a terminal, go to the root directory of the istio-proxy repository.
Send a request with the example exchanged token.
export token=$(cat src/envoy/http/jwt_auth/sample/APToken/APToken-example1.jwt)
curl --header "ingress-authorization:$token" http://localhost:9090/echo -d "hello world"
18 changes: 17 additions & 1 deletion test/integration/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,20 @@ envoy_cc_test(
"//src/envoy/utils:filter_names_lib",
"//src/envoy/http/mixer:filter_lib",
],
)
)

envoy_cc_test(
name = "exchanged_token_integration_test",
srcs = ["exchanged_token_integration_test.cc"],
repository = "@envoy",
deps = [
"@envoy//source/common/common:utility_lib",
"@envoy//test/integration:http_protocol_integration_lib",
"//include/istio/utils:attribute_names_header",
"//src/envoy/http/authn:filter_lib",
"//src/envoy/http/jwt_auth:http_filter_factory",
"//src/envoy/http/jwt_auth:jwt_lib",
"//src/envoy/utils:filter_names_lib",
"//src/envoy/http/mixer:filter_lib",
],
)
Loading