Reusable/Dynamic OauthV2 with VerifyAccessToken Operation

I'm trying to make the OauthV2 policy with VerifyAccessToken operation reusable by placing it in the Shared Flows along with the properties file residing in the API Proxy. I encountered an "Invalid Scope value" when I use the following variable 

 

<Scope>{propertyset.myProperty.oauth_scope}</Scope>

 

I saw the following community discussion, and variables are not allowed when VerifyAccessToken operation is used. I confirmed hardcoded the scope works. At first, I thought the issue that I had been experiencing was related to the usage of propertyset is not allowed in the shared flow. 

If I have the following scenarios (see the code below), how do I make this OauthV2 policy reusable for multiple API Proxies?

Proxy 1 - Proxy Endpoints' Preflow

 

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<OAuthV2 continueOnError="false" enabled="true" name="OAuth-VerifyAccessToken">
  <DisplayName>OAuth-VerifyAccessToken</DisplayName>
  <Properties/>
  <Attributes/>
  <ExternalAuthorization>false</ExternalAuthorization>
  <Operation>VerifyAccessToken</Operation>
  <SupportedGrantTypes/>
  <Scope>SomeScope1</Scope>
  <GenerateResponse enabled="true"/>
  <Tokens/>
  <RFCCompliantRequestResponse>true</RFCCompliantRequestResponse>
</OAuthV2>

 

Proxy 2 - Proxy Endpoints' Preflow

 

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<OAuthV2 continueOnError="false" enabled="true" name="OAuth-VerifyAccessToken">
  <DisplayName>OAuth-VerifyAccessToken</DisplayName>
  <Properties/>
  <Attributes/>
  <ExternalAuthorization>false</ExternalAuthorization>
  <Operation>VerifyAccessToken</Operation>
  <SupportedGrantTypes/>
  <Scope>AnotherScope2</Scope>
  <GenerateResponse enabled="true"/>
  <Tokens/>
  <RFCCompliantRequestResponse>true</RFCCompliantRequestResponse>
</OAuthV2>

 

What is the best practice to handle dynamic scope? Thanks!

 

Solved Solved
2 5 300
1 ACCEPTED SOLUTION

I agree with you about prioritizing long-term maintainability and understandability. That's the reason I asked the question in the very beginning. Anyway, I was able to push the deadline so I could do more testing. Thank you for your javascript solution. I have it working but with some modifications.

When I use the RaiseFault, the SharedFlow's UI has a script logo with ... I followed the documentation and the 3 dots won’t go away. As a result, it bypassed the FaultRule. Instead of throwing out an error in javascript, I leveraged setting fault.name and apply condition in the RaiseFault policy. Second, I replaced the lookup logic with the traditional for loop. The Array.prototype.every takes about 7ms and the traditional loop takes about 1ms. So here are the modified codes:

 

    var tokenScopes = context.getVariable('scope').split(' ');
    var requiredScopes = context.getVariable('propertyset.settings.required_oauth_scope').split(' ');
	var ok = true; // Inverting the logic using false as a default value takes longer time than true
	// Using traditional for loop to improve performance
	for (var i = 0; i < requiredScopes.length; i++) {
	    if (tokenScopes.indexOf(requiredScopes[i]) < 0) {
	        ok = false;
	        break;
	    }
	}
    if (!ok) {
      context.setVariable("fault.name", "InvalidScope");
    }
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<SharedFlow name="PreProxyFlow">
<!--
: redacted steps
:
-->
    <Step>
        <Name>OAuth-VerifyAccessToken</Name>
    </Step>
    <Step>
        <Name>JS-Check-Token-Scope</Name>
    </Step>
    <Step>
        <Name>RF-InvalidScope</Name>
        <Condition>fault.name == "InsufficientScope"</Condition>
    </Step>
<!--
: redacted steps
:
-->
</SharedFlow>	
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<RaiseFault continueOnError="false" enabled="true" name="RF-InvalidScope">
    <DisplayName>RF-InvalidScope</DisplayName>
    <Properties/>
    <FaultResponse>
        <Set>
            <Headers/>
            <Payload contentType="application/json">
                {
                "errorCode": "ERR403",
                "errorMessage": "Invalid scope value"
                }
            </Payload>
            <StatusCode>403</StatusCode>
            <ReasonPhrase>Invalid scope</ReasonPhrase>
        </Set>
    </FaultResponse>
    <IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
</RaiseFault>

 

View solution in original post

5 REPLIES 5

What is the best practice to handle dynamic scope? Thanks!

If you do not want to "hard code" the required scope within the VerifyAccessToken policy, then you need to check it outside the policy, after it completes. I suggest that you add a JavaScript policy, attached directly after the VerifyAccessToken policy. The JS should examine the "scope" variable which will be set by OAuthV2/VerifyAccessToken, and compare it to the required scope (or scopes), which may be stipulated in a context variable. The scope variable will be a space separated string. Supposing your required scopes is also a space-separated string, this should work:

 

<Javascript name='JS-Check-Token-Scope'>
  <Source>
    var tokenScopes = context.getVariable('scope').split(' ');
    var requiredScopes = context.getVariable('propertyset.settings.required_oauth_scope').split(' ');
    var ok = requiredScopes.every(function (required) {
      return tokenScopes.indexOf(required) >= 0;
    });
    if (!ok) {
      throw new Error('Invalid scope');
    }
  </Source>
</Javascript>

 

Throwing an Error from within a JS callout will cause the proxy to enter fault state. The default error message in this case will be something like this:

 

{
  "fault": {
    "faultstring": "Execution of JS-Check-Token-Scope failed with error: Exception thrown from Javascript &colon; Error: Invalid scope (JS_Check_Token_Scope#8)",
    "detail": {
      "errorcode": "steps.javascript.ScriptExecutionFailed"
    }
  }
}

 

...and the status will be 500. Neither the message nor the status is probably what you want. You know what to do: handle the fault with a FaultRule and set whatever is the appropriate fault message and status to be sent back to the client.  Maybe the FaultRule looks like this: 

    <FaultRule name='token-scope-check'>
      <Step>
        <Name>AM-Response-for-Token-Scope-Fault</Name>
      </Step>
      <Condition>javascript.JS-Check-Token-Scope.failed = true</Condition>
    </FaultRule>

If you'd like to be very friendly, you could modify the JS logic that checks for scopes, to also emit a list of the scopes that are required, but missing.  Then the message that you emit in the FaultRule might say....

{
  "status" : "error",
  "message" : "insufficient scope. Your token did not include one or more of the required scopes.", 
  "details" : "missing scope(s): scope1 scope2"
}

@dchiesa1 I have follow-up questions:

  • Will the Javascript and FaultRule policies increase the API latency? If we proxy a legacy API, will it contribute to the slowness?
  • When Javascript policy is used to validate the scope, what is the role of Oauth policy? Is it for verifying the token? If we handle token verification on the Javascript as well, can the Oauth policy be dropped? 
  • Is it for security reasons that variables are not allowed in the VerifyAccessToken operation?
  • When I specify the following 

 

<Scope ref="propertyset.myProperty.oauth_api_scope" />​

in the VerifyAccessToken operation, it passes but the policy doesn't seem to read from the properties file.

 

Based on another post, most likely we won't see a dynamic scope in the future. I have bundled CORS, VerifyAccessToken, and Quota policies into a shared flow. Moving the hardcoded Oauth policy into the proxy level makes the flow out of order. I am thinking of dropping the shared flow usage rather than dealing with latency issues.

  • Will the Javascript and FaultRule policies increase the API latency? If we proxy a legacy API, will it contribute to the slowness?

You are thinking about things correctly. Executing a JS policy after the OAuthV2/VerifyAccessToken will add some execution time to the API proxy that Apigee runs for you. The FaultRule also will imply some extra execution time.  But how much?  Will it be relevant? Here's the way I think about it. Of all of those policy steps, VerifyAccessToken will be "the most expensive". The reason is that VAT will perform a lookup of that token, in the persistent key store that is part of Apigee. That means I/O, reading a datastore. And even with solid state storage (SSDs) and network attached storage and superfast Google Cloud networks that are used by Apigee X, there is some additional latency for that data query and read. The good news is in two parts. #1, the I/O will be pretty fast anyway. Figure 2-5ms of additional latency on a VAT call. And #2, Apigee is built with caching, and specifically there is caching in the VerifyAccessToken operation, so that I/O cost is incurred only the first time a token is presented. Subsequent VAT calls with the same token use locally cached data and there's no I/O latency. It's an in-memory read and it's much much less than 1ms. 

OK. now moving to JS and FaultRule with AssignMessage. Both of these are in-memory operations. There's no I/O, so no network cost, no chance of contention at the database. These will incur "some latency" but the latency we're talking about here is <1ms.

Now compare this to whatever service you are protecting behind your Apigee proxy. You used the phrase "legacy service". That service is probably MUCH slower than whatever you are doing in Apigee. So I'd say, the entire cost of VerifyAccessToken + JavaScript , which happens in the happy path where the scope is sufficient, will be much much less than the latency cost of the backend service. One or two orders of magnitude difference.  Often I See API proxy latency with the entire burden of all policies, amount to ~8-10ms (with a cold token cache), and the backend system is 200ms. Will the "cost" of Apigee be detectable? Probably not. With a warm token cache the API Proxy latency will be less. So in practice, it just isn't large enough to matter. 

  • When Javascript policy is used to validate the scope, what is the role of Oauth policy? Is it for verifying the token? If we handle token verification on the Javascript as well, can the Oauth policy be dropped?

Yes, the OAuthV2 policy will be verfying that the token is valid, issued by Apigee, not expired, not revoked, and appropriate for the given API proxy (the API product check). For more on that last part, see this tutorial. The JS policy follows that up with a dynamic check on the scope attached to the token, vs the scope required for the given request.

It is not possible to use JS to "verify the access token". That requires a keystore token lookup, and that is done by the OAuthV2 policy. I think the reason you are asking this is because you imagine it might be more efficient to do everything in one policy step. Again, considering performance is a good instinct, but the latency delta you'll be dealing with here is so small as to be irrelevant.

  • Is it for security reasons that variables are not allowed in the VerifyAccessToken operation?

I don't know the reason, the policy step was just designed that way. But thinking about it, I believe. it probably IS slightly more secure to have a static scope requirement. Slightly less complicated to configure. Still, it's an interesting feature request.

  • When I specify ... in the VerifyAccessToken operation, it passes but the policy doesn't seem to read from the properties file.

Sadly the policy configuration validation logic does not validate that unsupported attribute there. That feels like a bug to me.

Also that errant (unsupported) attribute should be checked in Apigeelint as well. It's an open request to have apigeelint perform a schema-based validation of policy config. As of today, apigeelint does not yet perform that check.

I have bundled CORS, VerifyAccessToken, and Quota policies into a shared flow. Moving the hardcoded Oauth policy into the proxy level makes the flow out of order. I am thinking of dropping the shared flow usage rather than dealing with latency issues.

I think dropping the sharedflow is probably a bad idea. It sounds like you have assumed there will be "latency issues" but have not measured anything. I would be careful abou drawing conclusions like that, if I were you. As an architect, my first priority when building systems is long-term maintainability and understandability. These are security controls so I would want that to be pretty tight, simple to understand, and easy to govern and maintain. To my mind, that recommends a shared flow. Moving the OAuthV2 policy into each proxy will "work" but it may cause other downsides that just do not justify the "latency optimization" that you are considering here.

Measure it and see.

I agree with you about prioritizing long-term maintainability and understandability. That's the reason I asked the question in the very beginning. Anyway, I was able to push the deadline so I could do more testing. Thank you for your javascript solution. I have it working but with some modifications.

When I use the RaiseFault, the SharedFlow's UI has a script logo with ... I followed the documentation and the 3 dots won’t go away. As a result, it bypassed the FaultRule. Instead of throwing out an error in javascript, I leveraged setting fault.name and apply condition in the RaiseFault policy. Second, I replaced the lookup logic with the traditional for loop. The Array.prototype.every takes about 7ms and the traditional loop takes about 1ms. So here are the modified codes:

 

    var tokenScopes = context.getVariable('scope').split(' ');
    var requiredScopes = context.getVariable('propertyset.settings.required_oauth_scope').split(' ');
	var ok = true; // Inverting the logic using false as a default value takes longer time than true
	// Using traditional for loop to improve performance
	for (var i = 0; i < requiredScopes.length; i++) {
	    if (tokenScopes.indexOf(requiredScopes[i]) < 0) {
	        ok = false;
	        break;
	    }
	}
    if (!ok) {
      context.setVariable("fault.name", "InvalidScope");
    }
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<SharedFlow name="PreProxyFlow">
<!--
: redacted steps
:
-->
    <Step>
        <Name>OAuth-VerifyAccessToken</Name>
    </Step>
    <Step>
        <Name>JS-Check-Token-Scope</Name>
    </Step>
    <Step>
        <Name>RF-InvalidScope</Name>
        <Condition>fault.name == "InsufficientScope"</Condition>
    </Step>
<!--
: redacted steps
:
-->
</SharedFlow>	
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<RaiseFault continueOnError="false" enabled="true" name="RF-InvalidScope">
    <DisplayName>RF-InvalidScope</DisplayName>
    <Properties/>
    <FaultResponse>
        <Set>
            <Headers/>
            <Payload contentType="application/json">
                {
                "errorCode": "ERR403",
                "errorMessage": "Invalid scope value"
                }
            </Payload>
            <StatusCode>403</StatusCode>
            <ReasonPhrase>Invalid scope</ReasonPhrase>
        </Set>
    </FaultResponse>
    <IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
</RaiseFault>

 

Glad to hear you got it sorted out! 

Thanks for posting your solution.