OAuthV2 Policy - GenerateJWTAccessToken - Support for JWKS based validation and key rotation

Hey folks,

We're attempting to use the GenerateJWTAccessToken operation of the OAuthV2 policy and we wish to validate the generated JWT using a VerifyJWT policy such as the following:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<VerifyJWT name="jwt-verify-token" async="false" continueOnError="false" enabled="true">
  <DisplayName>jwt-verify-token</DisplayName>
  <Algorithm>RS256</Algorithm>
  <PublicKey>
    <JWKS uriRef="jwt.jwks_url"/>
  </PublicKey>
</VerifyJWT>

The reasons we want to do that instead of using the VerifyJWTAccessToken operation of the OAuthV2 policy are:

- As we migrate our token generation from our current system to Apigee, we want to have this policy to be able to validate tokens generated with both our older system and with Apigee OAuthV2 policy

- Similarly to the above, we'd eventually want to rotate our signing key, which could imply temporarily supporting tokens signed with our old key along with our new key

- We'd want to expose our JWKS as a /certs endpoint, which could be reached from other systems that may need to validate these JWTs

The VerifyJWT fails to find a matching public key despite the JWKS containing a record matching the key we used, reason apparently being that the OAuthV2 generated JWT does not containing the kid header claim (tokens generated with the GenerateJWT policy with the mentioned claim and the same private key worked)

Being able to configure a kid for JWTs generated with the OAuthV2 policy could solve our issue, but we weren't able to find a way to do so. Is it possible?

We're not too convinced about using GenerateJWT to generate our tokens as we lose the default OAuth2 behavior for the different grant types and also lose things like the automatic scope assignment based on the scopes we configure for our API Products and Developer App Keys

So, for JWTs generated with the OAuthV2 Policy - GenerateJWTAccessToken operation, what would be a recommended way to support key rotation? Is there a way to enable JWKS based validation that works with these tokens in combination with externally generated tokens? 

Thanks in advance for your advice

 

0 2 223
2 REPLIES 2

One alternative option would be to use the OAuthV2 policy with the GenerateAccessToken operation, but, in your case, use the ExternalAccessToken element. You can generate the JWT using the GenerateJWT policy before OAuthV2 policy. Then, the JWT token generated from this policy can be passed in as a variable to ExternalAccessToken. This may be a more suitable approach given your requirements. Pls investigate to see if this works for you and report back.

I'll look into your inquiry about configuring kid on the GenerateJWTAccessToken operation in the meantime.

Thanks @apickelsimer,

We gave the ExternalAccessToken approach a try and it seems feasible. It's a significantly more complex implementation, specially as we had to replicate the scope assignment logic to set the proper scope claim in our custom JWT. We were considering something more or less like this:

1. Retrieving the scope data of the incoming developer app key with an AccessEntity policy

 

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<AccessEntity name="ae-get-key-scopes">
  <EntityType value="consumerkey_scope"/>
  <EntityIdentifier ref="request.formparam.client_id" type="consumerkey"/>
</AccessEntity>

2. Extracting the XML array the AccessEntity got us for further processing. There may be better ways around it, but things like the ExtractVariable policy don't seem to handle arrays that well

 

 

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<XMLToJSON continueOnError="false" enabled="true" name="x2j-extract-scope">
  <Source>AccessEntity.ae-get-key-scopes</Source>
  <OutputVariable>dev-app-key.scopes</OutputVariable>
  <Options>
    <TreatAsArray>
      <Path unwrap="true">Scopes/Scope</Path>
    </TreatAsArray>
  </Options>
</XMLToJSON>
​

 

3. Using a Javascript policy to determine the scopes to assign based on the developer app key scopes and the requested scopes

 

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Javascript continueOnError="false" enabled="true" timeLimit="200" name="js-determine-scopes">
  <DisplayName>js-determine-scopes</DisplayName>
  <Properties/>
  <ResourceURL>jsc://determine-scopes.js</ResourceURL>
</Javascript>
​
var devAppKeyScopesJson = context.getVariable("dev-app-key.scopes");
var assignedScopes = "";
if (devAppKeyScopesJson) {
    var devAppKeyScopes = JSON.parse(devAppKeyScopesJson).Scopes || [];
    var inputScopesString = context.getVariable("request.formparam.scope");
    var inputScopes = inputScopesString ? inputScopesString.trim().split(" ") : [];
    if (inputScopes.length > 0) {
        var assignedScopeArray = inputScopes.filter(scope => devAppKeyScopes.includes(scope));
        assignedScopes = assignedScopeArray.join(" ");
    } else {
        assignedScopes = devAppKeyScopes.join(" ");    
    }
}
context.setVariable("assigned_scopes", assignedScopes);

 

4. Generate the custom JWT with the GenerateJWT policy, this has the JWKS compatible key id and the scopes determined by the prior steps

 

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<GenerateJWT name="gjwt-access-token">
  <Type>Signed</Type>
  <Algorithm>RS256</Algorithm>
  <IgnoreUnresolvedVariables>false</IgnoreUnresolvedVariables>
  <PrivateKey>
    <Value ref="private.rsa-jwt-privatekey"/>
    <Id>2a606dfcf5e609618bcb031349257d57</Id>
  </PrivateKey>
  <Subject>apigee-seattle-hatrack-montage</Subject>
  <Issuer>urn://apigee-JWT-policy-test</Issuer>
  <Audience>urn://c60511c0-12a2-473c-80fd-42528eb65a6a</Audience>
  <ExpiresIn>60m</ExpiresIn>
  <AdditionalClaims>
    <Claim name="scope" ref="assigned_scopes"/>
  </AdditionalClaims>
  <OutputVariable>custom_jwt</OutputVariable>
</GenerateJWT>

 

5. Use the GenerateAccessToken OAuthV2 policy with the ExternalAccessToken property set to the custom JWT

 

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<OAuthV2 name="oauthv2-custom-jwt">
  <DisplayName>oauthv2-custom-jwt</DisplayName>
  <ExternalAccessToken>custom_jwt</ExternalAccessToken>
  <ExternalAuthorization>false</ExternalAuthorization>
  <Operation>GenerateAccessToken</Operation>
  <GenerateResponse enabled="true"/>
  <ReuseRefreshToken>false</ReuseRefreshToken>
  <StoreToken>true</StoreToken>
  <Scope>assigned_scopes</Scope>
  <SupportedGrantTypes>
    <GrantType>client_credentials</GrantType>
    <GrantType>password</GrantType>
  </SupportedGrantTypes>
  <ExpiresIn>3600000</ExpiresIn>
</OAuthV2>
​

 

Further complications come whenever dealing with the refresh_token grant type, as we'll need the originally requested scope set to be present in the refreshed access token. These scopes will be set into the context during the execution of the RefreshAccessToken operation of the OAuthV2 policy, but we'll need them during the prior step that generates our custom access token. We may end up having a custom JWT for our refresh token, and have that refresh token transport the scopes so they're accessible to our custom JWT generation step.

Thoughts?

Also, any luck regarding the possibility to configure the kid for the GenerateJWTAccessToken?