GenerateJWT is escaping slashes in claim values before signing

Hi,

I have this JWT:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzdWJqZWN0LXN1YmplY3QiLCJhdWQiOlsiYXVkaWVuY2UxIiwiYXVkaWVuY2UyIl0sImlzcyI6InVybjpcL1wvYXBpZ2VlLWVkZ2UtSldULXBvbGljeS10ZXN0IiwiZXhwIjoxNTkwNTM3OTA4LCJhY3Rpb25zIjoiYVwvYlwvYVwvYlwvYVwvYyIsImlhdCI6MTU5MDUwOTEwOCwianRpIjoiM2M3NGEyM2QtZTU1ZS00N2I5LTkxZWUtMDUxOGQyY2MwYzFmIn0.nkD2RUr5NNGG3bqO0UMCO33MD_ld91CI0jgTdZhmy1M

This JWT have custom claim called actions:

"actions": "a/b/a/b/a/c"

as you can see, this string have the '/' element.

Once we are decoding this in Apigee (or other tools) - we can see in the final variable, that this claim getting escaped: (payload-json var)

{"sub":"subject-subject","aud":["audience1","audience2"],"iss":"urn:\/\/apigee-edge-JWT-policy-test","exp":1590540403,"actions":"a\/\/b\/\/a\/\/b\/\/a\/\/c","iat":1590511603,"jti":"675646c9-ec88-4fe0-9689-5aef7f75095e"}

and also urn...

My backend can't work with this escaping

Any ideas?

Solved Solved
1 11 2,754
1 ACCEPTED SOLUTION

Thanks for that. what I understand is:

  • you're using Apigee GenerateJWT to generate a JWT
  • one of the claims is created via an AdditionalClaims/Claim element, using a variable reference
  • you want the API proxy to send the output of that (the generated JWT) to the backend (upstream) system.
  • The people that run the upstream system are saying that the JWT they receive is "broken" - I guess because the forward-slashes are being backslash-escaped.

I just tested GenerateJWT with your configuration and a variable that holds "a/b/c/d" and I see that the base64-decoded payload looks like this:

{"aud":["audience1","audience2"],"iss":"urn:\/\/apigee-edge-JWT-policy-test","exp":1590643673,"actions":"a\/b\/c\/b","iat":1590614873,"jti":"a264a537-32cd-4fd1-9cda-507f96566297"}

...which is consistent with my understanding of your description.

It's unfortunate, but: Escapes of forward slash are valid though not required in JSON.

A correct processor of JSON must be able to handle backslash-escaped forward slashes. Since the JWT payload is simply JSON, While you might look at the behavior of Apigee GenerateJWT here and say "I don't want the escapes" , it is correct and valid. The way to avoid the problem is: the backend needs to handle the escaped JSON properly.

One *possible* workaround is to use the GenerateJWS policy. The GenerateJWS policy signs a payload, similar to the way the GenerateJWT signs a payload. But, the payload in a JWS can be "anything", any stream of bytes. The GenerateJWS policy doesn't assume you want a JSON payload and doesn't quietly insert backslash-escapes for forward-slashes if the payload happens to be JSON.

The policy configuration would look like this:

<GenerateJWS name='GenerateJWS-1'>
  <Algorithm>HS256</Algorithm>
  <IgnoreUnresolvedVariables>false</IgnoreUnresolvedVariables>
  <SecretKey>
    <Value ref='private.secretkey'/>
    <Id>cb24a396-4414-4793-b4ce-90333b694bff</Id>
  </SecretKey>
  <Payload ref='jws-payload'/>
  <AdditionalHeaders>
    <Claim name='typ'>JWT</Claim>
  </AdditionalHeaders>
  <OutputVariable>output-jwt</OutputVariable>
</GenerateJWS>

And before that, you would need to set into the variable "jws-payload", the exact JSON you want to sign. You could use something like this:

<AssignMessage name='AM-Payload'>
  <AssignVariable>
    <Name>jws-payload</Name>
    <Value>{"aud":["audience1","audience2"],"iss":"urn://apigee-edge-JWT-policy-test","exp":1590643673,"actions":"a/b/c/b","iat":1590614873,"jti":"a264a537-32cd-4fd1-9cda-507f96566297"}</Value>
  </AssignVariable>
</AssignMessage>

Be aware: the iat and exp claims are fixed in this example, and that's probably not what you want. So a better method may be to use a JS step to create the payload and initialize the iat/exp claims as you prefer. Maybe like this:

<Javascript name='JS-SetPayload' >
  <Source>
var payload = {
  "aud":["audience1","audience2"],
  "iss":"urn://apigee-edge-JWT-policy-test",
  "actions":"a/b/c/b"
};
var lifetimeInSeconds = 600;
var now = new Date();
now = Math.round(now.valueOf()/1000);
payload.iat = now
payload.exp = now + lifetimeInSeconds;
context.setVariable('jws-payload',JSON.stringify(payload));
  </Source>
</Javascript>

View solution in original post

11 REPLIES 11

Yes,

the behavior you are observing and reporting is expected. Frustrating, maybe, but also expected.

As noted on a previous community Q&A, other policies aside from DecodeJWT (such as ExtractVariables) can also apply escapes of forward slash.

Escapes of forward slash are valid but not required in JSON. The JSON standard instructs JSON processors (any app) to treat the two claims in this JSON blob as equivalent:

{
  "a" : "foo/bar/bam",
  "b" : "foo\/bar\/bam"
}

If you are sending actual JSON to the backend, such as what is contained in the variable jwt.POLICYNAME.ayload-json , then the backend system is incorrect if it cannot handle those escapes.

If you are sending one of the individual strings t the backend (like urn, or whatever), then you need to unescape the string before sending it.

An easy way to do this is with AssignMessage / AssignVariable, using the replaceAll function.

Here's an example:

<AssignMessage name='AM-UnescapeForwardSlash'>
  <IgnoreUnresolvedVariables>false</IgnoreUnresolvedVariables>

  <AssignVariable>
    <Name>escapedForwardSlash</Name>
    <!-- double the backslash to get one backslash -->
    <Value>\\/</Value>
  </AssignVariable>

  <AssignVariable>
    <Name>forwardSlash</Name>
    <Value>/</Value>
  </AssignVariable>

  <AssignVariable>
    <Name>unescaped</Name>
    <Template>{replaceAll(jwt.DecodeJWT-1.payload-json,escapedForwardSlash,forwardSlash)}</Template>
  </AssignVariable>

</AssignMessage>

Yes, exactly. What if we are talking about my backend is other APIM solution who expect to get the response not escaped? I guess the reason is that this is a string and JSON parser does this... and escapes

It looks like your reply was truncated... "Escapes confuse the other system..." I guess that's where you were going.

OK, so you will need to unescape them before passing them along. I added an example of the AssignMessage that should work, above. You can apply it to any of the variables set by the VerifyJWT policy, not just jwt.POLICY.payload-json .

This unescaping should be completely benign, should not change anything other than the escaped slashes.

Yes, but how? I transfer the string unescaped, and JWT seems to escape it automatically while signing... changing the signature and removing the escape after signature will break the signature, no?

const uri = "a/b/c/d"
context.setVariable("uri",uri)
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<GenerateJWT async="false" continueOnError="false" enabled="true" name="genJWT">
    <DisplayName>genJWT</DisplayName>
    <Algorithm>HS256</Algorithm>
    <SecretKey>
        <Value ref="private.secret"/>
    </SecretKey>
    <Subject>subject-subject</Subject>
    <Issuer>urn://apigee-edge-JWT-policy-test</Issuer>
    <Audience>audience1,audience2</Audience>
    <ExpiresIn>8h</ExpiresIn>
    <AdditionalClaims>
        <Claim name="actions" ref="uri" type="string"/>
    </AdditionalClaims>
    <OutputVariable>jwt-variable</OutputVariable>
</GenerateJWT>

oh! You are correct: if you modify the JSON, then whatever signature had been applied to the JSON will no longer be valid.

I think I have misunderstood your original requirement.

Are you planning to forward the original JWT to the upstrea,?

Are you hoping to forward the decoded-and-verified JSON payload (the output of VerifyJWT)?

Something else? Maybe you could elaborate.

We have B2B situation where we have Apigee as Client and other B as a backend. We are sending JWT there, with one of the fields with 'a/b/a' format. They receiving it with contact and escapes and telling us that our JWT broken.

So my goal is to try to send the JWT without the escape chars. I think magic happens inside JWT policy - where Jackson serializes the JSON and then signs the JWT. I might look at your custom JWT in the past to see if same behaviour occurs

I guess the reason is that this is a string and JSON parser does this...

Thanks for that. what I understand is:

  • you're using Apigee GenerateJWT to generate a JWT
  • one of the claims is created via an AdditionalClaims/Claim element, using a variable reference
  • you want the API proxy to send the output of that (the generated JWT) to the backend (upstream) system.
  • The people that run the upstream system are saying that the JWT they receive is "broken" - I guess because the forward-slashes are being backslash-escaped.

I just tested GenerateJWT with your configuration and a variable that holds "a/b/c/d" and I see that the base64-decoded payload looks like this:

{"aud":["audience1","audience2"],"iss":"urn:\/\/apigee-edge-JWT-policy-test","exp":1590643673,"actions":"a\/b\/c\/b","iat":1590614873,"jti":"a264a537-32cd-4fd1-9cda-507f96566297"}

...which is consistent with my understanding of your description.

It's unfortunate, but: Escapes of forward slash are valid though not required in JSON.

A correct processor of JSON must be able to handle backslash-escaped forward slashes. Since the JWT payload is simply JSON, While you might look at the behavior of Apigee GenerateJWT here and say "I don't want the escapes" , it is correct and valid. The way to avoid the problem is: the backend needs to handle the escaped JSON properly.

One *possible* workaround is to use the GenerateJWS policy. The GenerateJWS policy signs a payload, similar to the way the GenerateJWT signs a payload. But, the payload in a JWS can be "anything", any stream of bytes. The GenerateJWS policy doesn't assume you want a JSON payload and doesn't quietly insert backslash-escapes for forward-slashes if the payload happens to be JSON.

The policy configuration would look like this:

<GenerateJWS name='GenerateJWS-1'>
  <Algorithm>HS256</Algorithm>
  <IgnoreUnresolvedVariables>false</IgnoreUnresolvedVariables>
  <SecretKey>
    <Value ref='private.secretkey'/>
    <Id>cb24a396-4414-4793-b4ce-90333b694bff</Id>
  </SecretKey>
  <Payload ref='jws-payload'/>
  <AdditionalHeaders>
    <Claim name='typ'>JWT</Claim>
  </AdditionalHeaders>
  <OutputVariable>output-jwt</OutputVariable>
</GenerateJWS>

And before that, you would need to set into the variable "jws-payload", the exact JSON you want to sign. You could use something like this:

<AssignMessage name='AM-Payload'>
  <AssignVariable>
    <Name>jws-payload</Name>
    <Value>{"aud":["audience1","audience2"],"iss":"urn://apigee-edge-JWT-policy-test","exp":1590643673,"actions":"a/b/c/b","iat":1590614873,"jti":"a264a537-32cd-4fd1-9cda-507f96566297"}</Value>
  </AssignVariable>
</AssignMessage>

Be aware: the iat and exp claims are fixed in this example, and that's probably not what you want. So a better method may be to use a JS step to create the payload and initialize the iat/exp claims as you prefer. Maybe like this:

<Javascript name='JS-SetPayload' >
  <Source>
var payload = {
  "aud":["audience1","audience2"],
  "iss":"urn://apigee-edge-JWT-policy-test",
  "actions":"a/b/c/b"
};
var lifetimeInSeconds = 600;
var now = new Date();
now = Math.round(now.valueOf()/1000);
payload.iat = now
payload.exp = now + lifetimeInSeconds;
context.setVariable('jws-payload',JSON.stringify(payload));
  </Source>
</Javascript>

Hi Dino, I will take a look, thank you very much. The output of JWT will be like in the JWT e.g. xxx.xxx.xxx, i.e regular JWT token?

Yes, correct. And the output will be verifiable as a regular JWT.

Thanks Dino, it helped!