Copy link to clipboard
Copied
Hi All,
I'm struggling to connect our Coldfusion 11 (x64) to the oauth2 methods used by Exact Online accounting API s.
Every call I make always returns status 400 - Bad Request. No additional info is supplied, nor found on their development site.
I had contact with their helpdesk, but as they have no knowledge of Java/Coldfusion, they just send me a .Net HTTP call example:
strTokenBody = "code=" & strCode
strTokenBody = strTokenBody & "&client_id=" & strClientID
strTokenBody = strTokenBody & "&client_secret=" & strClientsecret
strTokenBody = strTokenBody & "&redirect_uri=" & strCallbackURL
strTokenBody = strTokenBody & "&grant_type=authorization_code"
byteArray = Encoding.ASCII.GetBytes(strTokenBody)
uri = strBaseURL & "/api/oauth2/token"
Dim req As WebRequest = WebRequest.Create(uri)
req.ContentType = "application/x-www-form-urlencoded"
req.Method = "POST"
req.ContentLength = byteArray.Length
Dim stream As Stream = req.GetRequestStream()
stream.Write(byteArray, 0, byteArray.Length)
stream.Close()
The credentials I have do check out and work with the above code example, according to the Helpdesk employee. He then stressed that the HTTP body should be a byte array of an ASCII encoded string, which is a numeration of all parameters to pass along.
I tried following approaches:
- I checked out the PHP code example the Exact has put up, but I cannot determine which setting I'm missing.
- changed call URL to a local template to dump the request (and headers) and I see that all parameters are there and display correctly.
Can anybody point me in the right direction on how to proceed in this?
Thanks for reading so far,
Regards Bert.
CFC I made :
<cfcomponent>
<cfscript>
// define all paths used for interfacing
VARIABLES.Exact = {
URL = {
main = "https://start.exactonline.be/api", // main access URL
authenticate = "/oauth2/auth", // for optaining an access code
token = "/oauth2/token", // to get / refresh an access token
user = "/v1/current/Me" // get current user data
},
access = {
client = {
name = "MyName", // from API access token
id = "pre-generated API Id ", // from API access token
secret = "pre-generated API secret" // from API access token
},
redirectURI = "My Site URL", forceLogin = 0,
code = "Optained code through manual login form"
},
settings = {
grant_authorization_code = "authorization_code",
grant_refresh_token = "refresh_token",
response_type_code = "code"
}
};
// define the access token used in all API calls. Must be optained before starting any work.
THIS.token = {
access = "", // access key used for the session
type = "", // type of token
expiresIn = 600, // seconds the token will be life, default = 10 min
refresh = "" // handle used to refresh current
};
</cfscript>
<cffunction name="CreateASCIIByteArray" access="private" returntype="Any" output="false" hint="Converts a ColdFusion string to an ASCII Java byte array.">
<cfargument name="str" type="string" required="true" hint="The string to get the byte array for.">
<cfscript>
return ToBinary( toBase64( ARGUMENTS.str ) );
</cfscript>
</cffunction>
<cffunction name="GetToken" access="public" returntype="any" output="false" hint="">
<cfscript>
LOCAL.URL = VARIABLES.Exact.URL.Main & VARIABLES.Exact.URL.token;
LOCAL.result = DoHTTPRequest(
method = "post",
url = LOCAL.URL,
params = [
NewRequestParam(type="formField", name="client_id", value=VARIABLES.Exact.access.client.id),
NewRequestParam(type="formField", name="client_secret", value=VARIABLES.Exact.access.client.secret),
NewRequestParam(type="formField", name="redirect_uri", value=VARIABLES.Exact.access.redirectURI),
NewRequestParam(type="formField", name="grant_type", value=VARIABLES.Exact.settings.grant_authorization_code),
NewRequestParam(type="formField", name="code", value=VARIABLES.Exact.access.code)
]
);
return LOCAL.result;
</cfscript>
</cffunction>
<cffunction name="NewRequestParam" access="private" returntype="struct" output="false" hint="Creates a new request parameter">
<cfargument name="type" type="string" required="false" default="URL" hint="[header,CGI,body,XML,file,URL,formField,cookie]">
<cfargument name="name" type="string" required="true" hint="Name of the parameter">
<cfargument name="value" type="string" required="false" default="" hint="value for the paramter">
<cfreturn ARGUMENTS>
</cffunction>
<cffunction name="DoHTTPRequest" access="private" returntype="Any" output="true" description="performs an HTTP request and returns the result as a structure.">
<cfargument name="method" type="string" required="false" default="get" hint="[get,post,delete]">
<cfargument name="url" type="string" required="true" hint="the complete URL to call">
<cfargument name="params" type="array" required="false" default="#ArrayNew(1)#" hint="parameter list to send along the request, created with NewRequestParam().">
<cfscript>
// launch the http request
LOCAL.httpRequest = new http(
method = uCase(ARGUMENTS.method),
charset = "utf-8",
url = ARGUMENTS.url,
compression = "none"
);
if (ARGUMENTS.method == "post") {
LOCAL.httpRequest.setMultipartType("form-data");
}
// add params to request body
LOCAL.body = [];
LOCAL.paramCount = arrayLen(ARGUMENTS.params);
for (LOCAL.index = 1; LOCAL.index <= LOCAL.paramCount; LOCAL.index++) {
LOCAL.currentParam = ARGUMENTS.params[LOCAL.index];
if (LOCAL.currentParam.type == "header") {
LOCAL.httpRequest.addParam( argumentCollection = LOCAL.currentParam );
} else {
arrayAppend(
LOCAL.body,
LOCAL.currentParam.name & "=" & LOCAL.currentParam.value
);
}
} // end-for
LOCAL.httpRequest.addParam(
type = "body",
value = CreateASCIIByteArray( str = arrayToList(LOCAL.body, "&") )
);
// fire request
try {
LOCAL.http = LOCAL.httpRequest.send().getPrefix();
// return serialized filecontent when able.
if ( isJSON(LOCAL.http.FileContent) ) {
LOCAL.return = deserializeJSON(LOCAL.http.Filecontent);
LOCAL.return.statusCode = listFirst(LOCAL.http.statusCode," "); // status code is returned as "200 OK" instead of the number.
} else {
LOCAL.return = {
statusCode = 500,
message = LOCAL.http.Filecontent,
request = LOCAL.httpRequest,
result = LOCAL.http
};
} // end-if
} catch (any except) {
LOCAL.return = {
statusCode = 500,
message = except.type & " : " & except.message,
URL = ARGUMENTS.url,
params = ARGUMENTS.params,
fullExcept = except
};
} // end-catch
return LOCAL.return;
</cfscript>
</cffunction>
</cfcomponent>
I finally manage to get it to work, it appears Exact Online needs some valid userAgent sent along! CF sends the value 'Coldfusion' as user agent string per default.
The moment I put in userAgent = "Mozilla/5.0" into my cfhttp, I got my refresh token.
To be on the safe side, I also added the compression = "none" parameter to my cfhttp.
Regards, Bert.
Copy link to clipboard
Copied
I haven't gone into the details of the code. But I have two suggestions:
1) The functions are quite linked. Debug them piecewise. Take doHTTPRequest, for example. It is complex and assembles many different parts. We would wish to know what kind of data it is passing in the request. Therefore put a line similar to this one, just before the return statement:
<cfdump output="c:\temp\exactTest.html" var='#local#' format="html">
Then examine the parameters. Are they what you expect?
2) In ColdFusion's interaction with external systems such as Exact, structs are usually more interoperable than arrays. So, convert the parameter Local.body to a struct, and see what happens.
Copy link to clipboard
Copied
Hi BKBK,
Thanks for your reply.
The code works when calling a local test URL and performing a cfdump on that test template. I gather it's not a syntax issue, but rather something missing or the way it works.
The body message is a byte-array value, so not an array or a string. But I also tested it without any body data, or as a plain string. Whatever I do, it always returns a '400 Bad Request'.
I just tried the chrome app 'Postman'. Using that interface, I can successfully log in and receive an access token.
Copy link to clipboard
Copied
I finally manage to get it to work, it appears Exact Online needs some valid userAgent sent along! CF sends the value 'Coldfusion' as user agent string per default.
The moment I put in userAgent = "Mozilla/5.0" into my cfhttp, I got my refresh token.
To be on the safe side, I also added the compression = "none" parameter to my cfhttp.
Regards, Bert.