Apigee Edge - javascript policy to pass form parameters in body

Hello,

I have a shared flow which contains a JavaScript policy to call out to Microsoft Graph API to get a token.  I have setup my form parameters like below yet a fault is generated stating that the body must contain the following parameter "grant_type".  In reviewing the trace all the required form parameters are in the body.  Below is a snippet of the call making the request. Also, based upon the documentation I'm making the assumption this is how you would pass in form parameters. 

In using PostMan that was shared with me this curl works and returns an access token:

curl --location 'https://login.microsoftonline.com/[tenantid]/oauth2/v2.0/token ' \
--form 'scope="https://graph.microsoft.com/.default"' \
--form 'grant_type="client_credentials"' \
--form 'client_secret="mysecret"' \
--form 'client_id="myclient"'

JavaScript file snippet:

context.setVariable('request.header.Content-Type', 'multipart/form-data');

context.setVariable("request.formparam.scope", 'https://graph.microsoft.com/.default');
context.setVariable("request.formparam.grant_type", 'client_credentials');
context.setVariable("request.formparam.client_id", clientid);
context.setVariable("request.formparam.client_secret", clientsecret);

var authRequest = new Request(authUrl, authMethod, headers);
var authRequestExchange = httpClient.send(authRequest);

Appreciate any suggestions

 

Solved Solved
0 3 450
1 ACCEPTED SOLUTION

I understand what you're trying to do.

Here's why it isn't working. Setting the variable request.formparam.foo to a string, DOES set a formparam, on the message variable known in the apiproxy scope as request. That part is working just fine.

But in JS, when you call "new Request()" , you are creating a ... (wait for it) new request. A new request message. It is not the message known as "request" in apiproxy scope. The thing you are creating is a message of type request. But... It isn't the same request.

It's a little confusing because there are two naming scopes we're discussing here. Context variables are known in the message context, and you can refer to them in Conditions and in policy configurations in your API proxy. So you could use a thing like

 

<Condition>request.formparam.foo = "bar"</Condition>

 

..in a ProxyEndpoint, and Apigee would evaluate the formparam named foo on the message named "request" and test it against the value "bar". That variable name is known in proxy scope.

The JavaScript execution environment has a different scope. You can't refer to request.formparam.foo in JavaScript directly. First, it's not a valid variable name, but second (and more importantly) - that name is known in proxy scope, not in JS scope. You can use context.getVariable() to "slurp in" a copy of a variable that is known in proxy scope and make the value of that variable available in the JS callout execution scope. And then in JS, you can put that value in a named variable. The name in JS scope is obviously different than the name in proxy scope.

 

var myvar = context.getVariable('request.formparam.foo');
// myvar is now known in the scope of the JS execution
// myvar holds the same value as the context variable, but it is a copy. 

 

And that variable which is now known in JS, is not known in proxy scope. And remember, it's a copy. If you were to set myvar to something else, the proxy scope wouldn't "see" that change. To "export" something from JS scope into proxy scope, you use context.setVariable(). This is what I mean by "different scopes".

ok... now back to your issue. In your JS code, you are creating a new request.

 

var authRequest = new Request(...);
// you now have a new Request message stored in a variable in JS execution scope

 

authRequest is known in JS execution scope. It is not known in proxy scope. And it does not magically inherit or adopt the settings from a different request message, the one that you can affect with context.setVariable('request.formparam.foo');

If you want a form on that created-in-JS message, then ... you need to insert the form parameters yourself. How?

A couple ways I can think of. One is really easy, at least it seems so to me.

As far as I understand, you are using This Azure Graph API, which accepts a x-www-form-urlencoded string as a payload. A payload encoded that way looks like this:

 

param1=foo&param2=bar&param3=7

 

It's a set of key=value things, concatenated with ampersands. Simple. The values need to be encoded if they have portions that are non-ascii or require encoding (like colon or slash or ampersand).

In the case of the Graph API token endpoint, the payload should look like

 

client_id=535fb089-9ff3-47b6-9bfb-4f1264799865
&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default
&client_secret=qWgdYAmab0YSkuL1qKv5bPX
&grant_type=client_credentials

 

That's all on one line, and the order of those parameters doesn't matter. You can see the value for the "scope" parameter has some slashes in it, and those get encoded to %2F.

This is how I would do what you want from within a JS callout in Apigee:

 

var params = {
  client_id: clientid,
  client_secret: clientsecret,
  grant_type: "client_credentials",
  scope: "https://graph.microsoft.com/.default"
};
var url = 'https://login.microsoftonline.com/[tenantid]/oauth2/v2.0/token';
var headers = { 'content-type' : 'application/x-www-form-urlencoded' };
var formstring = Object.keys(params)
  .map(function (key) {
    return key + "=" + encodeURIComponent(params[key]);
  })
  .join("&");
var req = new Request(url, 'POST', headers, formstring);

httpClient.send(req, onComplete);

 

(and you have to define your own onComplete callback)

Last thing - just for clarity - you said you have a curl command that uses --form arguments, and it works. I believe you. But the --form param for curl tells curl to send a multipart form. That is not the same as x-www-form-urlencoded. Google it to learn more. The documentation for the Azure Graph API token dispensing endpoint says that it accepts x-www-form-urlencoded, not multipart. So while your curl command may work, it is not using the interface that is documented by Microsoft. A curl command that uses documented interface would use -d arguments, like this:

 

curl --location 'https://login.microsoftonline.com/[tenantid]/oauth2/v2.0/token' \
 -d 'scope=https://graph.microsoft.com/.default' \
 -d 'grant_type=client_credentials' \
 -d 'client_secret=mysecret' \
 -d 'client_id=myclient'

 

View solution in original post

3 REPLIES 3

I understand what you're trying to do.

Here's why it isn't working. Setting the variable request.formparam.foo to a string, DOES set a formparam, on the message variable known in the apiproxy scope as request. That part is working just fine.

But in JS, when you call "new Request()" , you are creating a ... (wait for it) new request. A new request message. It is not the message known as "request" in apiproxy scope. The thing you are creating is a message of type request. But... It isn't the same request.

It's a little confusing because there are two naming scopes we're discussing here. Context variables are known in the message context, and you can refer to them in Conditions and in policy configurations in your API proxy. So you could use a thing like

 

<Condition>request.formparam.foo = "bar"</Condition>

 

..in a ProxyEndpoint, and Apigee would evaluate the formparam named foo on the message named "request" and test it against the value "bar". That variable name is known in proxy scope.

The JavaScript execution environment has a different scope. You can't refer to request.formparam.foo in JavaScript directly. First, it's not a valid variable name, but second (and more importantly) - that name is known in proxy scope, not in JS scope. You can use context.getVariable() to "slurp in" a copy of a variable that is known in proxy scope and make the value of that variable available in the JS callout execution scope. And then in JS, you can put that value in a named variable. The name in JS scope is obviously different than the name in proxy scope.

 

var myvar = context.getVariable('request.formparam.foo');
// myvar is now known in the scope of the JS execution
// myvar holds the same value as the context variable, but it is a copy. 

 

And that variable which is now known in JS, is not known in proxy scope. And remember, it's a copy. If you were to set myvar to something else, the proxy scope wouldn't "see" that change. To "export" something from JS scope into proxy scope, you use context.setVariable(). This is what I mean by "different scopes".

ok... now back to your issue. In your JS code, you are creating a new request.

 

var authRequest = new Request(...);
// you now have a new Request message stored in a variable in JS execution scope

 

authRequest is known in JS execution scope. It is not known in proxy scope. And it does not magically inherit or adopt the settings from a different request message, the one that you can affect with context.setVariable('request.formparam.foo');

If you want a form on that created-in-JS message, then ... you need to insert the form parameters yourself. How?

A couple ways I can think of. One is really easy, at least it seems so to me.

As far as I understand, you are using This Azure Graph API, which accepts a x-www-form-urlencoded string as a payload. A payload encoded that way looks like this:

 

param1=foo&param2=bar&param3=7

 

It's a set of key=value things, concatenated with ampersands. Simple. The values need to be encoded if they have portions that are non-ascii or require encoding (like colon or slash or ampersand).

In the case of the Graph API token endpoint, the payload should look like

 

client_id=535fb089-9ff3-47b6-9bfb-4f1264799865
&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default
&client_secret=qWgdYAmab0YSkuL1qKv5bPX
&grant_type=client_credentials

 

That's all on one line, and the order of those parameters doesn't matter. You can see the value for the "scope" parameter has some slashes in it, and those get encoded to %2F.

This is how I would do what you want from within a JS callout in Apigee:

 

var params = {
  client_id: clientid,
  client_secret: clientsecret,
  grant_type: "client_credentials",
  scope: "https://graph.microsoft.com/.default"
};
var url = 'https://login.microsoftonline.com/[tenantid]/oauth2/v2.0/token';
var headers = { 'content-type' : 'application/x-www-form-urlencoded' };
var formstring = Object.keys(params)
  .map(function (key) {
    return key + "=" + encodeURIComponent(params[key]);
  })
  .join("&");
var req = new Request(url, 'POST', headers, formstring);

httpClient.send(req, onComplete);

 

(and you have to define your own onComplete callback)

Last thing - just for clarity - you said you have a curl command that uses --form arguments, and it works. I believe you. But the --form param for curl tells curl to send a multipart form. That is not the same as x-www-form-urlencoded. Google it to learn more. The documentation for the Azure Graph API token dispensing endpoint says that it accepts x-www-form-urlencoded, not multipart. So while your curl command may work, it is not using the interface that is documented by Microsoft. A curl command that uses documented interface would use -d arguments, like this:

 

curl --location 'https://login.microsoftonline.com/[tenantid]/oauth2/v2.0/token' \
 -d 'scope=https://graph.microsoft.com/.default' \
 -d 'grant_type=client_credentials' \
 -d 'client_secret=mysecret' \
 -d 'client_id=myclient'

 

Dino not sure if my original response came through or not yet wanted to thank you for your very detailed explanation and coding examples.

Glad to help out, Bill.