From e3f0b6609da756d43ddfb8803876f167f2d5cc1e Mon Sep 17 00:00:00 2001 From: Lei Tang <32078630+lei-tang@users.noreply.github.com> Date: Mon, 10 Dec 2018 10:49:10 +0800 Subject: [PATCH 01/12] Authenticate an exchanged token --- src/envoy/http/authn/authenticator_base.cc | 8 +- src/envoy/http/authn/authn_utils.cc | 40 ++++++ src/envoy/http/authn/authn_utils.h | 6 + .../sample/APToken/APToken-example1.jwt | 1 + .../sample/APToken/aptoken-envoy.conf | 116 ++++++++++++++++++ .../http/jwt_auth/sample/APToken/jwks.json | 1 + .../http/jwt_auth/sample/APToken/key.pem | 27 ++++ .../http/jwt_auth/sample/APToken/pubkey.pem | 9 ++ 8 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 src/envoy/http/jwt_auth/sample/APToken/APToken-example1.jwt create mode 100644 src/envoy/http/jwt_auth/sample/APToken/aptoken-envoy.conf create mode 100644 src/envoy/http/jwt_auth/sample/APToken/jwks.json create mode 100644 src/envoy/http/jwt_auth/sample/APToken/key.pem create mode 100644 src/envoy/http/jwt_auth/sample/APToken/pubkey.pem diff --git a/src/envoy/http/authn/authenticator_base.cc b/src/envoy/http/authn/authenticator_base.cc index 9262d249e25..394285e73a5 100644 --- a/src/envoy/http/authn/authenticator_base.cc +++ b/src/envoy/http/authn/authenticator_base.cc @@ -68,7 +68,13 @@ 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 (AuthnUtils::ExtractOriginalPayload(jwt_payload, &original_payload)) { + payload_to_process = original_payload; + } + return AuthnUtils::ProcessJwtPayload(payload_to_process, + payload->mutable_jwt()); } return false; } diff --git a/src/envoy/http/authn/authn_utils.cc b/src/envoy/http/authn/authn_utils.cc index 4a022da8c1e..36508352e8e 100644 --- a/src/envoy/http/authn/authn_utils.cc +++ b/src/envoy/http/authn/authn_utils.cc @@ -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 APToken original claims +static const std::string kAPTokenOriginalPayload = "original_payload"; // Extract JWT claim as a string list. // This function only extracts string and string list claims. @@ -100,6 +104,42 @@ 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(kAPTokenOriginalPayload) == false) { + return false; + } + + Envoy::Json::ObjectSharedPtr original_payload_obj; + try { + auto original_payload_obj = json_obj->getObject(kAPTokenOriginalPayload); + std::string iss1 = json_obj->getString(kJwtIssuerKey, ""); + std::string iss2 = original_payload_obj->getString(kJwtIssuerKey, ""); + // Token exchange makes the issuers of the APToken and the original JWT + // to be different. + if (!(!iss1.empty() && !iss2.empty() && iss1 != iss2)) { + return false; + } + + *original_payload = original_payload_obj->asJsonString(); + ENVOY_LOG(debug, "{}: original_payload in APToken is {}", __FUNCTION__, + *original_payload); + } catch (...) { + ENVOY_LOG(debug, "{}: original_payload in APToken is of invalid format.", + __FUNCTION__); + return false; + } + + return true; +} + bool AuthnUtils::MatchString(const char* const str, const iaapi::StringMatch& match) { if (str == nullptr) { diff --git a/src/envoy/http/authn/authn_utils.h b/src/envoy/http/authn/authn_utils.h index 5a0f3fc4d45..010c365c645 100644 --- a/src/envoy/http/authn/authn_utils.h +++ b/src/envoy/http/authn/authn_utils.h @@ -38,6 +38,12 @@ class AuthnUtils : public Logger::Loggable { static bool ProcessJwtPayload(const std::string& jwt_payload_str, istio::authn::JwtPayload* payload); + // Parse the original_payload in an APToken. + // Return true if original_payload can be + // parsed successfully. Otherwise, return 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); diff --git a/src/envoy/http/jwt_auth/sample/APToken/APToken-example1.jwt b/src/envoy/http/jwt_auth/sample/APToken/APToken-example1.jwt new file mode 100644 index 00000000000..b1078d0af69 --- /dev/null +++ b/src/envoy/http/jwt_auth/sample/APToken/APToken-example1.jwt @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiIsImtpZCI6IkRIRmJwb0lVcXJZOHQyenBBMnFYZkNtcjVWTzVaRXI0UnpIVV8tZW52dlEiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJleGFtcGxlLWF1ZGllbmNlIiwiZW1haWwiOiJmb29AZ29vZ2xlLmNvbSIsImV4cCI6NDY5ODAyMzM3NywiaWF0IjoxNTQ0NDIzMzc3LCJpc3MiOiJodHRwczovL2Nsb3VkLmdvb2dsZS5jb20vaWFwIiwiaXN0aW9fYXR0cmlidXRlcyI6W3sic291cmNlLmlwIjoiMTI3LjAuMC4xIn1dLCJrZXkxIjpbInZhbDIiLCJ2YWwzIl0sIm9yaWdpbmFsX3BheWxvYWQiOnsiZW1haWwiOiJ1c2VyQGV4YW1wbGUuY29tIiwiaXNzIjoiaHR0cHM6Ly9hY2NvdW50cy5leGFtcGxlLmNvbSIsInN1YiI6ImV4YW1wbGUtc3ViamVjdCJ9LCJzdWIiOiJodHRwczovL2FjY291bnRzLmV4YW1wbGUuY29tLzEyMzQ1NTY3ODkwIn0.lY-bObNybFN8RxguZcgEd3TrC7-dSRH6VjXqkMvaN1WXz5xk8EEca0vlfkc2RRCZCCGDsSJiZOWOkV5c3RX5EAGcHppMFxuX2YZMQOoConuN_WCdp88gTKuICDUp_ZLi06nWOzqhIr4GG2QYDxSOMxTFqMl_qcJTKEcjpJM-pbhVTgbTb020C4_mFOEespff2Kh_ySzkZnlreddP_oUvUrKKoIHBf3M_ZKpivuSyyBjL52iFmsMJL4nmsk-xLIDuw5NeNyUGDa5j-EwiD3XvrFurrIAkKPT42E5Z5uomuw8h-zUoJt_jpdIynOOA2eyQXfXwsVnpOnMWDut66u92xQ diff --git a/src/envoy/http/jwt_auth/sample/APToken/aptoken-envoy.conf b/src/envoy/http/jwt_auth/sample/APToken/aptoken-envoy.conf new file mode 100644 index 00000000000..46820ceab22 --- /dev/null +++ b/src/envoy/http/jwt_auth/sample/APToken/aptoken-envoy.conf @@ -0,0 +1,116 @@ +{ + "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://cloud.google.com/iap", + "local_jwks": { + "inline_string": "{ \"keys\":[ {\"e\":\"AQAB\",\"kid\":\"DHFbpoIUqrY8t2zpA2qXfCmr5VO5ZEr4RzHU_-envvQ\",\"kty\":\"RSA\",\"n\":\"xAE7eB6qugXyCAG3yhh7pkDkT65pHymX-P7KfIupjf59vsdo91bSP9C8H07pSAGQO1MV_xFj9VswgsCg4R6otmg5PV2He95lZdHtOcU5DXIg_pbhLdKXbi66GlVeK6ABZOUW3WYtnNHD-91gVuoeJT_DwtGGcp4ignkgXfkiEm4sw-4sfb4qdt5oLbyVpmW6x9cfa7vs2WTfURiCrBoUqgBo_-4WTiULmmHSGZHOjzwa8WtrtOQGsAFjIbno85jp6MnGGGZPYZbDAa_b3y5u-YpW7ypZrvD8BgtKVjgtQgZhLAGezMt0ua3DRrWnKqTZ0BJ_EyxOGuHJrLsn00fnMQ\"}]}", + }, + "forward_payload_header": "test-jwt-payload-output" + } + ] + } + }, + { + "name":"istio_authn", + "config":{ + "policy":{ + "origins":[ + { + "jwt":{ + "issuer":"https://cloud.google.com/iap", + } + } + ], + "principal_binding":1 + } + } + }, + { + "name": "envoy.router" + } + ], + "route_config": { + "name": "backend", + "virtual_hosts": [ + { + "name": "backend", + "domains": [ + "*" + ], + "routes": [ + { + "match": { + "prefix": "/" + }, + "route": { + "cluster": "service1", + "timeout": "0s" + } + } + ] + } + ] + } + } + } + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/src/envoy/http/jwt_auth/sample/APToken/jwks.json b/src/envoy/http/jwt_auth/sample/APToken/jwks.json new file mode 100644 index 00000000000..b2c93b48a1d --- /dev/null +++ b/src/envoy/http/jwt_auth/sample/APToken/jwks.json @@ -0,0 +1 @@ +{ "keys":[ {"e":"AQAB","kid":"DHFbpoIUqrY8t2zpA2qXfCmr5VO5ZEr4RzHU_-envvQ","kty":"RSA","n":"xAE7eB6qugXyCAG3yhh7pkDkT65pHymX-P7KfIupjf59vsdo91bSP9C8H07pSAGQO1MV_xFj9VswgsCg4R6otmg5PV2He95lZdHtOcU5DXIg_pbhLdKXbi66GlVeK6ABZOUW3WYtnNHD-91gVuoeJT_DwtGGcp4ignkgXfkiEm4sw-4sfb4qdt5oLbyVpmW6x9cfa7vs2WTfURiCrBoUqgBo_-4WTiULmmHSGZHOjzwa8WtrtOQGsAFjIbno85jp6MnGGGZPYZbDAa_b3y5u-YpW7ypZrvD8BgtKVjgtQgZhLAGezMt0ua3DRrWnKqTZ0BJ_EyxOGuHJrLsn00fnMQ"}]} \ No newline at end of file diff --git a/src/envoy/http/jwt_auth/sample/APToken/key.pem b/src/envoy/http/jwt_auth/sample/APToken/key.pem new file mode 100644 index 00000000000..68fa79265a8 --- /dev/null +++ b/src/envoy/http/jwt_auth/sample/APToken/key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAxAE7eB6qugXyCAG3yhh7pkDkT65pHymX+P7KfIupjf59vsdo +91bSP9C8H07pSAGQO1MV/xFj9VswgsCg4R6otmg5PV2He95lZdHtOcU5DXIg/pbh +LdKXbi66GlVeK6ABZOUW3WYtnNHD+91gVuoeJT/DwtGGcp4ignkgXfkiEm4sw+4s +fb4qdt5oLbyVpmW6x9cfa7vs2WTfURiCrBoUqgBo/+4WTiULmmHSGZHOjzwa8Wtr +tOQGsAFjIbno85jp6MnGGGZPYZbDAa/b3y5u+YpW7ypZrvD8BgtKVjgtQgZhLAGe +zMt0ua3DRrWnKqTZ0BJ/EyxOGuHJrLsn00fnMQIDAQABAoIBAQCfv3ler2/qaYoX +6H6I4md02xK5tqbK1TWdpNwXoiSxLCgEY7YzULnPdSq+Qax0GYIaN9+Hof7cLFRf +XOxCTqCm+k8cqqwP0vTFhdFY8ltLkCdAGGyy0h7FmKPpboZv+9rnBqgaDntCgty+ +3HD2pZ2oMk407Fwt8qChwmMU9EZGycIRxwTFB8xGpss8gFc+fTAmvGFI/s8aclu+ ++XmvPkw0lz+dLaNsne0yBHVMPFtncqhbsFbnbB8OAMtWsVbTwU60OVeZ/oOYJVY9 +vtuMQ4IAi5kMx8AWQ1UDvW2ZnuGpQkr6dZ2YP/IMOXGaywbgcHPUitrW3iIYYW+U +NIVWXkA5AoGBAPd9MesQduCsngWNvtIjrNUEakbXkg0vbw+dRIbIqy2Bx/tyWhim +qFNYcNjZELqp5C3f3hlO/k9K8xxJLIYloYg1948DbpOhVk8SdFGgpCYC4q/fBPw+ +kAFQEKhZelwKj2Srf58BCrhIe1KPHbQfTF/JZ7JBndIw79/1p9rU6wWLAoGBAMq+ +yd+7L8wpCZNtmjDUTWmGq3LmFLBzBMhwmV2NOKbTFakMVyEtSORQFOJbxFwuWx5z +j3nLq9ivQgNKLPLSTF/Dtej2lzgNd9YG/9XW+6HeZ841XNUHE0XsU6Mm1EAyadfu +AAnoxTh/NqYvF81qCM9tl9Bsjx8klOyXrrGKj/WzAoGAKJC2u+bI9W6VwCdJnbwH +OistGEuBPvQFajPG5ajClgTtuIM3zU6TzIV0ibaajV4HbpWBG/jcqjaIvpwn1h0Y +6vCdkS1o1H3fXbqSokaIYUqbyWPut0Gx7OUotc9kxO1eL4wEsRVEoowO2qtmnP18 +UT775jXnHmqzBqyHRNEdbJ0CgYAdgPAdp77H5fznwF5c1rhBMADJIqRGHSbICGK5 +E3D4DeWsCQiw4kcmOmUfn50OkQxffQ+W+MWULcTcd7Hc0C+fC/rv4NqWpJcYxUH7 +m2JY5uWSQ3+z3Gi4lzCAoIjooq12Z8MHriDtHM4WFupO0SxhCyC5iuK09HzbhSM9 +4N0cMwKBgGfYJRmX5pW7rKo/O1Zl6oG6VBA6fYXIe28X9sZ0YCbVIuX8MgRk2Vpm +y47qmzEQ/1kwWtQzZOcePM8rd8qTTFPCkM45S1ieh7qjNSMVIbl8I635UqCCoetW +VDwvUhVLO4mBT0p2F8g6Wngmpy9ZBh2B52w3Rz9UxHffwdC7hhQD +-----END RSA PRIVATE KEY----- diff --git a/src/envoy/http/jwt_auth/sample/APToken/pubkey.pem b/src/envoy/http/jwt_auth/sample/APToken/pubkey.pem new file mode 100644 index 00000000000..db058e7ad39 --- /dev/null +++ b/src/envoy/http/jwt_auth/sample/APToken/pubkey.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxAE7eB6qugXyCAG3yhh7 +pkDkT65pHymX+P7KfIupjf59vsdo91bSP9C8H07pSAGQO1MV/xFj9VswgsCg4R6o +tmg5PV2He95lZdHtOcU5DXIg/pbhLdKXbi66GlVeK6ABZOUW3WYtnNHD+91gVuoe +JT/DwtGGcp4ignkgXfkiEm4sw+4sfb4qdt5oLbyVpmW6x9cfa7vs2WTfURiCrBoU +qgBo/+4WTiULmmHSGZHOjzwa8WtrtOQGsAFjIbno85jp6MnGGGZPYZbDAa/b3y5u ++YpW7ypZrvD8BgtKVjgtQgZhLAGezMt0ua3DRrWnKqTZ0BJ/EyxOGuHJrLsn00fn +MQIDAQAB +-----END PUBLIC KEY----- From a27974febea4365faea44a644c104f3b38d02800 Mon Sep 17 00:00:00 2001 From: Lei Tang <32078630+lei-tang@users.noreply.github.com> Date: Thu, 13 Dec 2018 17:23:11 +0800 Subject: [PATCH 02/12] Change issuer name and jwt-authn output with key being original issuer --- src/envoy/http/jwt_auth/jwt_authenticator.cc | 35 ++++++++++++++++++- .../sample/APToken/APToken-example1.jwt | 2 +- .../sample/APToken/aptoken-envoy.conf | 4 +-- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/envoy/http/jwt_auth/jwt_authenticator.cc b/src/envoy/http/jwt_auth/jwt_authenticator.cc index b309b6b169a..dcd86915abe 100644 --- a/src/envoy/http/jwt_auth/jwt_authenticator.cc +++ b/src/envoy/http/jwt_auth/jwt_authenticator.cc @@ -24,6 +24,10 @@ namespace { // The HTTP header to pass verified token payload. const LowerCaseString kJwtPayloadKey("sec-istio-auth-userinfo"); +// The JWT issuer key name +const std::string kJwtIssuerKey = "iss"; +// The key name for the APToken original claims +const std::string kAPTokenOriginalPayload = "original_payload"; // Extract host and path from a URI void ExtractUriHostPath(const std::string& uri, std::string* host, @@ -47,6 +51,31 @@ void ExtractUriHostPath(const std::string& uri, std::string* host, } } +bool ExtractOriginalIssuer(const Envoy::Json::ObjectSharedPtr& payload, + std::string* issuer) { + if (payload->hasObject(kAPTokenOriginalPayload) == false) { + return false; + } + + Envoy::Json::ObjectSharedPtr original_payload_obj; + try { + auto original_payload_obj = payload->getObject(kAPTokenOriginalPayload); + std::string iss1 = payload->getString(kJwtIssuerKey, ""); + std::string iss2 = original_payload_obj->getString(kJwtIssuerKey, ""); + // Token exchange makes the issuers of the APToken and the original JWT + // to be different. + if (!(!iss1.empty() && !iss2.empty() && iss1 != iss2)) { + return false; + } + + *issuer = iss2; + } catch (...) { + return false; + } + + return true; +} + } // namespace JwtAuthenticator::JwtAuthenticator(Upstream::ClusterManager& cm, @@ -199,7 +228,11 @@ void JwtAuthenticator::VerifyKey(const PubkeyCacheItem& issuer_item) { // User the issuer as the entry key for simplicity. The forward_payload_header // field can be removed or replace by a boolean (to make `save` is // conditional) - callback_->savePayload(issuer_item.jwt_config().issuer(), jwt_->PayloadStr()); + std::string issuer; + if (!ExtractOriginalIssuer(jwt_->Payload(), &issuer)) { + issuer = issuer_item.jwt_config().issuer(); + } + callback_->savePayload(issuer, jwt_->PayloadStr()); if (!issuer_item.jwt_config().forward()) { // Remove JWT from headers. diff --git a/src/envoy/http/jwt_auth/sample/APToken/APToken-example1.jwt b/src/envoy/http/jwt_auth/sample/APToken/APToken-example1.jwt index b1078d0af69..61bdab5fd79 100644 --- a/src/envoy/http/jwt_auth/sample/APToken/APToken-example1.jwt +++ b/src/envoy/http/jwt_auth/sample/APToken/APToken-example1.jwt @@ -1 +1 @@ -eyJhbGciOiJSUzI1NiIsImtpZCI6IkRIRmJwb0lVcXJZOHQyenBBMnFYZkNtcjVWTzVaRXI0UnpIVV8tZW52dlEiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJleGFtcGxlLWF1ZGllbmNlIiwiZW1haWwiOiJmb29AZ29vZ2xlLmNvbSIsImV4cCI6NDY5ODAyMzM3NywiaWF0IjoxNTQ0NDIzMzc3LCJpc3MiOiJodHRwczovL2Nsb3VkLmdvb2dsZS5jb20vaWFwIiwiaXN0aW9fYXR0cmlidXRlcyI6W3sic291cmNlLmlwIjoiMTI3LjAuMC4xIn1dLCJrZXkxIjpbInZhbDIiLCJ2YWwzIl0sIm9yaWdpbmFsX3BheWxvYWQiOnsiZW1haWwiOiJ1c2VyQGV4YW1wbGUuY29tIiwiaXNzIjoiaHR0cHM6Ly9hY2NvdW50cy5leGFtcGxlLmNvbSIsInN1YiI6ImV4YW1wbGUtc3ViamVjdCJ9LCJzdWIiOiJodHRwczovL2FjY291bnRzLmV4YW1wbGUuY29tLzEyMzQ1NTY3ODkwIn0.lY-bObNybFN8RxguZcgEd3TrC7-dSRH6VjXqkMvaN1WXz5xk8EEca0vlfkc2RRCZCCGDsSJiZOWOkV5c3RX5EAGcHppMFxuX2YZMQOoConuN_WCdp88gTKuICDUp_ZLi06nWOzqhIr4GG2QYDxSOMxTFqMl_qcJTKEcjpJM-pbhVTgbTb020C4_mFOEespff2Kh_ySzkZnlreddP_oUvUrKKoIHBf3M_ZKpivuSyyBjL52iFmsMJL4nmsk-xLIDuw5NeNyUGDa5j-EwiD3XvrFurrIAkKPT42E5Z5uomuw8h-zUoJt_jpdIynOOA2eyQXfXwsVnpOnMWDut66u92xQ +eyJhbGciOiJSUzI1NiIsImtpZCI6IkRIRmJwb0lVcXJZOHQyenBBMnFYZkNtcjVWTzVaRXI0UnpIVV8tZW52dlEiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJleGFtcGxlLWF1ZGllbmNlIiwiZW1haWwiOiJmb29AZ29vZ2xlLmNvbSIsImV4cCI6NDY5ODI4NTY2MiwiaWF0IjoxNTQ0Njg1NjYyLCJpc3MiOiJodHRwczovL2V4YW1wbGUudG9rZW5fc2VydmljZS5jb20iLCJpc3Rpb19hdHRyaWJ1dGVzIjpbeyJzb3VyY2UuaXAiOiIxMjcuMC4wLjEifV0sImtleTEiOlsidmFsMiIsInZhbDMiXSwib3JpZ2luYWxfcGF5bG9hZCI6eyJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJpc3MiOiJodHRwczovL2FjY291bnRzLmV4YW1wbGUuY29tIiwic3ViIjoiZXhhbXBsZS1zdWJqZWN0In0sInN1YiI6Imh0dHBzOi8vYWNjb3VudHMuZXhhbXBsZS5jb20vMTIzNDU1Njc4OTAifQ.ZAVGs7atnllS0symhdD5bKV8qmaj1S5T9d1M-5rt-glfvYGpqLTRz-ru9js2os2RBnhnhwYsBt8wIPPnDwcj_iktESV4QlvX73RXj5rrG9O3HELet8GBqzxBiPOrA5pfSLvEQZanw-yf-NDw4vhwczReZe8L90NihAVrdwRPf5aZRZlzkpsCkQR-GdeJviDqkMhrRFVho5sq6QohUr3vt2DuItxCkKaFj_5024lb37iFuqEKkAa-3T-Di15-TywGZgTcz_723Uj7TL5Dj7ar3gdtGIXnK7KvTLiR3Ue2oOrgWkkOL62qF34wKFbJCEKOOIOFF8CC9jF88nVf2rgVGQ \ No newline at end of file diff --git a/src/envoy/http/jwt_auth/sample/APToken/aptoken-envoy.conf b/src/envoy/http/jwt_auth/sample/APToken/aptoken-envoy.conf index 46820ceab22..508f9d8e4d5 100644 --- a/src/envoy/http/jwt_auth/sample/APToken/aptoken-envoy.conf +++ b/src/envoy/http/jwt_auth/sample/APToken/aptoken-envoy.conf @@ -55,7 +55,7 @@ "config": { "rules": [ { - "issuer": "https://cloud.google.com/iap", + "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\"}]}", }, @@ -71,7 +71,7 @@ "origins":[ { "jwt":{ - "issuer":"https://cloud.google.com/iap", + "issuer":"https://accounts.example.com" } } ], From 68be7c14a249a95dec5ae83a7f6393873b1360dd Mon Sep 17 00:00:00 2001 From: Lei Tang <32078630+lei-tang@users.noreply.github.com> Date: Fri, 14 Dec 2018 12:51:51 +0800 Subject: [PATCH 03/12] Revised the code based on the discussion --- src/envoy/http/authn/authenticator_base.cc | 15 +++++++- src/envoy/http/authn/authn_utils.cc | 4 +-- src/envoy/http/jwt_auth/jwt_authenticator.cc | 35 +------------------ .../sample/APToken/APToken-example1.jwt | 2 +- .../sample/APToken/aptoken-envoy.conf | 4 ++- .../http/jwt_auth/sample/APToken/guide.txt | 18 ++++++++++ .../http/jwt_auth/sample/APToken/jwks.json | 1 - .../http/jwt_auth/sample/APToken/key.pem | 27 -------------- .../http/jwt_auth/sample/APToken/pubkey.pem | 9 ----- 9 files changed, 39 insertions(+), 76 deletions(-) create mode 100644 src/envoy/http/jwt_auth/sample/APToken/guide.txt delete mode 100644 src/envoy/http/jwt_auth/sample/APToken/jwks.json delete mode 100644 src/envoy/http/jwt_auth/sample/APToken/key.pem delete mode 100644 src/envoy/http/jwt_auth/sample/APToken/pubkey.pem diff --git a/src/envoy/http/authn/authenticator_base.cc b/src/envoy/http/authn/authenticator_base.cc index 394285e73a5..37b8ecdaaed 100644 --- a/src/envoy/http/authn/authenticator_base.cc +++ b/src/envoy/http/authn/authenticator_base.cc @@ -29,6 +29,11 @@ namespace Http { namespace Istio { namespace AuthN { +namespace { +// The default header name for an exchanged token +static const std::string kExchangedTokenHeaderName = "x-ingress-authorization"; +} // namespace + AuthenticatorBase::AuthenticatorBase(FilterContext* filter_context) : filter_context_(*filter_context) {} @@ -70,7 +75,15 @@ bool AuthenticatorBase::validateJwt(const iaapi::Jwt& jwt, Payload* payload) { if (filter_context()->getJwtPayload(jwt.issuer(), &jwt_payload)) { std::string payload_to_process = jwt_payload; std::string original_payload; - if (AuthnUtils::ExtractOriginalPayload(jwt_payload, &original_payload)) { + bool found = false; + for (auto h : jwt.jwt_headers()) { + if (kExchangedTokenHeaderName == h) { + found = true; + break; + } + } + if (found && + AuthnUtils::ExtractOriginalPayload(jwt_payload, &original_payload)) { payload_to_process = original_payload; } return AuthnUtils::ProcessJwtPayload(payload_to_process, diff --git a/src/envoy/http/authn/authn_utils.cc b/src/envoy/http/authn/authn_utils.cc index 36508352e8e..6c43adb2936 100644 --- a/src/envoy/http/authn/authn_utils.cc +++ b/src/envoy/http/authn/authn_utils.cc @@ -30,7 +30,7 @@ static const std::string kJwtAudienceKey = "aud"; // The JWT issuer key name static const std::string kJwtIssuerKey = "iss"; // The key name for the APToken original claims -static const std::string kAPTokenOriginalPayload = "original_payload"; +static const std::string kAPTokenOriginalPayload = "original_claims"; // Extract JWT claim as a string list. // This function only extracts string and string list claims. @@ -129,7 +129,7 @@ bool AuthnUtils::ExtractOriginalPayload(const std::string& token, } *original_payload = original_payload_obj->asJsonString(); - ENVOY_LOG(debug, "{}: original_payload in APToken is {}", __FUNCTION__, + ENVOY_LOG(debug, "{}: the original payload in APToken is {}", __FUNCTION__, *original_payload); } catch (...) { ENVOY_LOG(debug, "{}: original_payload in APToken is of invalid format.", diff --git a/src/envoy/http/jwt_auth/jwt_authenticator.cc b/src/envoy/http/jwt_auth/jwt_authenticator.cc index dcd86915abe..b309b6b169a 100644 --- a/src/envoy/http/jwt_auth/jwt_authenticator.cc +++ b/src/envoy/http/jwt_auth/jwt_authenticator.cc @@ -24,10 +24,6 @@ namespace { // The HTTP header to pass verified token payload. const LowerCaseString kJwtPayloadKey("sec-istio-auth-userinfo"); -// The JWT issuer key name -const std::string kJwtIssuerKey = "iss"; -// The key name for the APToken original claims -const std::string kAPTokenOriginalPayload = "original_payload"; // Extract host and path from a URI void ExtractUriHostPath(const std::string& uri, std::string* host, @@ -51,31 +47,6 @@ void ExtractUriHostPath(const std::string& uri, std::string* host, } } -bool ExtractOriginalIssuer(const Envoy::Json::ObjectSharedPtr& payload, - std::string* issuer) { - if (payload->hasObject(kAPTokenOriginalPayload) == false) { - return false; - } - - Envoy::Json::ObjectSharedPtr original_payload_obj; - try { - auto original_payload_obj = payload->getObject(kAPTokenOriginalPayload); - std::string iss1 = payload->getString(kJwtIssuerKey, ""); - std::string iss2 = original_payload_obj->getString(kJwtIssuerKey, ""); - // Token exchange makes the issuers of the APToken and the original JWT - // to be different. - if (!(!iss1.empty() && !iss2.empty() && iss1 != iss2)) { - return false; - } - - *issuer = iss2; - } catch (...) { - return false; - } - - return true; -} - } // namespace JwtAuthenticator::JwtAuthenticator(Upstream::ClusterManager& cm, @@ -228,11 +199,7 @@ void JwtAuthenticator::VerifyKey(const PubkeyCacheItem& issuer_item) { // User the issuer as the entry key for simplicity. The forward_payload_header // field can be removed or replace by a boolean (to make `save` is // conditional) - std::string issuer; - if (!ExtractOriginalIssuer(jwt_->Payload(), &issuer)) { - issuer = issuer_item.jwt_config().issuer(); - } - callback_->savePayload(issuer, jwt_->PayloadStr()); + callback_->savePayload(issuer_item.jwt_config().issuer(), jwt_->PayloadStr()); if (!issuer_item.jwt_config().forward()) { // Remove JWT from headers. diff --git a/src/envoy/http/jwt_auth/sample/APToken/APToken-example1.jwt b/src/envoy/http/jwt_auth/sample/APToken/APToken-example1.jwt index 61bdab5fd79..82f1e6ab448 100644 --- a/src/envoy/http/jwt_auth/sample/APToken/APToken-example1.jwt +++ b/src/envoy/http/jwt_auth/sample/APToken/APToken-example1.jwt @@ -1 +1 @@ -eyJhbGciOiJSUzI1NiIsImtpZCI6IkRIRmJwb0lVcXJZOHQyenBBMnFYZkNtcjVWTzVaRXI0UnpIVV8tZW52dlEiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJleGFtcGxlLWF1ZGllbmNlIiwiZW1haWwiOiJmb29AZ29vZ2xlLmNvbSIsImV4cCI6NDY5ODI4NTY2MiwiaWF0IjoxNTQ0Njg1NjYyLCJpc3MiOiJodHRwczovL2V4YW1wbGUudG9rZW5fc2VydmljZS5jb20iLCJpc3Rpb19hdHRyaWJ1dGVzIjpbeyJzb3VyY2UuaXAiOiIxMjcuMC4wLjEifV0sImtleTEiOlsidmFsMiIsInZhbDMiXSwib3JpZ2luYWxfcGF5bG9hZCI6eyJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJpc3MiOiJodHRwczovL2FjY291bnRzLmV4YW1wbGUuY29tIiwic3ViIjoiZXhhbXBsZS1zdWJqZWN0In0sInN1YiI6Imh0dHBzOi8vYWNjb3VudHMuZXhhbXBsZS5jb20vMTIzNDU1Njc4OTAifQ.ZAVGs7atnllS0symhdD5bKV8qmaj1S5T9d1M-5rt-glfvYGpqLTRz-ru9js2os2RBnhnhwYsBt8wIPPnDwcj_iktESV4QlvX73RXj5rrG9O3HELet8GBqzxBiPOrA5pfSLvEQZanw-yf-NDw4vhwczReZe8L90NihAVrdwRPf5aZRZlzkpsCkQR-GdeJviDqkMhrRFVho5sq6QohUr3vt2DuItxCkKaFj_5024lb37iFuqEKkAa-3T-Di15-TywGZgTcz_723Uj7TL5Dj7ar3gdtGIXnK7KvTLiR3Ue2oOrgWkkOL62qF34wKFbJCEKOOIOFF8CC9jF88nVf2rgVGQ \ No newline at end of file +eyJhbGciOiJSUzI1NiIsImtpZCI6IkRIRmJwb0lVcXJZOHQyenBBMnFYZkNtcjVWTzVaRXI0UnpIVV8tZW52dlEiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJleGFtcGxlLWF1ZGllbmNlIiwiZW1haWwiOiJmb29AZ29vZ2xlLmNvbSIsImV4cCI6NDY5ODM2MTUwOCwiaWF0IjoxNTQ0NzYxNTA4LCJpc3MiOiJodHRwczovL2V4YW1wbGUudG9rZW5fc2VydmljZS5jb20iLCJpc3Rpb19hdHRyaWJ1dGVzIjpbeyJzb3VyY2UuaXAiOiIxMjcuMC4wLjEifV0sImtleTEiOlsidmFsMiIsInZhbDMiXSwib3JpZ2luYWxfY2xhaW1zIjp7ImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlzcyI6Imh0dHBzOi8vYWNjb3VudHMuZXhhbXBsZS5jb20iLCJzdWIiOiJleGFtcGxlLXN1YmplY3QifSwic3ViIjoiaHR0cHM6Ly9hY2NvdW50cy5leGFtcGxlLmNvbS8xMjM0NTU2Nzg5MCJ9.mLm9Gmcd748anwybiPxGPEuYgJBChqoHkVOvRhQN-H9jMqVKyF-7ynud1CJp5n72VeMB1FzvKAV0ErzSyWQc0iofQywG6whYXP6zL-Oc0igUrLDvzb6PuBDkbWOcZrvHkHM4tIYAkF4j880GqMWEP3gGrykziIEY9g4povquCFSdkLjjyol2-Ge_6MFdayYoeWLLOaMP7tHiPTm_ajioQ4jcz5whBWu3DZWx4IuU5UIBYlHG_miJZv5zmwwQ60T1_p_sW7zkABJgDhCvu6cHh6g-hZdQvZbATFwMfN8VDzttTjRG8wuLlkQ1TTOCx5PDv-_gHfQfRWt8Z94HrIJPuQ \ No newline at end of file diff --git a/src/envoy/http/jwt_auth/sample/APToken/aptoken-envoy.conf b/src/envoy/http/jwt_auth/sample/APToken/aptoken-envoy.conf index 508f9d8e4d5..d0f03755217 100644 --- a/src/envoy/http/jwt_auth/sample/APToken/aptoken-envoy.conf +++ b/src/envoy/http/jwt_auth/sample/APToken/aptoken-envoy.conf @@ -59,6 +59,7 @@ "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": "x-ingress-authorization"}], "forward_payload_header": "test-jwt-payload-output" } ] @@ -71,7 +72,8 @@ "origins":[ { "jwt":{ - "issuer":"https://accounts.example.com" + "issuer":"https://example.token_service.com", + "jwt_headers":["x-ingress-authorization"] } } ], diff --git a/src/envoy/http/jwt_auth/sample/APToken/guide.txt b/src/envoy/http/jwt_auth/sample/APToken/guide.txt new file mode 100644 index 00000000000..832e726b7fb --- /dev/null +++ b/src/envoy/http/jwt_auth/sample/APToken/guide.txt @@ -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 "x-ingress-authorization:$token" http://localhost:9090/echo -d "hello world" diff --git a/src/envoy/http/jwt_auth/sample/APToken/jwks.json b/src/envoy/http/jwt_auth/sample/APToken/jwks.json deleted file mode 100644 index b2c93b48a1d..00000000000 --- a/src/envoy/http/jwt_auth/sample/APToken/jwks.json +++ /dev/null @@ -1 +0,0 @@ -{ "keys":[ {"e":"AQAB","kid":"DHFbpoIUqrY8t2zpA2qXfCmr5VO5ZEr4RzHU_-envvQ","kty":"RSA","n":"xAE7eB6qugXyCAG3yhh7pkDkT65pHymX-P7KfIupjf59vsdo91bSP9C8H07pSAGQO1MV_xFj9VswgsCg4R6otmg5PV2He95lZdHtOcU5DXIg_pbhLdKXbi66GlVeK6ABZOUW3WYtnNHD-91gVuoeJT_DwtGGcp4ignkgXfkiEm4sw-4sfb4qdt5oLbyVpmW6x9cfa7vs2WTfURiCrBoUqgBo_-4WTiULmmHSGZHOjzwa8WtrtOQGsAFjIbno85jp6MnGGGZPYZbDAa_b3y5u-YpW7ypZrvD8BgtKVjgtQgZhLAGezMt0ua3DRrWnKqTZ0BJ_EyxOGuHJrLsn00fnMQ"}]} \ No newline at end of file diff --git a/src/envoy/http/jwt_auth/sample/APToken/key.pem b/src/envoy/http/jwt_auth/sample/APToken/key.pem deleted file mode 100644 index 68fa79265a8..00000000000 --- a/src/envoy/http/jwt_auth/sample/APToken/key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAxAE7eB6qugXyCAG3yhh7pkDkT65pHymX+P7KfIupjf59vsdo -91bSP9C8H07pSAGQO1MV/xFj9VswgsCg4R6otmg5PV2He95lZdHtOcU5DXIg/pbh -LdKXbi66GlVeK6ABZOUW3WYtnNHD+91gVuoeJT/DwtGGcp4ignkgXfkiEm4sw+4s -fb4qdt5oLbyVpmW6x9cfa7vs2WTfURiCrBoUqgBo/+4WTiULmmHSGZHOjzwa8Wtr -tOQGsAFjIbno85jp6MnGGGZPYZbDAa/b3y5u+YpW7ypZrvD8BgtKVjgtQgZhLAGe -zMt0ua3DRrWnKqTZ0BJ/EyxOGuHJrLsn00fnMQIDAQABAoIBAQCfv3ler2/qaYoX -6H6I4md02xK5tqbK1TWdpNwXoiSxLCgEY7YzULnPdSq+Qax0GYIaN9+Hof7cLFRf -XOxCTqCm+k8cqqwP0vTFhdFY8ltLkCdAGGyy0h7FmKPpboZv+9rnBqgaDntCgty+ -3HD2pZ2oMk407Fwt8qChwmMU9EZGycIRxwTFB8xGpss8gFc+fTAmvGFI/s8aclu+ -+XmvPkw0lz+dLaNsne0yBHVMPFtncqhbsFbnbB8OAMtWsVbTwU60OVeZ/oOYJVY9 -vtuMQ4IAi5kMx8AWQ1UDvW2ZnuGpQkr6dZ2YP/IMOXGaywbgcHPUitrW3iIYYW+U -NIVWXkA5AoGBAPd9MesQduCsngWNvtIjrNUEakbXkg0vbw+dRIbIqy2Bx/tyWhim -qFNYcNjZELqp5C3f3hlO/k9K8xxJLIYloYg1948DbpOhVk8SdFGgpCYC4q/fBPw+ -kAFQEKhZelwKj2Srf58BCrhIe1KPHbQfTF/JZ7JBndIw79/1p9rU6wWLAoGBAMq+ -yd+7L8wpCZNtmjDUTWmGq3LmFLBzBMhwmV2NOKbTFakMVyEtSORQFOJbxFwuWx5z -j3nLq9ivQgNKLPLSTF/Dtej2lzgNd9YG/9XW+6HeZ841XNUHE0XsU6Mm1EAyadfu -AAnoxTh/NqYvF81qCM9tl9Bsjx8klOyXrrGKj/WzAoGAKJC2u+bI9W6VwCdJnbwH -OistGEuBPvQFajPG5ajClgTtuIM3zU6TzIV0ibaajV4HbpWBG/jcqjaIvpwn1h0Y -6vCdkS1o1H3fXbqSokaIYUqbyWPut0Gx7OUotc9kxO1eL4wEsRVEoowO2qtmnP18 -UT775jXnHmqzBqyHRNEdbJ0CgYAdgPAdp77H5fznwF5c1rhBMADJIqRGHSbICGK5 -E3D4DeWsCQiw4kcmOmUfn50OkQxffQ+W+MWULcTcd7Hc0C+fC/rv4NqWpJcYxUH7 -m2JY5uWSQ3+z3Gi4lzCAoIjooq12Z8MHriDtHM4WFupO0SxhCyC5iuK09HzbhSM9 -4N0cMwKBgGfYJRmX5pW7rKo/O1Zl6oG6VBA6fYXIe28X9sZ0YCbVIuX8MgRk2Vpm -y47qmzEQ/1kwWtQzZOcePM8rd8qTTFPCkM45S1ieh7qjNSMVIbl8I635UqCCoetW -VDwvUhVLO4mBT0p2F8g6Wngmpy9ZBh2B52w3Rz9UxHffwdC7hhQD ------END RSA PRIVATE KEY----- diff --git a/src/envoy/http/jwt_auth/sample/APToken/pubkey.pem b/src/envoy/http/jwt_auth/sample/APToken/pubkey.pem deleted file mode 100644 index db058e7ad39..00000000000 --- a/src/envoy/http/jwt_auth/sample/APToken/pubkey.pem +++ /dev/null @@ -1,9 +0,0 @@ ------BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxAE7eB6qugXyCAG3yhh7 -pkDkT65pHymX+P7KfIupjf59vsdo91bSP9C8H07pSAGQO1MV/xFj9VswgsCg4R6o -tmg5PV2He95lZdHtOcU5DXIg/pbhLdKXbi66GlVeK6ABZOUW3WYtnNHD+91gVuoe -JT/DwtGGcp4ignkgXfkiEm4sw+4sfb4qdt5oLbyVpmW6x9cfa7vs2WTfURiCrBoU -qgBo/+4WTiULmmHSGZHOjzwa8WtrtOQGsAFjIbno85jp6MnGGGZPYZbDAa/b3y5u -+YpW7ypZrvD8BgtKVjgtQgZhLAGezMt0ua3DRrWnKqTZ0BJ/EyxOGuHJrLsn00fn -MQIDAQAB ------END PUBLIC KEY----- From 586c9ee30ecf838880f5f9b4f00f1d2e765524b0 Mon Sep 17 00:00:00 2001 From: Lei Tang <32078630+lei-tang@users.noreply.github.com> Date: Sat, 15 Dec 2018 16:32:10 +0800 Subject: [PATCH 04/12] Address review comments and add a test --- src/envoy/http/authn/authenticator_base.cc | 24 ++++++---- .../http/authn/authenticator_base_test.cc | 44 +++++++++++++++++++ .../sample/APToken/APToken-example1.jwt | 0 .../sample/APToken/aptoken-envoy.conf | 4 +- .../sample/APToken/guide.txt | 2 +- 5 files changed, 62 insertions(+), 12 deletions(-) rename src/envoy/http/{jwt_auth => authn}/sample/APToken/APToken-example1.jwt (100%) rename src/envoy/http/{jwt_auth => authn}/sample/APToken/aptoken-envoy.conf (95%) rename src/envoy/http/{jwt_auth => authn}/sample/APToken/guide.txt (89%) diff --git a/src/envoy/http/authn/authenticator_base.cc b/src/envoy/http/authn/authenticator_base.cc index 37b8ecdaaed..d819942c935 100644 --- a/src/envoy/http/authn/authenticator_base.cc +++ b/src/envoy/http/authn/authenticator_base.cc @@ -31,7 +31,20 @@ namespace AuthN { namespace { // The default header name for an exchanged token -static const std::string kExchangedTokenHeaderName = "x-ingress-authorization"; +static const std::string kExchangedTokenHeaderName = "ingress-authorization"; + +// Return whether the header for an exchanged token is found +bool FindHeaderOfExchangedToken(const iaapi::Jwt& jwt) { + bool found = false; + for (auto h : jwt.jwt_headers()) { + if (kExchangedTokenHeaderName == h) { + found = true; + break; + } + } + return found; +} + } // namespace AuthenticatorBase::AuthenticatorBase(FilterContext* filter_context) @@ -75,14 +88,7 @@ bool AuthenticatorBase::validateJwt(const iaapi::Jwt& jwt, Payload* payload) { if (filter_context()->getJwtPayload(jwt.issuer(), &jwt_payload)) { std::string payload_to_process = jwt_payload; std::string original_payload; - bool found = false; - for (auto h : jwt.jwt_headers()) { - if (kExchangedTokenHeaderName == h) { - found = true; - break; - } - } - if (found && + if (FindHeaderOfExchangedToken(jwt) && AuthnUtils::ExtractOriginalPayload(jwt_payload, &original_payload)) { payload_to_process = original_payload; } diff --git a/src/envoy/http/authn/authenticator_base_test.cc b/src/envoy/http/authn/authenticator_base_test.cc index 793fac11720..467e67e3ddf 100644 --- a/src/envoy/http/authn/authenticator_base_test.cc +++ b/src/envoy/http/authn/authenticator_base_test.cc @@ -50,6 +50,22 @@ 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" + } + } + )"; + class MockAuthenticatorBase : public AuthenticatorBase { public: MockAuthenticatorBase(FilterContext* filter_context) @@ -293,6 +309,34 @@ 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\",\"sub\":\"example-subject\",\"iss\":\"https://accounts.example.com\"}" + } + } + )", + &expected_payload, google::protobuf::util::JsonParseOptions{}); + + EXPECT_TRUE(authenticator_.validateJwt(jwt_, payload_)); + EXPECT_TRUE(MessageDifferencer::Equals(expected_payload, *payload_)); +} + } // namespace } // namespace AuthN } // namespace Istio diff --git a/src/envoy/http/jwt_auth/sample/APToken/APToken-example1.jwt b/src/envoy/http/authn/sample/APToken/APToken-example1.jwt similarity index 100% rename from src/envoy/http/jwt_auth/sample/APToken/APToken-example1.jwt rename to src/envoy/http/authn/sample/APToken/APToken-example1.jwt diff --git a/src/envoy/http/jwt_auth/sample/APToken/aptoken-envoy.conf b/src/envoy/http/authn/sample/APToken/aptoken-envoy.conf similarity index 95% rename from src/envoy/http/jwt_auth/sample/APToken/aptoken-envoy.conf rename to src/envoy/http/authn/sample/APToken/aptoken-envoy.conf index d0f03755217..a5905812f7c 100644 --- a/src/envoy/http/jwt_auth/sample/APToken/aptoken-envoy.conf +++ b/src/envoy/http/authn/sample/APToken/aptoken-envoy.conf @@ -59,7 +59,7 @@ "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": "x-ingress-authorization"}], + "from_headers": [{"name": "ingress-authorization"}], "forward_payload_header": "test-jwt-payload-output" } ] @@ -73,7 +73,7 @@ { "jwt":{ "issuer":"https://example.token_service.com", - "jwt_headers":["x-ingress-authorization"] + "jwt_headers":["ingress-authorization"] } } ], diff --git a/src/envoy/http/jwt_auth/sample/APToken/guide.txt b/src/envoy/http/authn/sample/APToken/guide.txt similarity index 89% rename from src/envoy/http/jwt_auth/sample/APToken/guide.txt rename to src/envoy/http/authn/sample/APToken/guide.txt index 832e726b7fb..7fdf21c9d6f 100644 --- a/src/envoy/http/jwt_auth/sample/APToken/guide.txt +++ b/src/envoy/http/authn/sample/APToken/guide.txt @@ -15,4 +15,4 @@ an example exchanged token. 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 "x-ingress-authorization:$token" http://localhost:9090/echo -d "hello world" + curl --header "ingress-authorization:$token" http://localhost:9090/echo -d "hello world" From c7931e3c1b65a65ec1f007cd439cdd53aa8a86d1 Mon Sep 17 00:00:00 2001 From: Lei Tang <32078630+lei-tang@users.noreply.github.com> Date: Sun, 16 Dec 2018 22:11:15 +0800 Subject: [PATCH 05/12] Address new review comments --- src/envoy/http/authn/authenticator_base.cc | 3 + .../http/authn/authenticator_base_test.cc | 70 +++++++++++++++++++ src/envoy/http/authn/authn_utils.h | 4 +- 3 files changed, 75 insertions(+), 2 deletions(-) diff --git a/src/envoy/http/authn/authenticator_base.cc b/src/envoy/http/authn/authenticator_base.cc index d819942c935..87169e60ac9 100644 --- a/src/envoy/http/authn/authenticator_base.cc +++ b/src/envoy/http/authn/authenticator_base.cc @@ -90,6 +90,9 @@ bool AuthenticatorBase::validateJwt(const iaapi::Jwt& jwt, Payload* payload) { std::string original_payload; if (FindHeaderOfExchangedToken(jwt) && 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; } return AuthnUtils::ProcessJwtPayload(payload_to_process, diff --git a/src/envoy/http/authn/authenticator_base_test.cc b/src/envoy/http/authn/authenticator_base_test.cc index 467e67e3ddf..e2d71d188f0 100644 --- a/src/envoy/http/authn/authenticator_base_test.cc +++ b/src/envoy/http/authn/authenticator_base_test.cc @@ -66,6 +66,15 @@ const std::string kExchangedTokenPayload = } )"; +const std::string kExchangedTokenPayloadNoOriginalClaims = + R"( + { + "iss": "token-service", + "sub": "subject", + "aud": ["aud1", "aud2"] + } + )"; + class MockAuthenticatorBase : public AuthenticatorBase { public: MockAuthenticatorBase(FilterContext* filter_context) @@ -337,6 +346,67 @@ TEST_F(ValidateJwtTest, OriginalPayloadOfExchangedToken) { EXPECT_TRUE(MessageDifferencer::Equals(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)); + + 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 }\n " + } + } + )", + &expected_payload, google::protobuf::util::JsonParseOptions{}); + + // When no original_claims in an exchanged token, 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_)); +} + +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 diff --git a/src/envoy/http/authn/authn_utils.h b/src/envoy/http/authn/authn_utils.h index 010c365c645..07e7de1cacf 100644 --- a/src/envoy/http/authn/authn_utils.h +++ b/src/envoy/http/authn/authn_utils.h @@ -38,8 +38,8 @@ class AuthnUtils : public Logger::Loggable { static bool ProcessJwtPayload(const std::string& jwt_payload_str, istio::authn::JwtPayload* payload); - // Parse the original_payload in an APToken. - // Return true if original_payload can be + // Parses the original_payload in an APToken. + // Returns true if original_payload can be // parsed successfully. Otherwise, return false. static bool ExtractOriginalPayload(const std::string& token, std::string* original_payload); From 8315ce42474903f52dc7fdedfa3a658ab460a7d8 Mon Sep 17 00:00:00 2001 From: Lei Tang <32078630+lei-tang@users.noreply.github.com> Date: Tue, 18 Dec 2018 16:08:20 +0800 Subject: [PATCH 06/12] Add integration tests and address review comments --- src/envoy/http/authn/authenticator_base.cc | 20 +- .../http/authn/authenticator_base_test.cc | 24 +- src/envoy/http/authn/authn_utils.cc | 8 - test/integration/BUILD | 18 +- .../exchanged_token_integration_test.cc | 418 ++++++++++++++++++ 5 files changed, 452 insertions(+), 36 deletions(-) create mode 100644 test/integration/exchanged_token_integration_test.cc diff --git a/src/envoy/http/authn/authenticator_base.cc b/src/envoy/http/authn/authenticator_base.cc index 87169e60ac9..6bca88db253 100644 --- a/src/envoy/http/authn/authenticator_base.cc +++ b/src/envoy/http/authn/authenticator_base.cc @@ -88,12 +88,20 @@ bool AuthenticatorBase::validateJwt(const iaapi::Jwt& jwt, Payload* payload) { if (filter_context()->getJwtPayload(jwt.issuer(), &jwt_payload)) { std::string payload_to_process = jwt_payload; std::string original_payload; - if (FindHeaderOfExchangedToken(jwt) && - 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; + 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, "The token is invalid. The payload is {}", + jwt_payload); + return false; + } } return AuthnUtils::ProcessJwtPayload(payload_to_process, payload->mutable_jwt()); diff --git a/src/envoy/http/authn/authenticator_base_test.cc b/src/envoy/http/authn/authenticator_base_test.cc index e2d71d188f0..b469392bc0f 100644 --- a/src/envoy/http/authn/authenticator_base_test.cc +++ b/src/envoy/http/authn/authenticator_base_test.cc @@ -336,7 +336,7 @@ TEST_F(ValidateJwtTest, OriginalPayloadOfExchangedToken) { "sub": ["example-subject"], "email": ["user@example.com"] }, - "raw_claims": "{\"email\":\"user@example.com\",\"sub\":\"example-subject\",\"iss\":\"https://accounts.example.com\"}" + "raw_claims": "{\"email\":\"user@example.com\",\"iss\":\"https://accounts.example.com\",\"sub\":\"example-subject\"}" } } )", @@ -354,27 +354,9 @@ TEST_F(ValidateJwtTest, OriginalPayloadOfExchangedTokenMissing) { .MergeFrom(MessageUtil::keyValueStruct( "token-service", kExchangedTokenPayloadNoOriginalClaims)); - 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 }\n " - } - } - )", - &expected_payload, google::protobuf::util::JsonParseOptions{}); - // When no original_claims in an exchanged token, 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_)); + // is treated as invalid. + EXPECT_FALSE(authenticator_.validateJwt(jwt_, payload_)); } TEST_F(ValidateJwtTest, OriginalPayloadOfExchangedTokenNotInIntendedHeader) { diff --git a/src/envoy/http/authn/authn_utils.cc b/src/envoy/http/authn/authn_utils.cc index 6c43adb2936..a358aa9b682 100644 --- a/src/envoy/http/authn/authn_utils.cc +++ b/src/envoy/http/authn/authn_utils.cc @@ -120,14 +120,6 @@ bool AuthnUtils::ExtractOriginalPayload(const std::string& token, Envoy::Json::ObjectSharedPtr original_payload_obj; try { auto original_payload_obj = json_obj->getObject(kAPTokenOriginalPayload); - std::string iss1 = json_obj->getString(kJwtIssuerKey, ""); - std::string iss2 = original_payload_obj->getString(kJwtIssuerKey, ""); - // Token exchange makes the issuers of the APToken and the original JWT - // to be different. - if (!(!iss1.empty() && !iss2.empty() && iss1 != iss2)) { - return false; - } - *original_payload = original_payload_obj->asJsonString(); ENVOY_LOG(debug, "{}: the original payload in APToken is {}", __FUNCTION__, *original_payload); diff --git a/test/integration/BUILD b/test/integration/BUILD index 6d4d98562b2..5ea6b708487 100644 --- a/test/integration/BUILD +++ b/test/integration/BUILD @@ -35,4 +35,20 @@ envoy_cc_test( "//src/envoy/utils:filter_names_lib", "//src/envoy/http/mixer:filter_lib", ], -) \ No newline at end of file +) + +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", + ], +) diff --git a/test/integration/exchanged_token_integration_test.cc b/test/integration/exchanged_token_integration_test.cc new file mode 100644 index 00000000000..5827b072759 --- /dev/null +++ b/test/integration/exchanged_token_integration_test.cc @@ -0,0 +1,418 @@ +/* Copyright 2018 Istio Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// The integration tests in this file test the end-to-end behaviour of +// an exchanged token when going through the HTTP filter chains +// (jwt-authn + istio-authn + istio-mixer). Filters pass on processing +// results next filters using the request info through dynamic metadata +// and the results generated by the filters can be observed at the mixer +// backend). + +#include "fmt/printf.h" +#include "gmock/gmock.h" +#include "include/istio/utils/attribute_names.h" +#include "mixer/v1/mixer.pb.h" +#include "src/envoy/utils/filter_names.h" +#include "test/integration/http_protocol_integration.h" + +using ::google::protobuf::util::error::Code; +using ::testing::Contains; +using ::testing::Not; + +namespace Envoy { +namespace { + +// An example exchanged token +constexpr char kExchangedToken[] = + "eyJhbGciOiJSUzI1NiIsImtpZCI6IkRIRmJwb0lVcXJZOHQyenBBMnFYZkNtcjVWTzVaRXI0Un" + "pIVV8tZW52dlEiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJleGFtcGxlLWF1ZGllbmNlIiwiZW1ha" + "WwiOiJmb29AZ29vZ2xlLmNvbSIsImV4cCI6NDY5ODM2MTUwOCwiaWF0IjoxNTQ0NzYxNTA4LCJ" + "pc3MiOiJodHRwczovL2V4YW1wbGUudG9rZW5fc2VydmljZS5jb20iLCJpc3Rpb19hdHRyaWJ1d" + "GVzIjpbeyJzb3VyY2UuaXAiOiIxMjcuMC4wLjEifV0sImtleTEiOlsidmFsMiIsInZhbDMiXSw" + "ib3JpZ2luYWxfY2xhaW1zIjp7ImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlzcyI6Imh0d" + "HBzOi8vYWNjb3VudHMuZXhhbXBsZS5jb20iLCJzdWIiOiJleGFtcGxlLXN1YmplY3QifSwic3V" + "iIjoiaHR0cHM6Ly9hY2NvdW50cy5leGFtcGxlLmNvbS8xMjM0NTU2Nzg5MCJ9.mLm9Gmcd748a" + "nwybiPxGPEuYgJBChqoHkVOvRhQN-H9jMqVKyF-7ynud1CJp5n72VeMB1FzvKAV0ErzSyWQc0i" + "ofQywG6whYXP6zL-Oc0igUrLDvzb6PuBDkbWOcZrvHkHM4tIYAkF4j880GqMWEP3gGrykziIEY" + "9g4povquCFSdkLjjyol2-Ge_6MFdayYoeWLLOaMP7tHiPTm_ajioQ4jcz5whBWu3DZWx4IuU5U" + "IBYlHG_miJZv5zmwwQ60T1_p_sW7zkABJgDhCvu6cHh6g-hZdQvZbATFwMfN8VDzttTjRG8wuL" + "lkQ1TTOCx5PDv-_gHfQfRWt8Z94HrIJPuQ"; + +// An example token without original_claims +constexpr char kTokenWithoutOriginalClaims[] = + "eyJhbGciOiJSUzI1NiIsImtpZCI6IkRIRmJwb0lVcXJZOHQyenBBMnFYZkNtcjVWTzVaRXI0Un" + "pIVV8tZW52dlEiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJleGFtcGxlLWF1ZGllbmNlIiwiZW1ha" + "WwiOiJmb29AZ29vZ2xlLmNvbSIsImV4cCI6NDY5ODcyNzc2NiwiaWF0IjoxNTQ1MTI3NzY2LCJ" + "pc3MiOiJodHRwczovL2V4YW1wbGUudG9rZW5fc2VydmljZS5jb20iLCJpc3Rpb19hdHRyaWJ1d" + "GVzIjpbeyJzb3VyY2UuaXAiOiIxMjcuMC4wLjEifV0sImtleTEiOlsidmFsMiIsInZhbDMiXSw" + "ic3ViIjoiaHR0cHM6Ly9hY2NvdW50cy5leGFtcGxlLmNvbS8xMjM0NTU2Nzg5MCJ9.FVskjGxS" + "cTuNFtKGRnQvQgejgcdPbunCAbXlj_ZYMawrHIYnrMt_Ddw5nOojxQu2zfkwoB004196ozNjDR" + "ED4jpJA0T6HP7hyTHGbrp6h6Z4dQ_PcmAxdR2_g8GEo-bcJ-CcbATEyBtrDqLtFcgP-ev_ctAo" + "BQHGp7qMgdpkQIJ07BTT1n6mghPFFCnA__RYWjPUwMLGZs_bOtWxHYbd-bkDSwg4Kbtf5-9oPI" + "nwJc6oMGMVzdjmJYMadg5GEor5XhgYz3TThPzLlEsxa0loD9eJDBGgdwjA1cLuAGgM7_HgRfg7" + "8ameSmQgSCsNlFB4k3ODeC-YC62KYdZ5Jdrg2A"; + +constexpr char kExpectedPrincipal[] = + "https://accounts.example.com/example-subject"; +constexpr char kDestinationNamespace[] = "pod"; +constexpr char kDestinationUID[] = "kubernetes://dest.pod"; +constexpr char kSourceUID[] = "kubernetes://src.pod"; +constexpr char kTelemetryBackend[] = "telemetry-backend"; +constexpr char kPolicyBackend[] = "policy-backend"; +const std::string kHeaderForExchangedToken = "ingress-authorization"; + +// Generates basic test request header. +Http::TestHeaderMapImpl BaseRequestHeaders() { + return Http::TestHeaderMapImpl{{":method", "GET"}, + {":path", "/"}, + {":scheme", "http"}, + {":authority", "host"}, + {"x-forwarded-for", "10.0.0.1"}}; +} + +// Generates test request header with given token. +Http::TestHeaderMapImpl HeadersWithToken(const std::string& header, + const std::string& token) { + auto headers = BaseRequestHeaders(); + headers.addCopy(header, token); + return headers; +} + +std::string MakeJwtFilterConfig() { + constexpr char kJwtFilterTemplate[] = R"( + name: %s + config: + rules: + - issuer: "https://example.token_service.com" + from_headers: + - name: ingress-authorization + local_jwks: + inline_string: "%s" + - issuer: "testing-rbac@secure.istio.io" + local_jwks: + inline_string: "%s" + allow_missing_or_failed: true + )"; + // From + // https://github.com/istio/istio/blob/master/security/tools/jwt/samples/jwks.json + constexpr char kJwksInline[] = + "{ \"keys\":[ " + "{\"e\":\"AQAB\",\"kid\":\"DHFbpoIUqrY8t2zpA2qXfCmr5VO5ZEr4RzHU_-envvQ\"," + "\"kty\":\"RSA\",\"n\":\"xAE7eB6qugXyCAG3yhh7pkDkT65pHymX-" + "P7KfIupjf59vsdo91bSP9C8H07pSAGQO1MV" + "_xFj9VswgsCg4R6otmg5PV2He95lZdHtOcU5DXIg_" + "pbhLdKXbi66GlVeK6ABZOUW3WYtnNHD-91gVuoeJT_" + "DwtGGcp4ignkgXfkiEm4sw-4sfb4qdt5oLbyVpmW6x9cfa7vs2WTfURiCrBoUqgBo_-" + "4WTiULmmHSGZHOjzwa8WtrtOQGsAFjIbno85jp6MnGGGZPYZbDAa_b3y5u-" + "YpW7ypZrvD8BgtKVjgtQgZhLAGezMt0ua3DRrWnKqTZ0BJ_EyxOGuHJrLsn00fnMQ\"}]}"; + + return fmt::sprintf(kJwtFilterTemplate, Utils::IstioFilterName::kJwt, + StringUtil::escape(kJwksInline), + StringUtil::escape(kJwksInline)); +} + +std::string MakeAuthFilterConfig() { + constexpr char kAuthnFilterWithJwtTemplate[] = R"( + name: %s + config: + policy: + origins: + - jwt: + issuer: https://example.token_service.com + jwt_headers: + - ingress-authorization + principalBinding: USE_ORIGIN +)"; + return fmt::sprintf(kAuthnFilterWithJwtTemplate, + Utils::IstioFilterName::kAuthentication); +} + +std::string MakeRbacFilterConfig() { + constexpr char kRbacFilterTemplate[] = R"( + name: envoy.filters.http.rbac + config: + rules: + policies: + "foo": + permissions: + - any: true + principals: + - metadata: + filter: %s + path: + - key: %s + value: + string_match: + exact: %s +)"; + return fmt::sprintf( + kRbacFilterTemplate, Utils::IstioFilterName::kAuthentication, + istio::utils::AttributeName::kRequestAuthPrincipal, kExpectedPrincipal); +} + +std::string MakeMixerFilterConfig() { + constexpr char kMixerFilterTemplate[] = R"( + name: mixer + config: + defaultDestinationService: "default" + mixerAttributes: + attributes: { + } + serviceConfigs: { + "default": {} + } + transport: + attributes_for_mixer_proxy: + attributes: { + "source.uid": { + string_value: %s + } + } + report_cluster: %s + check_cluster: %s + )"; + return fmt::sprintf(kMixerFilterTemplate, kSourceUID, kTelemetryBackend, + kPolicyBackend); +} + +class ExchangedTokenIntegrationTest : public HttpProtocolIntegrationTest { + public: + void createUpstreams() override { + HttpProtocolIntegrationTest::createUpstreams(); + fake_upstreams_.emplace_back(new FakeUpstream( + 0, FakeHttpConnection::Type::HTTP2, version_, timeSystem())); + telemetry_upstream_ = fake_upstreams_.back().get(); + + fake_upstreams_.emplace_back(new FakeUpstream( + 0, FakeHttpConnection::Type::HTTP2, version_, timeSystem())); + policy_upstream_ = fake_upstreams_.back().get(); + } + + void SetUp() override { + config_helper_.addConfigModifier(addNodeMetadata()); + + config_helper_.addFilter(MakeMixerFilterConfig()); + config_helper_.addFilter(MakeRbacFilterConfig()); + config_helper_.addFilter(MakeAuthFilterConfig()); + config_helper_.addFilter(MakeJwtFilterConfig()); + + config_helper_.addConfigModifier(addCluster(kTelemetryBackend)); + config_helper_.addConfigModifier(addCluster(kPolicyBackend)); + + HttpProtocolIntegrationTest::initialize(); + } + + void TearDown() override { + cleanupConnection(fake_upstream_connection_); + cleanupConnection(telemetry_connection_); + cleanupConnection(policy_connection_); + } + + ConfigHelper::ConfigModifierFunction addNodeMetadata() { + return [](envoy::config::bootstrap::v2::Bootstrap& bootstrap) { + ::google::protobuf::Struct meta; + MessageUtil::loadFromJson( + fmt::sprintf(R"({ + "ISTIO_VERSION": "1.0.1", + "NODE_UID": "%s", + "NODE_NAMESPACE": "%s" + })", + kDestinationUID, kDestinationNamespace), + meta); + bootstrap.mutable_node()->mutable_metadata()->MergeFrom(meta); + }; + } + + ConfigHelper::ConfigModifierFunction addCluster(const std::string& name) { + return [name](envoy::config::bootstrap::v2::Bootstrap& bootstrap) { + auto* cluster = bootstrap.mutable_static_resources()->add_clusters(); + cluster->MergeFrom(bootstrap.static_resources().clusters()[0]); + cluster->mutable_http2_protocol_options(); + cluster->set_name(name); + }; + } + + void waitForTelemetryRequest(::istio::mixer::v1::ReportRequest* request) { + AssertionResult result = telemetry_upstream_->waitForHttpConnection( + *dispatcher_, telemetry_connection_); + RELEASE_ASSERT(result, result.message()); + result = telemetry_connection_->waitForNewStream(*dispatcher_, + telemetry_request_); + RELEASE_ASSERT(result, result.message()); + + result = telemetry_request_->waitForGrpcMessage(*dispatcher_, *request); + RELEASE_ASSERT(result, result.message()); + } + + // Must be called after waitForTelemetryRequest + void sendTelemetryResponse() { + telemetry_request_->startGrpcStream(); + telemetry_request_->sendGrpcMessage(::istio::mixer::v1::ReportResponse{}); + telemetry_request_->finishGrpcStream(Grpc::Status::Ok); + } + + void waitForPolicyRequest(::istio::mixer::v1::CheckRequest* request) { + AssertionResult result = policy_upstream_->waitForHttpConnection( + *dispatcher_, policy_connection_); + RELEASE_ASSERT(result, result.message()); + result = + policy_connection_->waitForNewStream(*dispatcher_, policy_request_); + RELEASE_ASSERT(result, result.message()); + + result = policy_request_->waitForGrpcMessage(*dispatcher_, *request); + RELEASE_ASSERT(result, result.message()); + } + + // Must be called after waitForPolicyRequest + void sendPolicyResponse() { + policy_request_->startGrpcStream(); + ::istio::mixer::v1::CheckResponse response; + response.mutable_precondition()->mutable_status()->set_code(Code::OK); + policy_request_->sendGrpcMessage(response); + policy_request_->finishGrpcStream(Grpc::Status::Ok); + } + + void cleanupConnection(FakeHttpConnectionPtr& connection) { + if (connection != nullptr) { + AssertionResult result = connection->close(); + RELEASE_ASSERT(result, result.message()); + result = connection->waitForDisconnect(); + RELEASE_ASSERT(result, result.message()); + } + } + + FakeUpstream* telemetry_upstream_{}; + FakeHttpConnectionPtr telemetry_connection_{}; + FakeStreamPtr telemetry_request_{}; + + FakeUpstream* policy_upstream_{}; + FakeHttpConnectionPtr policy_connection_{}; + FakeStreamPtr policy_request_{}; +}; + +INSTANTIATE_TEST_CASE_P( + Protocols, ExchangedTokenIntegrationTest, + testing::ValuesIn(HttpProtocolIntegrationTest::getProtocolTestParams()), + HttpProtocolIntegrationTest::protocolTestParamsToString); + +TEST_P(ExchangedTokenIntegrationTest, ValidExchangeToken) { + codec_client_ = + makeHttpConnection(makeClientConnection((lookupPort("http")))); + + // A valid exchanged token in the header for an exchanged token + auto response = codec_client_->makeHeaderOnlyRequest( + HeadersWithToken(kHeaderForExchangedToken, kExchangedToken)); + + ::istio::mixer::v1::CheckRequest check_request; + waitForPolicyRequest(&check_request); + // Check request should see the authn attributes in the original payload. + EXPECT_THAT( + check_request.attributes().words(), + ::testing::AllOf(Contains(kDestinationUID), Contains("10.0.0.1"), + Contains(kExpectedPrincipal), Contains("sub"), + Contains("example-subject"), Contains("iss"), + Contains("https://accounts.example.com"), + Contains("email"), Contains("user@example.com"))); + sendPolicyResponse(); + + waitForNextUpstreamRequest(0); + // Send backend response. + upstream_request_->encodeHeaders(Http::TestHeaderMapImpl{{":status", "200"}}, + true); + response->waitForEndStream(); + + // Report is sent after the backend responds. + ::istio::mixer::v1::ReportRequest report_request; + waitForTelemetryRequest(&report_request); + // Report request should also see the same authn attributes. + EXPECT_THAT( + report_request.default_words(), + ::testing::AllOf(Contains(kDestinationUID), Contains("10.0.0.1"), + Contains(kExpectedPrincipal), Contains("sub"), + Contains("example-subject"), Contains("iss"), + Contains("https://accounts.example.com"), + Contains("email"), Contains("user@example.com"))); + + sendTelemetryResponse(); + + EXPECT_TRUE(response->complete()); + EXPECT_STREQ("200", response->headers().Status()->value().c_str()); +} + +TEST_P(ExchangedTokenIntegrationTest, ValidExchangeTokenAtWrongHeader) { + codec_client_ = + makeHttpConnection(makeClientConnection((lookupPort("http")))); + + // When a token is not in the header for an exchanged token, + // it will not be regarded as an exchanged token. + auto response = codec_client_->makeHeaderOnlyRequest( + HeadersWithToken("wrong-header", kExchangedToken)); + + ::istio::mixer::v1::ReportRequest report_request; + waitForTelemetryRequest(&report_request); + EXPECT_THAT(report_request.default_words(), + ::testing::AllOf(Contains(kDestinationUID), Contains("10.0.0.1"), + Not(Contains(kExpectedPrincipal)))); + sendTelemetryResponse(); + + response->waitForEndStream(); + EXPECT_TRUE(response->complete()); + EXPECT_STREQ("401", response->headers().Status()->value().c_str()); +} + +TEST_P(ExchangedTokenIntegrationTest, TokenWithoutOriginalClaims) { + codec_client_ = + makeHttpConnection(makeClientConnection((lookupPort("http")))); + + // When a token does not contain original_claims, + // it will be regarded as an invalid exchanged token. + auto response = codec_client_->makeHeaderOnlyRequest( + HeadersWithToken(kHeaderForExchangedToken, kTokenWithoutOriginalClaims)); + + ::istio::mixer::v1::ReportRequest report_request; + waitForTelemetryRequest(&report_request); + EXPECT_THAT(report_request.default_words(), + ::testing::AllOf(Contains(kDestinationUID), Contains("10.0.0.1"), + Not(Contains(kExpectedPrincipal)))); + sendTelemetryResponse(); + + response->waitForEndStream(); + EXPECT_TRUE(response->complete()); + EXPECT_STREQ("401", response->headers().Status()->value().c_str()); +} + +TEST_P(ExchangedTokenIntegrationTest, InvalidExchangeToken) { + codec_client_ = + makeHttpConnection(makeClientConnection((lookupPort("http")))); + + // When an invalid exchanged token is in the header for an exchanged token, + // the request will be rejected. + auto response = codec_client_->makeHeaderOnlyRequest( + HeadersWithToken(kHeaderForExchangedToken, "invalid-token")); + + ::istio::mixer::v1::ReportRequest report_request; + waitForTelemetryRequest(&report_request); + EXPECT_THAT(report_request.default_words(), + ::testing::AllOf(Contains(kDestinationUID), Contains("10.0.0.1"), + Not(Contains(kExpectedPrincipal)))); + sendTelemetryResponse(); + + response->waitForEndStream(); + EXPECT_TRUE(response->complete()); + EXPECT_STREQ("401", response->headers().Status()->value().c_str()); +} + +} // namespace +} // namespace Envoy From ef1629f2ca237defeb18be756c55c4c41d134fbb Mon Sep 17 00:00:00 2001 From: Lei Tang <32078630+lei-tang@users.noreply.github.com> Date: Wed, 19 Dec 2018 21:38:54 +0800 Subject: [PATCH 07/12] Fix a flaky test and address new review comments --- src/envoy/http/authn/authenticator_base.cc | 16 ++++++++-------- src/envoy/http/authn/authenticator_base_test.cc | 11 ++++++++++- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/envoy/http/authn/authenticator_base.cc b/src/envoy/http/authn/authenticator_base.cc index 6bca88db253..689890a37cc 100644 --- a/src/envoy/http/authn/authenticator_base.cc +++ b/src/envoy/http/authn/authenticator_base.cc @@ -33,14 +33,12 @@ namespace { // The default header name for an exchanged token static const std::string kExchangedTokenHeaderName = "ingress-authorization"; -// Return whether the header for an exchanged token is found +// Returns whether the header for an exchanged token is found bool FindHeaderOfExchangedToken(const iaapi::Jwt& jwt) { bool found = false; - for (auto h : jwt.jwt_headers()) { - if (kExchangedTokenHeaderName == h) { - found = true; - break; - } + if (jwt.jwt_headers_size() == 1 && + kExchangedTokenHeaderName == jwt.jwt_headers(0)) { + found = true; } return found; } @@ -98,8 +96,10 @@ bool AuthenticatorBase::validateJwt(const iaapi::Jwt& jwt, Payload* payload) { // 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, "The token is invalid. The payload is {}", - jwt_payload); + ENVOY_LOG( + error, + "Expect exchanged-token with original payload claim. Received: {}", + jwt_payload); return false; } } diff --git a/src/envoy/http/authn/authenticator_base_test.cc b/src/envoy/http/authn/authenticator_base_test.cc index b469392bc0f..1a476f065e7 100644 --- a/src/envoy/http/authn/authenticator_base_test.cc +++ b/src/envoy/http/authn/authenticator_base_test.cc @@ -343,7 +343,16 @@ TEST_F(ValidateJwtTest, OriginalPayloadOfExchangedToken) { &expected_payload, google::protobuf::util::JsonParseOptions{}); EXPECT_TRUE(authenticator_.validateJwt(jwt_, payload_)); - EXPECT_TRUE(MessageDifferencer::Equals(expected_payload, *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) { From 7dfb3994e3fef9428a48de259860110410cfeebe Mon Sep 17 00:00:00 2001 From: Lei Tang <32078630+lei-tang@users.noreply.github.com> Date: Fri, 21 Dec 2018 10:17:44 +0800 Subject: [PATCH 08/12] Small grammar fixes --- src/envoy/http/authn/authn_utils.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/envoy/http/authn/authn_utils.h b/src/envoy/http/authn/authn_utils.h index 07e7de1cacf..cf2a0e69d5b 100644 --- a/src/envoy/http/authn/authn_utils.h +++ b/src/envoy/http/authn/authn_utils.h @@ -38,9 +38,9 @@ class AuthnUtils : public Logger::Loggable { static bool ProcessJwtPayload(const std::string& jwt_payload_str, istio::authn::JwtPayload* payload); - // Parses the original_payload in an APToken. + // Parses the original_payload in an exchanged JWT. // Returns true if original_payload can be - // parsed successfully. Otherwise, return false. + // parsed successfully. Otherwise, returns false. static bool ExtractOriginalPayload(const std::string& token, std::string* original_payload); From 8026dba58e1bfc4607f82a61159a07c0c03f49cc Mon Sep 17 00:00:00 2001 From: Lei Tang <32078630+lei-tang@users.noreply.github.com> Date: Fri, 21 Dec 2018 11:28:54 +0800 Subject: [PATCH 09/12] Revise the function of finding the token header --- src/envoy/http/authn/authenticator_base.cc | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/envoy/http/authn/authenticator_base.cc b/src/envoy/http/authn/authenticator_base.cc index 689890a37cc..68cb5c396f4 100644 --- a/src/envoy/http/authn/authenticator_base.cc +++ b/src/envoy/http/authn/authenticator_base.cc @@ -35,12 +35,8 @@ static const std::string kExchangedTokenHeaderName = "ingress-authorization"; // Returns whether the header for an exchanged token is found bool FindHeaderOfExchangedToken(const iaapi::Jwt& jwt) { - bool found = false; - if (jwt.jwt_headers_size() == 1 && - kExchangedTokenHeaderName == jwt.jwt_headers(0)) { - found = true; - } - return found; + return (jwt.jwt_headers_size() == 1 && + kExchangedTokenHeaderName == jwt.jwt_headers(0)); } } // namespace From e2ddef617428e374f9d942c78e939dd75fee78b6 Mon Sep 17 00:00:00 2001 From: Lei Tang <32078630+lei-tang@users.noreply.github.com> Date: Fri, 21 Dec 2018 11:44:11 +0800 Subject: [PATCH 10/12] Use case-insensitive compare for the header name --- src/envoy/http/authn/authenticator_base.cc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/envoy/http/authn/authenticator_base.cc b/src/envoy/http/authn/authenticator_base.cc index 68cb5c396f4..53a877d17cd 100644 --- a/src/envoy/http/authn/authenticator_base.cc +++ b/src/envoy/http/authn/authenticator_base.cc @@ -36,7 +36,8 @@ 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 && - kExchangedTokenHeaderName == jwt.jwt_headers(0)); + LowerCaseString(kExchangedTokenHeaderName) == + LowerCaseString(jwt.jwt_headers(0))); } } // namespace From 32ebbb6618c1f3fa1c69004128a74513ebdd10a9 Mon Sep 17 00:00:00 2001 From: Lei Tang <32078630+lei-tang@users.noreply.github.com> Date: Fri, 21 Dec 2018 14:55:32 +0800 Subject: [PATCH 11/12] Change the name of a variable --- src/envoy/http/authn/authn_utils.cc | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/envoy/http/authn/authn_utils.cc b/src/envoy/http/authn/authn_utils.cc index a358aa9b682..9cf852c80ed 100644 --- a/src/envoy/http/authn/authn_utils.cc +++ b/src/envoy/http/authn/authn_utils.cc @@ -29,8 +29,8 @@ namespace { static const std::string kJwtAudienceKey = "aud"; // The JWT issuer key name static const std::string kJwtIssuerKey = "iss"; -// The key name for the APToken original claims -static const std::string kAPTokenOriginalPayload = "original_claims"; +// 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. @@ -113,13 +113,14 @@ bool AuthnUtils::ExtractOriginalPayload(const std::string& token, return false; } - if (json_obj->hasObject(kAPTokenOriginalPayload) == false) { + if (json_obj->hasObject(kExchangedTokenOriginalPayload) == false) { return false; } Envoy::Json::ObjectSharedPtr original_payload_obj; try { - auto original_payload_obj = json_obj->getObject(kAPTokenOriginalPayload); + auto original_payload_obj = + json_obj->getObject(kExchangedTokenOriginalPayload); *original_payload = original_payload_obj->asJsonString(); ENVOY_LOG(debug, "{}: the original payload in APToken is {}", __FUNCTION__, *original_payload); From a505e9dd69e2d6ebb8711cec2b064e6029f400c1 Mon Sep 17 00:00:00 2001 From: Lei Tang <32078630+lei-tang@users.noreply.github.com> Date: Fri, 21 Dec 2018 15:03:26 +0800 Subject: [PATCH 12/12] Revise log statements --- src/envoy/http/authn/authn_utils.cc | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/envoy/http/authn/authn_utils.cc b/src/envoy/http/authn/authn_utils.cc index 9cf852c80ed..df81d43f30d 100644 --- a/src/envoy/http/authn/authn_utils.cc +++ b/src/envoy/http/authn/authn_utils.cc @@ -122,10 +122,11 @@ bool AuthnUtils::ExtractOriginalPayload(const std::string& token, auto original_payload_obj = json_obj->getObject(kExchangedTokenOriginalPayload); *original_payload = original_payload_obj->asJsonString(); - ENVOY_LOG(debug, "{}: the original payload in APToken is {}", __FUNCTION__, - *original_payload); + ENVOY_LOG(debug, "{}: the original payload in exchanged token is {}", + __FUNCTION__, *original_payload); } catch (...) { - ENVOY_LOG(debug, "{}: original_payload in APToken is of invalid format.", + ENVOY_LOG(debug, + "{}: original_payload in exchanged token is of invalid format.", __FUNCTION__); return false; }