Conversation
src/envoy/http/authn/authn_utils.cc
Outdated
| // The JWT issuer key name | ||
| static const std::string kJwtIssuerKey = "iss"; | ||
| // The APToken issuer | ||
| static const std::string kAPTokenIssuer = "https://cloud.google.com/iap"; |
7b3d487 to
bb55390
Compare
| 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)) { |
There was a problem hiding this comment.
we may require token exchange service to keep origin header of jwt, so that filter could restore original jwt payload (for back compatibility in case app process original jwt payload); this could be done later. /cc @diemtvu
There was a problem hiding this comment.
Is the goal making AP token "transparent" to the service admin? I don't think this is right way of handling ap token. The service/namespace admin should be well aware that he/she is handling AP token, which is different from an external token.
There was a problem hiding this comment.
Agree with Limin. This should be configured explicitly. There could be use cases that admin want to keep the aptoken credential, or it can pack original payload differently (which we may need more than just a boolean flag).
I would suggest adding a flag to JWT configuration, and calling this function base on that value.
Regarding sending the original payload to backend, it's up to our decision on the feature. If we want user can use IAP without any change on there application, then it would be a nice option to have, though I think it would be more complicated. We can ignore that for now.
There was a problem hiding this comment.
Based on the discussion today, the code has been revised.
| @@ -0,0 +1 @@ | |||
| eyJhbGciOiJSUzI1NiIsImtpZCI6IkRIRmJwb0lVcXJZOHQyenBBMnFYZkNtcjVWTzVaRXI0UnpIVV8tZW52dlEiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJleGFtcGxlLWF1ZGllbmNlIiwiZW1haWwiOiJmb29AZ29vZ2xlLmNvbSIsImV4cCI6NDY5ODAyMzM3NywiaWF0IjoxNTQ0NDIzMzc3LCJpc3MiOiJodHRwczovL2Nsb3VkLmdvb2dsZS5jb20vaWFwIiwiaXN0aW9fYXR0cmlidXRlcyI6W3sic291cmNlLmlwIjoiMTI3LjAuMC4xIn1dLCJrZXkxIjpbInZhbDIiLCJ2YWwzIl0sIm9yaWdpbmFsX3BheWxvYWQiOnsiZW1haWwiOiJ1c2VyQGV4YW1wbGUuY29tIiwiaXNzIjoiaHR0cHM6Ly9hY2NvdW50cy5leGFtcGxlLmNvbSIsInN1YiI6ImV4YW1wbGUtc3ViamVjdCJ9LCJzdWIiOiJodHRwczovL2FjY291bnRzLmV4YW1wbGUuY29tLzEyMzQ1NTY3ODkwIn0.lY-bObNybFN8RxguZcgEd3TrC7-dSRH6VjXqkMvaN1WXz5xk8EEca0vlfkc2RRCZCCGDsSJiZOWOkV5c3RX5EAGcHppMFxuX2YZMQOoConuN_WCdp88gTKuICDUp_ZLi06nWOzqhIr4GG2QYDxSOMxTFqMl_qcJTKEcjpJM-pbhVTgbTb020C4_mFOEespff2Kh_ySzkZnlreddP_oUvUrKKoIHBf3M_ZKpivuSyyBjL52iFmsMJL4nmsk-xLIDuw5NeNyUGDa5j-EwiD3XvrFurrIAkKPT42E5Z5uomuw8h-zUoJt_jpdIynOOA2eyQXfXwsVnpOnMWDut66u92xQ | |||
There was a problem hiding this comment.
when have time, could you pls add a short readme on how this fake jwt is generated ? thanks !
There was a problem hiding this comment.
When I have time, I can create a separate PR for the readme of how this JWT is generated.
| "config": { | ||
| "rules": [ | ||
| { | ||
| "issuer": "https://cloud.google.com/iap", |
There was a problem hiding this comment.
Change to "https://example.token_service.com".
| return true; | ||
| } | ||
|
|
||
| bool AuthnUtils::ExtractOriginalPayload(const std::string& token, |
There was a problem hiding this comment.
How do you decide when to extract original claims instead of normal claims? From the code, it seems that as long as "original_payload" claim exists, you think it is an AP token and extract from original payload? I don't think this is the right way to handle it. Admin should either specifically mention that this is AP token, or "iss" matches configured AP token issuer.
There was a problem hiding this comment.
If a token is an APToken, the original claims are extracted. Otherwise, normal claims are extracted. What token is considered an APToken is based on Quanjie's suggestion: if a token has an original_payload claim, and there is an issuer claim in the original_payload claim, and the issuer in the original_payload claim is different from the issuer of the token, the token is considered an APToken.
The rationale of such code is that a token exchange makes the issuers of the APToken and the original JWT to be different (if the two issuers are the same, there is no need for the token exchange).The code to check that the issuers are different are on the lines 125~129.
There was a problem hiding this comment.
Will the extracted original_payload be verified? If not, it seems there will be a security hole that the current request could impersonate any issuer and claims by using this original_payload field even if APToken is not enabled.
It might be better to only extract the original_payload if the outter jwt is issued by google.com. This prevents user/attacker to abuse the original_payload field without the need to do another verification.
/cc @quanjielin @diemtvu
There was a problem hiding this comment.
The extracted original_payload will not be verified by a recipient of the APToken: the APToken issuer verified the original JWT and the recipient of the APToken verified the APToken. If an APToken is valid, the recipient regards the payload is valid.
There was a problem hiding this comment.
@yangminzhu already gave an example why just checking "original_payload" (and issuers are different) is not enough to prove the token is indeed an AP token. My preferred way is to introduce APTokenConfig where users can specify their specific AP token properties (e.g., iss). If you want to work something out faster, an alternative is to standardize AP token header (e.g., x-ingress-authorization (TBD)). We can say that the token in the standardized header must be AP token.
There was a problem hiding this comment.
While agree with not doing this implicitly, I don't totally agree with Yangmin example. The original_payload (and all other claims in the outer jwt) are still verified by the outer jwt issuer, right
There was a problem hiding this comment.
Based on the discussion today, the code has been revised.
There was a problem hiding this comment.
@diemtvu
Sorry for missing the background, I was trying to say what should we do when the outter issuer is compromised. In this case, the request could explicitly include the original_claims field, like the following one:
{
"aud": "abc",
"exp": 4698361508,
"iat": 1544761508,
"iss": "xyz.com",
"original_claims": {
"iss": "google.com",
"sub": "userABC"
}
}The issuer xyz.com is compromised so the above JWT will pass the verification. However it contains the original_claims with google.com as the issuer and userABC as subject. This is not a problem for now because even if the xyz.com gets compromised, it still cannot impersonate the google.com.
@liminw
Could the attacker set the x-ingress-authorization header explicitly in their request to trick us to believe it's a JWT (APToken) returned by the token exchange service, right?
There was a problem hiding this comment.
@yangminzhu "xyz.com" is listed by the operator as a trusted issuer, and we also do signature verification to make sure the token is signed by "xyz.com". So whatever contained in the token, we can assume that it is trusted.
"ingress-authorization" header is the default AP token location. The token put in there we can assume it is AP token. We will validate the token's signature to ensure that it is trusted. So I don't have concern on its security aspect.
src/envoy/http/authn/authn_utils.cc
Outdated
| 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 |
There was a problem hiding this comment.
This logic sounds like a hack. It basically says "if original_payload exists, and original issuer is not the same as new issuer, it is AP token". This is not how AP token is defined.
There was a problem hiding this comment.
Based on the discussion today, the code has been revised.
There was a problem hiding this comment.
Do we still need to compare issuer?
There was a problem hiding this comment.
I recommend comparing issuers because if an token exchange occurs, the issuer of the exchanged token should be different from the issuer of the original token. @quanjielin @liminw, what is your thought on comparing issuers?
There was a problem hiding this comment.
I don't think it is a hard requirement that the issuers are different. We can probably drop the check here.
There was a problem hiding this comment.
Done: the compare/check issuers have been removed from the code.
| 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)) { |
There was a problem hiding this comment.
Is the goal making AP token "transparent" to the service admin? I don't think this is right way of handling ap token. The service/namespace admin should be well aware that he/she is handling AP token, which is different from an external token.
| 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)) { |
There was a problem hiding this comment.
Agree with Limin. This should be configured explicitly. There could be use cases that admin want to keep the aptoken credential, or it can pack original payload differently (which we may need more than just a boolean flag).
I would suggest adding a flag to JWT configuration, and calling this function base on that value.
Regarding sending the original payload to backend, it's up to our decision on the feature. If we want user can use IAP without any change on there application, then it would be a nice option to have, though I think it would be more complicated. We can ignore that for now.
src/envoy/http/authn/authn_utils.cc
Outdated
| // 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"; |
There was a problem hiding this comment.
Do we have this agreement on APToken format?
There was a problem hiding this comment.
Based on the discussion today, original_payload has been changed to original_claims.
| @@ -0,0 +1,27 @@ | |||
| -----BEGIN RSA PRIVATE KEY----- | |||
There was a problem hiding this comment.
What are these key/cert file for. Please add a README if they are used to generate test data, and/or how to replace this file if need to.
There was a problem hiding this comment.
These key/cert files have been removed. I have added 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.
| @@ -0,0 +1,116 @@ | |||
| { | |||
There was a problem hiding this comment.
Does this file used in unit test? I don't see any code using this.
There was a problem hiding this comment.
This file is used in a guide I added. I have added 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.
There was a problem hiding this comment.
It's better to write a test. You can add them to test/integration. There is already few test setup examples.
There was a problem hiding this comment.
A test has been added.
| return true; | ||
| } | ||
|
|
||
| bool AuthnUtils::ExtractOriginalPayload(const std::string& token, |
There was a problem hiding this comment.
While agree with not doing this implicitly, I don't totally agree with Yangmin example. The original_payload (and all other claims in the outer jwt) are still verified by the outer jwt issuer, right
| @@ -0,0 +1,116 @@ | |||
| { | |||
There was a problem hiding this comment.
It's better to write a test. You can add them to test/integration. There is already few test setup examples.
| std::string payload_to_process = jwt_payload; | ||
| std::string original_payload; | ||
| bool found = false; | ||
| for (auto h : jwt.jwt_headers()) { |
There was a problem hiding this comment.
For readability, make a helper function (say IsAPTokenIssuer) for this.
| } | ||
| } | ||
| if (found && | ||
| AuthnUtils::ExtractOriginalPayload(jwt_payload, &original_payload)) { |
There was a problem hiding this comment.
If it is APToken (found), but cannot extract original payload, then probably should return false.
There was a problem hiding this comment.
Limin mentioned that an APToken may not necessarily contain the original payload (i.e., original_claims). If an APToken must contain the original payload, we can use it as a way to tell whether a token is an APToken. Hence, no false is returned here.
There was a problem hiding this comment.
Is that so? Again, I feel wrong to while making assumption of the location, but not on the token format and it's behavior. Anyway, please add comment to explain the rationale.
There was a problem hiding this comment.
Done: added comments to explain the rationale.
There was a problem hiding this comment.
I think for the first version, it is ok to assume that AP token always carries original_claims. This may not be true going forward, but let's focus on this JWT token exchange use case for now.
There was a problem hiding this comment.
Code has been revised to require an APToken to always carry original_claims: if it is APToken, but cannot extract original payload from original_claims, the code will return false.
| @@ -68,7 +73,21 @@ bool AuthenticatorBase::validateX509(const iaapi::MutualTls& mtls, | |||
| bool AuthenticatorBase::validateJwt(const iaapi::Jwt& jwt, Payload* payload) { | |||
There was a problem hiding this comment.
You should have new unit tests for this change.
|
|
||
| namespace { | ||
| // The default header name for an exchanged token | ||
| static const std::string kExchangedTokenHeaderName = "x-ingress-authorization"; |
| } | ||
| } | ||
| if (found && | ||
| AuthnUtils::ExtractOriginalPayload(jwt_payload, &original_payload)) { |
There was a problem hiding this comment.
Instead of doing string copy, you can directly put "payload_to_process" in the function.
AuthnUtils::ExtractOriginalPayload(jwt_payload, &payload_to_process)
There was a problem hiding this comment.
If put "payload_to_process" in ExtractOriginalPayload(), ExtractOriginalPayload() may change "payload_to_process" even when ExtractOriginalPayload() returns false. Therefore, "payload_to_process" should not be put in ExtractOriginalPayload().
| return true; | ||
| } | ||
|
|
||
| bool AuthnUtils::ExtractOriginalPayload(const std::string& token, |
There was a problem hiding this comment.
@yangminzhu "xyz.com" is listed by the operator as a trusted issuer, and we also do signature verification to make sure the token is signed by "xyz.com". So whatever contained in the token, we can assume that it is trusted.
"ingress-authorization" header is the default AP token location. The token put in there we can assume it is AP token. We will validate the token's signature to ensure that it is trusted. So I don't have concern on its security aspect.
a7ea99a to
c1d0aab
Compare
| // 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()) { |
There was a problem hiding this comment.
If we make assumption on the location value, should we also make assumption/requirement that the Jwt spec should declare only one header location. It seems weird (and could be wrong) to define more than one for aptoken.
There was a problem hiding this comment.
yes, sounds reasonable. We can require that there is only one header specified.
There was a problem hiding this comment.
Please change this per agreement. You can add check for jwt_headers() size and compare the only one in there.
| } | ||
| } | ||
| if (found && | ||
| AuthnUtils::ExtractOriginalPayload(jwt_payload, &original_payload)) { |
There was a problem hiding this comment.
Is that so? Again, I feel wrong to while making assumption of the location, but not on the token format and it's behavior. Anyway, please add comment to explain the rationale.
| EXPECT_TRUE(MessageDifferencer::Equals(expected_payload, *payload_)); | ||
| } | ||
|
|
||
| TEST_F(ValidateJwtTest, OriginalPayloadOfExchangedToken) { |
There was a problem hiding this comment.
Please add a test for the case when original payload was not there. Any may be another test with original payload, but jwt was not in the intended location.
There was a problem hiding this comment.
Done: added two more tests.
src/envoy/http/authn/authn_utils.cc
Outdated
| 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 |
There was a problem hiding this comment.
Do we still need to compare issuer?
src/envoy/http/authn/authn_utils.h
Outdated
| static bool ProcessJwtPayload(const std::string& jwt_payload_str, | ||
| istio::authn::JwtPayload* payload); | ||
|
|
||
| // Parse the original_payload in an APToken. |
There was a problem hiding this comment.
nit Parse -> Parses, Return -> Returns
| @@ -0,0 +1,118 @@ | |||
| { | |||
There was a problem hiding this comment.
Please add integration test to cover this setup, rather than giving sample.
There was a problem hiding this comment.
aptoken-envoy.conf and the user guide added in this PR are for a developer that is interested in going through the APToken authentication flow on Istio proxy. The code change in this PR is on validateJwt(). Three new tests on validateJwt() have been added to test the code change. An integration test will duplicate what these three new tests have tested with respect to the code change on validateJwt().
There was a problem hiding this comment.
The tests you added already simulate the payload input to authN filter. It doesn't cover the full interaction between jwt filter and authn filter (and even mixer) as you can have with 'integration' test (I admit, the name is a bit misleading; it's still unit test, but testing few filters chained together instead of one).
Having sample config add a little benefit to the maintenance in the long run (config might change, overhead of setting up runtime etc). Given the way to set up the test envoy exactly likes the one specified by this config is fairly simple, I would suggest convert this into a test.
There was a problem hiding this comment.
The Istio authn integration tests that cover jwt filter, authn filter and mixer are in a different repository istio/istio under the path https://github.com/istio/istio/mixer/test/client. To add an integration test for authenticating an exchange token, this PR should be merged first, then the SHA in the istio.deps of the istio/istio proxy should be updated to the new SHA of istio-proxy, and finally an integration test for an exchanged token can be added under https://github.com/istio/istio/mixer/test/client. I have created an issue istio/istio#10514 to add an integration test in the istio/istio repository and assigned it to myself.
There was a problem hiding this comment.
Done: integration tests have been added to https://github.com/istio/proxy/tree/master/test/integration.
| @@ -0,0 +1,118 @@ | |||
| { | |||
There was a problem hiding this comment.
The tests you added already simulate the payload input to authN filter. It doesn't cover the full interaction between jwt filter and authn filter (and even mixer) as you can have with 'integration' test (I admit, the name is a bit misleading; it's still unit test, but testing few filters chained together instead of one).
Having sample config add a little benefit to the maintenance in the long run (config might change, overhead of setting up runtime etc). Given the way to set up the test envoy exactly likes the one specified by this config is fairly simple, I would suggest convert this into a test.
| @@ -0,0 +1,18 @@ | |||
| This is a guide of sending an example exchanged token to | |||
There was a problem hiding this comment.
To echo the previous point about adding test, why developer needs to follow this series of steps to maintain/debug the code if they can do so with a simple "go test" command? Not to mention, the instruction is not clear what significant one needs to look for to see if APToken was handled correctly.
There was a problem hiding this comment.
The "go test" for running the Istio authn integration tests are in a different repository istio/istio under the path https://github.com/istio/istio/mixer/test/client. To add an integration test for authenticating an exchange token, this PR should be merged first, then the SHA in the istio.deps of the istio/istio proxy should be updated to the new SHA of istio-proxy, and finally an integration test for an exchanged token can be added. I have created an issue istio/istio#10514 to add an integration test in the istio/istio repository and assigned it to myself.
There was a problem hiding this comment.
I mean https://github.com/istio/proxy/tree/master/test/integration. The one in istio repo is a plus, but not really necessary.
There was a problem hiding this comment.
Done: integration tests have been added to https://github.com/istio/proxy/tree/master/test/integration.
96c30cd to
805bc28
Compare
805bc28 to
784474a
Compare
| Payload expected_payload; | ||
| JsonStringToMessage( | ||
| R"({ | ||
| "jwt": { |
There was a problem hiding this comment.
Why original_claims is not shown up in the output?
There was a problem hiding this comment.
Because original_claims is not a string or string list.
| // 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 {}", |
There was a problem hiding this comment.
Please make the error message self-explained. Change to smt like: "Expect exchanged-token with original payload claim. Received: ", ...
| // 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 |
| static const std::string kExchangedTokenHeaderName = "ingress-authorization"; | ||
|
|
||
| // Return whether the header for an exchanged token is found | ||
| bool FindHeaderOfExchangedToken(const iaapi::Jwt& jwt) { |
There was a problem hiding this comment.
IsExchangedToken or IsAPToken may be a better name.
There was a problem hiding this comment.
FindHeaderOfExchangedToken() accurately describes what this function does.
| // 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()) { |
There was a problem hiding this comment.
Please change this per agreement. You can add check for jwt_headers() size and compare the only one in there.
| const std::string kHeaderForExchangedToken = "ingress-authorization"; | ||
|
|
||
| // Generates basic test request header. | ||
| Http::TestHeaderMapImpl BaseRequestHeaders() { |
There was a problem hiding this comment.
I'm ok with this for now, but it would be nicer if you refactor these to a share library. Alternative (even better), you can have new test cases in the same test suit. They are not much different than the others.
There was a problem hiding this comment.
Done: added a check for jwt_headers() size and compare the only one in there.
|
@lei-tang Based on conversation with Kurt today, AP token has the following format I think the current JWT filter expects "Bearer ". For AP token, it should be "Istio " instead. This will require some changes in JWT filter, I think. |
|
cc @kmbarry1 |
|
@liminw due to time constraints, I suggest staying with the agreed solution of using ingress-authorization header for alpha. The new "Ingress-Authorization: Istio" can be put in for the next release after its design is finalized. @quanjielin, @diemtvu, @wenchenglu |
|
@lei-tang It's not a "new" header. We are still using "Ingress-Authorization" header. It's just instead of "Ingress-Authorization: Bearer ", the format is "Ingress-Authorization: Istio ". I do not expect this will require a big code change. Can you evaluate it? We must sync with Kurt on the expected header. Otherwise, it won't work end-to-end. cc @kmbarry1 |
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: lei-tang, liminw, quanjielin If they are not already assigned, you can assign the PR to them by writing The full list of commands accepted by this bot can be found here. The pull request process is described here DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
|
@lei-tang could you create another local branch with I notice that you create this PR at 12/12, the release-1.1 branch might have diverged from master branch at that time. It would be risky to force current branch merge-authn-exchanged-token rebase to upstream/release-1.1. When updating Proxy SHA into istio/istio release-1.1 branch, this change would break pilot e2e tests. |
src/envoy/http/authn/authn_utils.h
Outdated
| static bool ProcessJwtPayload(const std::string& jwt_payload_str, | ||
| istio::authn::JwtPayload* payload); | ||
|
|
||
| // Parses the original_payload in an APToken. |
There was a problem hiding this comment.
Is APToken a standard term or an new name defined in a design doc? Could you elaborate a little more?
There was a problem hiding this comment.
Changed to exchanged token.
src/envoy/http/authn/authn_utils.h
Outdated
|
|
||
| // Parses the original_payload in an APToken. | ||
| // Returns true if original_payload can be | ||
| // parsed successfully. Otherwise, return false. |
|
As the master branch is locked, #2070 is created to merge the code in this PR to the release-1.1 branch. |
diemtvu
left a comment
There was a problem hiding this comment.
It's better have other PR submitted, then cherry pick to master (or vice versa).
|
@lei-tang: PR needs rebase. DetailsInstructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository. |
|
#2078 has cherry-picked this PR from release 1.1 branch to the master branch. |
What this PR does / why we need it: authenticate an exchanged token.
Which issue this PR fixes (optional, in
fixes #<issue number>(, fixes #<issue_number>, ...)format, will close that issue when PR gets merged): fixes #2062Special notes for your reviewer:
Release note: