Verify JWT Using JWKS Invalid Key Configuration Error - Apigee X

Hi @dchiesa1 

Our enterprise is using Apigee X , and in order to validate an IDP minted token which is generated independent of Apigee, we are using the Verify JWT Policy. The policy is currently configured as below, where we are passing the JWKS url within the Public Key element. But we have been receiving the following error below. The value for uriref is fetched from a previous step in Assign Message Policy:

Error Response:

{
    "fault": {
        "faultstring": "Invalid Key configuration : policy(JWT-VerifyJWKS) element(PublicKey)",
        "detail": {
            "errorcode": "steps.jwt.InvalidKeyConfiguration"
        }
    }
}
 
Assign Message :

 

 

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<AssignMessage continueOnError="false" enabled="true" name="AM-AssignJWKS">
<DisplayName>AM-AssignJWKS</DisplayName>
<Properties/>
<AssignVariable>
<Name>kid_header</Name>
<Ref>jwt.JWT-DecodeJWT-KID.header.kid</Ref>
</AssignVariable>
<AssignVariable>
<Name>jwksurl</Name>
<Value>https://test.auth.highmark.com/oauth2/rest/security --header X-OAUTH-IDENTITY-DOMAIN-NAME: <value of kid>/Value>
</AssignVariable>
<IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
<AssignTo createNew="false" transport="http" type="request"/>
</AssignMessage>

 

 

Verify JWT

 

 

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<VerifyJWT continueOnError="false" enabled="true" name="JWT-VerifyJWKS">
    <DisplayName>JWT-VerifyJWKS</DisplayName>
    <Algorithm>RS256</Algorithm>
    <!-- <Source>request.header.authorization</Source> -->
    <PublicKey>
        <JWKS uriRef="jwksurl"/>
    </PublicKey>
</VerifyJWT>

 

 

I have tried to hardcode value of kid in Assign Message policy, I have also tried to assign the value of variable that is having the value of the kid in the Assign Message policy but even that has not worked.
 

I am sharing the JWT token (expired) :

eyJraWQiOiJhcGlnZWUtZGVtby1hcGltIiwieDV0IjoicUd5V1QxUVQteW9zcHpmZzN4M2htOFkxcDNZIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL29hdXRocWEuaG1ocy5jb20iLCJhdWQiOlsiaHR0cHM6Ly9jZHNzb3Rlc3QuaGlnaG1hcmsuY29tOjQ0My9vYXV0aDIiLCJyZXNvdXJjZSIsImFiMCJdLCJleHAiOjE3MTExMDg4MjgsImp0aSI6IjhBZ1FjaEptLVlaTU5TNldhVGRHanciLCJpYXQiOjE3MTExMDUyMjgsInN1YiI6IjQyMzQ0NDctMDMwNC01NDM3LWE2NzQyNDQ0ZTRhNTM1NjU0MmY0ZDQ0NzU2NTY3M2QzZCIsImNsaWVudCI6IjQyMzQ0NDctMDMwNC01NDM3LWE2NzQyNDQ0ZTRhNTM1NjU0MmY0ZDQ0NzU2NTY3M2QzZCIsInNjb3BlIjpbInJlc291cmNlLlJFQUQiXSwiZG9tYWluIjoiYXBpZ2VlLWRlbW8tYXBpbSIsInVpZCI6IiIsImhta1Rva2VuVHlwZSI6IkNsaWVudElEIiwibWFpbCI6IiIsImdpdmVuTmFtZSI6IiIsInNlc3Npb25JZCI6IiIsInNuIjoiIiwiaXNtZW1iZXJvZiI6IiIsInBybiI6IjQyMzQ0NDctMDMwNC01NDM3LWE2NzQyNDQ0ZTRhNTM1NjU0MmY0ZDQ0NzU2NTY3M2QzZCIsInJlc1NydkF0dHIiOiJSRVNPVVJDRUNPTlNUIiwiaG1rVG9rZW5WZXJzaW9uIjoidjQifQ.nJIi95radCKvclvam_V-wJMkePsF9emoMhGBqj-DI4z-8R6toRqGdTESTWKnUu3lvHXZzt1z3BNaSs9cKGog7_loNYKixwPTqcoBK4VjPhL7SsV8_H5YkrOqfBAWsfsOG3yfsrMD4f3swTukRR6UDv9Gq2XTdULe5y8CK56FseNBy-iILqYr4gf8QW1z7KRyigX-mRCgERR1H0TnjnlkCp6gm-2U18ioxyv4t22iB6NUdCocDd9ayEL2JF_dbpg-qJaQnYgsaWe5Iof4tIM175AjnopFsTpCpWujeeCmuPud_xrXALX3okrlfvstVuM5Uym5XPfi3VCsla0-46IiJA

 

Request your inputs as what I could be missing or done wrong.

Thanks,

Debjit

Solved Solved
2 13 292
3 ACCEPTED SOLUTIONS

Hi Debjit

I am unclear on how the VerifyJWT Policy can use the proxy from Step 1. The policy has options to pass a static uri or an uriRef, but am unclear on how the policy can leverage this proxy.

If I understand what you've done, you have a proxy that accepts a request like

https://test.auth.highmark.com/jwks-getter?domain=apigee-demo-apim

That's a static URL. You can just use that directly in your VerifyJWT policy.

 

<VerifyJWT continueOnError="false" enabled="true" name="JWT-VerifyJWKS">
    <DisplayName>JWT-VerifyJWKS</DisplayName>
    <Algorithm>RS256</Algorithm>
    <!-- <Source>request.header.authorization</Source> -->
    <PublicKey>
        <JWKS uri="https://test.auth.highmark.com/jwks-getter?domain=apigee-demo-apim"/>
    </PublicKey>
</VerifyJWT>

 

If you are thinking that you need to support. multiple different oauth domains with the same policy, that means you need to have a dynamic (determined at runtime) uri. In that case you can set a variable, and use uriRef. Set the variable in a policy like

 

<AssignMessage name='AM-OAuth-JWKS-URI'>
  <IgnoreUnresolvedVariables>false</IgnoreUnresolvedVariables>
  <AssignVariable>
    <Name>oauth_domain_fulluri</Name>
    <Template>https://test.auth.highmark.com/jwks-getter?domain={desired_oauth_domain}</Template>
  </AssignVariable>
</AssignMessage>

 

And then the VerifyJWT would be like this;

 

<VerifyJWT continueOnError="false" enabled="true" name="JWT-VerifyJWKS">
    <DisplayName>JWT-VerifyJWKS</DisplayName>
    <Algorithm>RS256</Algorithm>
    <!-- <Source>request.header.authorization</Source> -->
    <PublicKey>
        <JWKS uriRef="oauth_domain_fulluri"/>
    </PublicKey>
</VerifyJWT>

 

Also, let me clarify what I said earlier

Embed the Identity domain name as a path segment, rather than a header.

I was suggesting a Path segment, not a query param. So the inbound URL would look like

htt​ps://test.auth.highmark.com/oauth2/apigee-demo-apim/.well-known/jwks

And in the jwks wrapper proxy, you would use ExtractVariables to extract the path segment. Like this:

 

<ExtractVariables name='EV-OAuth-Domain'>
   <Source>request</Source>
   <VariablePrefix>extracted</VariablePrefix>
   <URIPath>
      <!-- put the extracted value into extracted.oauth_domain -->
      <!-- NB: The Pattern is matched against pathsuffix (what comes AFTER the basepath) -->
      <Pattern ignoreCase='false'>/{oauth_domain}/.well-known/jwks</Pattern>
   </URIPath>
   <IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
</ExtractVariables>

 

And then you have a variable extracted.oauth_domain , holding the value that you could set into the required header, X-OAUTH-IDENTITY-DOMAIN-NAME , with an AssignMessage.

THIS WILL WORK. Your query param approach will work, too, though it feels slightly less elegant to me.

But if I were the IDP architect on this system, I would want to avoid the wrapper proxy entirely. Ideally, the JWKS endpoint should not require that header. The JWKS should "natively" extract the domain from a URL path. Wrapping an Apigee proxy around that endpoint as a wrapper, helps, but the ideal solution would be to modify that JWKS endpoint at the source. I understand that might not be quickly feasible!

View solution in original post

Hi Dino,

Thank you for sharing the steps in detail, I will keep you posted how it goes. I am sorry but the decisions to update the JWKS URLs is with the IDP team, I will keep you posted about any updates on this front also.

 

Regards,

Debjit

View solution in original post

I just read about this in Assign Message Policy - AssingVariable - Templates

https://cloud.google.com/apigee/docs/api-platform/reference/message-template-intro#targetendpoint-el... 

Does that mean in Assign Message policy we can proxy another API which has as hostname as https://api-{env}.highmark.com

If yes will that be applicable to solution you have suggested and help. Could you please share any samples if available that helps to call proxy from the Assign Message if its possible.

View solution in original post

13 REPLIES 13

Yes - thanks for the clear explanation.

here's what's happening.

The "normal practice" for a JWKS is that there is a simple URI, that any party can send a GET to, to retrieve the JWKS. The VerifyJWT policy uses that convention, with the uri or uriRef parameter. You must specify a URI, only a URI, in one of those parameters. An example of this normal convention is the JWKS endpoint for googleapis , or the JWKS endpoint for any Okta tenant (documentation, example)

In your case, your JWKS endpoint does not respond to GET requests without the presence of the special Header, X-OAUTH-IDENTITY-DOMAIN-NAME.

Your attempt to insert that header into the URI, like this:

 

<AssignMessage continueOnError="false" enabled="true" name="AM-AssignJWKS">
  ...
  <AssignVariable>
    <Name>jwksurl</Name>
    <Value>https://test.auth.highmark.com/oauth2/rest/security --header X-OAUTH-IDENTITY-DOMAIN-NAME: <value of kid>/Value>
  </AssignVariable>
  ...
</AssignMessage>

 

... I understand what you're going for there, but that's not going to work. the VerifyJWT policy is looking for a simple URI. Not a thing with a URI and a --header argument. That's not going to work there.

So the way to get this to work is to send the request-for-JWKS with that header. You can do that with ServiceCallout.

 

<ServiceCallout name='SC-Get-JWKS'>
  <Request variable='simpleGetRequest'>
    <Set>
      <Headers>
        <Header name='X-OAUTH-IDENTITY-DOMAIN-NAME'>{variable-holding-kid}</Header>
      <Headers>
      <Verb>GET</Verb>
    </Set>
  </Request>
  <Response>jwksResponse</Response>
  <HTTPTargetConnection>
    <SSLInfo>
      <Enabled>true</Enabled>
      <IgnoreValidationErrors>false</IgnoreValidationErrors>
    </SSLInfo>
    <Properties>
      <Property name='success.codes'>2xx</Property>
    </Properties>
    <URL>https://test.auth.highmark.com/oauth2/rest/security</URL>
  </HTTPTargetConnection>
</ServiceCallout>

 

And then in your VerifyJWT, you can do this:

 

<VerifyJWT continueOnError="false" enabled="true" name="JWT-VerifyJWKS">
    <DisplayName>JWT-VerifyJWKS</DisplayName>
    <Algorithm>RS256</Algorithm>
    <PublicKey>
        <JWKS ref='jwksResponse.content'/>
    </PublicKey>
      ...other elements here...
</VerifyJWT>

 

This will "work", but don't do it. There's a better way. I have some further comments.

  • Using a simple servicecallout before the VerifyJWT policy will mean every time you verify a JWT, you have to make a network call. This is non-optimal. So you'd want to wrap a cache around that ServiceCallout policy. That's a bunch of extra work, at least 3 more policies probably.
  • To avoid that, consider re-designing your JWKS endpoint to follow the simple GET convention. Embed the Identity domain name as a path segment, rather than a header. This makes it much easier for VerifyJWT to use, and in fact easier for ANY other system to use. The resulting URL might look like

    htt​ps://test.auth.highmark.com/oauth2/apigee-demo-apim/.well-known/jwks

    Lots of libraries and packages depend on retrieving the JWKS with a simple GET. This will make your JWKS endpoint compatible with that approach. The VerifyJWT policy will automatically cache the JWKS, if you use the simple GET URI approach. And also, web systems will cache the data in the network, returning a 304 when it hasn't changed. No need for you to implement a cache yourself.

    It could mean you would need to rebuild the implementation of whatever is dispensing JWKS. But if you don't want to do that, then... using Apigee, you could just wrap your JWKS endpoint in another API Proxy, that accepts the path-segment parameter, then injects the required X-OAUTH-IDENTITY-DOMAIN-NAME header. And your VerifyJWT would use the proxy (facade) in front of the actual JWKS.

  • Regarding a kid value of apigee-demo-apim , That's probably not a good idea. Key IDs are generally random strings (see the examples I linked to above, for Google and Okta. The kids look like 09bcf8028e06537d... or aqnGtFezdHbKX57zyafb...). and the key IDs are NOT the same as the realm or domain for the keys. If you look at the JWKS url for an Okta tenant, htt​ps://dev-19504381.okta.com/oauth2/default/v1/keys , the realm or domain is "default". The keys returned have random strings for their kid values.

    It seems like in your case you are asking for the JWKS, given a specific KID. You pass the value of the KID in the X-OAUTH-IDENTITY-DOMAIN-NAME header. Which means the kid itself is equivalent to the identity domain. That , again, goes against all convention and best practice I have seen. Consider re-designing that too. Nobody else does it the way you are doing it! Normally within an identity domain (in your case, apigee-demo-apim), there are multiple keys, each with different kids (key1, key2, key3). If you do it this way, It will make it easier to rotate keys, when the time comes. It makes caching much easier for all clients or relying parties.

You might want to consult with an identity architect, or someone who has some more depth of experience implementing JWT and JWKS things. I can suggest some consultants who might be able to help, if you like.

Hi Dino, am looking for your advise, on one of the solutions which you had laid out.

Step 1 - I have made an API on Apigee that accepts a query parameter, which I use to pass it as the header value for - X-OAUTH-IDENTITY-DOMAIN-NAME and then call the JWKS endpoint. This proxy is working as expected, and giving a response. 

As the next step I am unclear on how the VerifyJWT Policy can use the proxy from Step 1. The policy has options to pass a static uri or an uriRef, but am unclear on how the policy can leverage this proxy. 

Looking for your advise. Other options you have suggested is being discussed by the Architects, and might take time to come to a conclusion.

Thanks,

Debjit

 


@dchiesa1 wrote:

Apigee, you could just wrap your JWKS endpoint in another API Proxy, that accepts the path-segment parameter, then injects the required X-OAUTH-IDENTITY-DOMAIN-NAME header. And your VerifyJWT would use the proxy (facade) in front of the actual JWKS.


 

 

Hi Debjit

I am unclear on how the VerifyJWT Policy can use the proxy from Step 1. The policy has options to pass a static uri or an uriRef, but am unclear on how the policy can leverage this proxy.

If I understand what you've done, you have a proxy that accepts a request like

https://test.auth.highmark.com/jwks-getter?domain=apigee-demo-apim

That's a static URL. You can just use that directly in your VerifyJWT policy.

 

<VerifyJWT continueOnError="false" enabled="true" name="JWT-VerifyJWKS">
    <DisplayName>JWT-VerifyJWKS</DisplayName>
    <Algorithm>RS256</Algorithm>
    <!-- <Source>request.header.authorization</Source> -->
    <PublicKey>
        <JWKS uri="https://test.auth.highmark.com/jwks-getter?domain=apigee-demo-apim"/>
    </PublicKey>
</VerifyJWT>

 

If you are thinking that you need to support. multiple different oauth domains with the same policy, that means you need to have a dynamic (determined at runtime) uri. In that case you can set a variable, and use uriRef. Set the variable in a policy like

 

<AssignMessage name='AM-OAuth-JWKS-URI'>
  <IgnoreUnresolvedVariables>false</IgnoreUnresolvedVariables>
  <AssignVariable>
    <Name>oauth_domain_fulluri</Name>
    <Template>https://test.auth.highmark.com/jwks-getter?domain={desired_oauth_domain}</Template>
  </AssignVariable>
</AssignMessage>

 

And then the VerifyJWT would be like this;

 

<VerifyJWT continueOnError="false" enabled="true" name="JWT-VerifyJWKS">
    <DisplayName>JWT-VerifyJWKS</DisplayName>
    <Algorithm>RS256</Algorithm>
    <!-- <Source>request.header.authorization</Source> -->
    <PublicKey>
        <JWKS uriRef="oauth_domain_fulluri"/>
    </PublicKey>
</VerifyJWT>

 

Also, let me clarify what I said earlier

Embed the Identity domain name as a path segment, rather than a header.

I was suggesting a Path segment, not a query param. So the inbound URL would look like

htt​ps://test.auth.highmark.com/oauth2/apigee-demo-apim/.well-known/jwks

And in the jwks wrapper proxy, you would use ExtractVariables to extract the path segment. Like this:

 

<ExtractVariables name='EV-OAuth-Domain'>
   <Source>request</Source>
   <VariablePrefix>extracted</VariablePrefix>
   <URIPath>
      <!-- put the extracted value into extracted.oauth_domain -->
      <!-- NB: The Pattern is matched against pathsuffix (what comes AFTER the basepath) -->
      <Pattern ignoreCase='false'>/{oauth_domain}/.well-known/jwks</Pattern>
   </URIPath>
   <IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
</ExtractVariables>

 

And then you have a variable extracted.oauth_domain , holding the value that you could set into the required header, X-OAUTH-IDENTITY-DOMAIN-NAME , with an AssignMessage.

THIS WILL WORK. Your query param approach will work, too, though it feels slightly less elegant to me.

But if I were the IDP architect on this system, I would want to avoid the wrapper proxy entirely. Ideally, the JWKS endpoint should not require that header. The JWKS should "natively" extract the domain from a URL path. Wrapping an Apigee proxy around that endpoint as a wrapper, helps, but the ideal solution would be to modify that JWKS endpoint at the source. I understand that might not be quickly feasible!

Hi Dino,

Thank you for sharing the steps in detail, I will keep you posted how it goes. I am sorry but the decisions to update the JWKS URLs is with the IDP team, I will keep you posted about any updates on this front also.

 

Regards,

Debjit

Hi Dino,

I am unable to find the option to attach the proxy I worked on but providing the steps I have done :

1. Stripping the Bearer from the access token. using Extract Variable.

2. Used Decode JWT Policy.

3. Using the value of jwt.<policy_name>.header.kid and assigning it to the header X-OAUTH-IDENTITY-DOMAIN-NAME.

4. Building the jwks URL.

5. Calling the Verify JWT policy.

The header value does get assigned correctly, but the error still persists. I have tried passing the value as path segment and query parameter while building the jwks endpoint in the proxy and assigning to the variable jwks but to no avail. 
I will contact the IDP team on this to see if they can work on getting the endpoint accepting a query parameter instead of passing the value in a header.
The hostname for the proxy which has the policies is

https://api-{env}.highmark.com/, and users use this hostname to call the  APIs based on environment.


The hostname for jwks endpoint within the Assign Message is https://test.auth.highmark.com

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<AssignMessage continueOnError="false" enabled="true" name="AM-AssignJWKS">
    <DisplayName>AM-AssignJWKS</DisplayName>
    <Properties/>
    <Add>
        <Headers>
            <Header name="X-OAUTH-IDENTITY-DOMAIN-NAME">{jwt.JWT-DecodeJWT-KID.header.kid}</Header>
        </Headers>
    </Add>
    <AssignVariable>
        <Name>jwksurl</Name>
        <Template>https://test.auth.highmark.com/oauth2/rest/security</Template>
    </AssignVariable>
    <IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
    <AssignTo createNew="false" transport="http" type="request"/>
</AssignMessage>

 

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<VerifyJWT continueOnError="false" enabled="true" name="JWT-VerifyJWKS">
    <DisplayName>JWT-VerifyJWKS</DisplayName>
    <Algorithm>RS256</Algorithm>
    <!-- <Source>request.header.authorization</Source> -->
    <PublicKey>
        <JWKS uriRef="jwksurl"/>
    </PublicKey>
</VerifyJWT>


Thanks,

Debjit

I just read about this in Assign Message Policy - AssingVariable - Templates

https://cloud.google.com/apigee/docs/api-platform/reference/message-template-intro#targetendpoint-el... 

Does that mean in Assign Message policy we can proxy another API which has as hostname as https://api-{env}.highmark.com

If yes will that be applicable to solution you have suggested and help. Could you please share any samples if available that helps to call proxy from the Assign Message if its possible.


@debjitd18 wrote:

Does that mean in Assign Message policy we can proxy another API which has as hostname as https://api-{env}.highmark.com

 


 

AssignMessage does not proxy to an endpoint.  You need a proxy , to proxy to an endpint.  AssignMessage is just a policy.  It sets and updates values on messages, or variables. 

The way a template works, if you specify "abc{def}" as the template, and a context variable exists named def holding value 123, then evaluating the template will insert the value of def in place of the string {def}, resulting in "abc123".  Message templates can be used while setting variables (AssignVariable with the Template element) or in many other places in Apigee policies.

If you want to connect to an external system, you can user ServiceCallout.  You can specify a template as the target URL.  Check the documentation. 

Hi @dchiesa1 

A follow up note after I had a call with an IDP team member, he mentioned that value of kid should come in as a header in the jwks url, looks then I am not setting it up correctly in my Assign Message. Could you please advise if this can be handled within the Assign Message or JavaScript policy.

yes - our messages crossed .  I addressed this above. 


@debjitd18 wrote:

A follow up note after I had a call with an IDP team member, he mentioned that value of kid should come in as a header in the jwks url,


I think what you're saying is that is how the system is designed. That is not a correct design.  I described this in one of the previous replies here. You should get the IDP team to fix it.  You should not pass the kid to the jWKS url.  That's not how it's supposed to work, and no public JWKS provider that I know of, uses that approach.  There's a good reason.  


@debjitd18 wrote:

Could you please advise if this can be handled within the Assign Message or JavaScript policy.


 

I don't know what you're asking. If you want to know whether an AssignMessage can assign a value into a header, then the answer is yes. But that's such a simple question, that I'm sure that's not what you're asking. 

 

Thanks Dino, we came up with approach after a consultation with our TAM, I will share the response with my EA and then it forward from there. Is it fine if I keep this thread open for a few days, as I am sure this discussion will go longer.

Regards,

Debjit

Sure, I will be interested to hear how the discussion goes. 

Hi Dino,

Thanks for your adivse, we have decided to not use JWKS as a way to verify the access tokens as our IDP team have raised some apprehensions over this. I am marking this as Accept as Solution for now.
We will be using the cache mechanism for now, I have some questions around it and we will start a new Topic, which I think is helpful even for others who might want to look up if they have similar queries.

Apologies for bugging you with so many questions back and forth.

Thanks,

Debjit