Proper way of adding body to cfhttp - coldfusion

I want to make an API request using lucee/coldfusion.
I setup my token request like this:
cfhttp(
url="[myurl]"
method="POST"
result="token"
) {
cfhttpparam(type="header" name="host" value="[url]");
cfhttpparam(type="body" name="client_id" value="[id]");
cfhttpparam(type="body" name="client_secret" value="[secret]");
cfhttpparam(type="body" name="grant_type" value="[credentials]");
cfhttpparam(type="body" name="scope" value="[url]");
};
But the error message tells me that "grant_type" needs to be included, so it seems like my body here is not sent properly.
Can someone help me out?
Edit:
I also tried this:
var body = {
"host": "[url]",
"client_id": "[id]",
"client_secret": "[secret]",
"grant_type": "[credentials]",
"scope": "[url]"
}
// Token
cfhttp(
url="[url]"
method="POST"
result="token"
) {
cfhttpparam(type="header" name="host" value="[url]");
cfhttpparam(type="body" value="#body.toJson()#");
};

Your second attempt is the right way to do it, but you need to add a Content-Type header specifying the body as JSON:
body = {
"host": "[url]",
"client_id": "[id]",
"client_secret": "[secret]",
"grant_type": "[credentials]",
"scope": "[url]"
}
// Token
cfhttp(
url="[url]"
method="POST"
result="token"
) {
cfhttpparam(type="header", name="host", value="[url]");
cfhttpparam(type="header", name="Content-Type", value="application/json");
cfhttpparam(type="body", value="#body.toJson()#");
}
Obviously the body JSON also needs to match whatever the API is expecting.

Related

Getting 403 Forbidden when trying to upload file to AWS S3 with presigned post using Boto3 (Django + Javascript)

I've tried researching other threads here on SO and other forums, but still can't overcome this issue. I'm generating a presigned post to S3 and trying to upload a file to it using these headers, but getting a 403: Forbidden.
Permissions
The IAM user loaded in with Boto3 has permissions to list, read and write to S3.
CORS
CORS from all origins and all headers are allowed
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"GET",
"HEAD",
"POST",
"PUT"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": []
}
]
The code
The code is based on Python in Django as well as Javascript. This is the logic:
First the file is retrieved from the HTML, and used to call a function for retrieving the signed URL.
(function () {
document.getElementById("file-input").onchange = function () {
let files = document.getElementById("file-input").files;
let file = files[0];
Object.defineProperty(file, "name", {
writeable: true,
value: `${uuidv4()}.pdf`
})
if (!file) {
return alert("No file selected");
}
getSignedRequest(file);
}
})();
Then a GET request is sent to retrieve the signed URL, using a Django view (described in the next section after this one)
function getSignedRequest(file) {
var xhr = new XMLHttpRequest();
xhr.open("GET", "/sign_s3?file_name=" + file.name + "&file_type=" + file.type)
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
let response = JSON.parse(xhr.responseText)
uploadFile(file, response.data, response.url)
}
else {
alert("Could not get signed URL")
}
}
};
xhr.send()
}
The Django view generating the signed URL
def Sign_s3(request):
S3_BUCKET = os.environ.get("BUCKET_NAME")
if (request.method == "GET"):
file_name = request.GET.get('file_name')
file_type = request.GET.get('file_type')
s3 = boto3.client('s3', config = boto3.session.Config(signature_version = 's3v4'))
presigned_post = s3.generate_presigned_post(
Bucket = S3_BUCKET,
Key = file_name,
Fields = {"acl": "public-read", "Content-Type": file_type},
Conditions = [
{"acl": "public-read"},
{"Content-Type": file_type}
],
ExpiresIn = 3600
)
return JsonResponse({
"data": presigned_post,
"url": "https://%s.s3.amazonaws.com/%s" % (S3_BUCKET, file_name)
})
Finally the file should be uploaded to the bucket (this is where I'm getting the 403 error)
function uploadFile(file, s3Data, url) {
let xhr = new XMLHttpRequest();
xhr.open("POST", s3Data.url)
let postData = new FormData()
for (key in s3Data.fields) {
postData.append(key, s3Data.fields[key])
}
postData.append("file", file)
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200 || xhr.status === 204) {
document.getElementById("cv-url").value = url
}
else {
alert("Could not upload file")
}
}
};
xhr.send(postData)
}
The network request
This is how the network request looks in the browser
#jellycsc helped me. I had to open up the BlockPublicAcl option for the bucket for it to work.
The URL that you should be using in the upload is supposed to be the one that the presigned response has. Don't just upload whatever url you want.
Update your response to be:
return JsonResponse({
"data": presigned_post,
"url": presigned_post.url
})
Specifically the url you are using looks like:
https://BUCKTET_NAME.s3.amazonaws.com/KEY_PATH
When it should look like:
https://s3.REGION.amazonaws.com/BUCKET_NAME
However looking at your code this is what it should be doing, but your screen shot from inspector says otherwise. Why does the url in the network request NOT match the url that was returned by the create_presigned_post request?

Set multiple cookies in google app script with UrlFetchApp

I am using the following script to access a web page that requires user to be logged-in. The script works up to the point where I retrieve the login cookies I need help with using the cookies in the last part of the following code.
function login(strUsername, strPassword) {
//request Page
var loginPage = UrlFetchApp.fetch("https://login.url.org/");
//strip out "authState" value from response
var sessionDetails = loginPage.getContentText()
var searchString = sessionDetails.indexOf('"authState":"');
var newStringRight = sessionDetails.substring(searchString+13, searchString+6000);
var authState = newStringRight.substring(0, newStringRight.indexOf('"'));
//Logger.log(authState);
//set payload
var payload =
{"authState":authState,
"username":strUsername,
"password":strPassword
};
var options =
{
"method" : "post",
"payload" : JSON.stringify(payload),
"followRedirects" : false,
muteHttpExceptions: true,
"contentType": "application/json"
};
//Logger.log(options);
var loginResponse = UrlFetchApp.fetch("https://login.url.com/api/authenticate/credentials", options);
var loginHtml = loginResponse.getContentText();
Logger.log(loginHtml);
// Log has the following response
//{"output":{"method":"SUCCESS","cookies":{"ObSSOCookie":"xxx","ChurchSSO":"yyy"}}}
if (loginHtml.indexOf("SUCCESS")>0){
Browser.msgBox('Pass', 'Successfully loged in.', Browser.Buttons.OK);
//strip out cookies
searchStringStart = loginHtml.indexOf('{"ObSSOCookie":"');
searchStringEnd = loginHtml.indexOf('"}}');
var Cookie = loginHtml.substring(searchStringStart, searchStringEnd+2);
Logger.log(Cookie);
/*
Logger.log(cookie) returns
{"ObSSOCookie":"xxxx","ChurchSSO":"yyyy"}
*/
////////////////////////////Works up to here/////////////////////////////////////////
var options =
{
"headers" : {"cookies" : Cookie},
muteHttpExceptions: true,
"contentType": "application/json"
};
var adminpage= UrlFetchApp.fetch("https://url.com/admin",options );
Logger.log(adminpage);
/*
<h1>Access Denied</h1>
<p>You are not authorized to access this page.</p>
<p>Retry
*/
}else{
Browser.msgBox('Fail', 'Sign in failed. Please try again.', Browser.Buttons.OK);
}
}
I think it may have to do with the fact that I am trying to use multiple cookies {"ObSSOCookie":"xxxx","ChurchSSO":"yyyy"}not sure how to approperatly pass them into the new page call. Any help would be great
Cookies should be sent inside a single Cookie header separated by a ;(semicolon and a space):
Cookie: name1=value1; name2=value2;
Snippet:
var options =
{
"headers" : {"Cookie" : "ObSSOCookie=xxxx; ChurchSSO=yyyy"}},
};
Reference:
HTTP Cookies

Check for null values in request body using Wiremock

I am trying to setup a wiremock stub that will return a 400 error if any field has a null value in the json payload. Basically to simulate a Bad Request. I've been trying with a regex that matches any lowercase string for the json key but it doesn't seem to like it. I can't find any examples of what I want online so not sure if it's even possible.
My Bad Request body:
{
"cat": null,
"dog": {
"id": 1344
},
"horse": {
"id": 1
},
"fish": 1
}
My Stub:
wireMockServer.stubFor(post(urlEqualTo("/sample-api"))
.withRequestBody(matchingJsonPath("$.^[a-z]*", equalTo(null)))
.willReturn(aResponse()
.withStatus(400)
.withHeader("Content-Type", "application/json")))
In this example I would expect the stub to match "cat" as the value of it is null. This isn't the case. Can anyone tell me what I'm doing wrong?
In the WireMock documentation on Request Matching the section on JSON Path matching. In the source code there is a reference to com.jayway.jsonpath.JsonPath library used. The build.gradle refers to version 2.4.0. The documentation for the Jayway JSON Path library can be found on their Github project page. There is a good, but by no means perfect online evaluator here.
The WireMock documentation only shows support for Regular Expression for the node values in the form of the "matchesJsonPath". In the Jayway documenatation there is an online example: $..book[?(#.author =~ /.*REES/i)]. For this reason the only approach is to name all the nodes that are not allowed to be null.
In the below example mapping all the mentioned nodes will be tested, regardless of their depth (see #id). This mapping will not trigger if all the mentioned nodes are not null, but some unmentioned ones are.
{
"request": {
"urlPattern": "/sample-api",
"method": "GET",
"bodyPatterns" : [ {
"matchesJsonPath" : "$..[?(#.cat == null || #.dog == null || #.horse == null || #.fish == null || #.id == null)]"
} ]
},
"response": {
"status": "400",
"headers": {
"Content-Type": "application/json; charset=utf-8"
},
"jsonBody": {
"message": "some sapi message"
}
}
}
If you weren't aware of all possible keys, you could use a Custom Request Matcher to check if the request body contained any null values, and if so, return your 400 error. I'd recommend creating your own class, something that resembles...
public class BodyNullCheck extends RequestMatcherExtension {
#Override
public MatchResult match(Request request, Parameters parameters) {
JSONParser parser = new JSONParser();
try {
JSONObject body = (JSONObject) parser.parse(request.getBody().toString());
for(Iterator iterator = body.keySet().iterator(); iterator.hasNext();) {
String key = (String) iterator.next();
if (body.get(key) == null) {
return MatchResult.of(true);
}
}
} catch (ParseException ex) {
ex.printStackTrace();
}
return MatchResult.of(false);
}
}
The above takes the request body and converts it to a JSONObject, and then iterates over all keys in the JSONObject. If any of their values are null, then we will return true. If after iterating over all of them, a null value isn't found, we return false.

Django CSRF token failure risk

On our production server, periodically we suffer from many CSRF token failures. The site does work fine for the rest, and I am aware CSRF failures may be user-side errors. However, for example this morning we received a flood of new failures, so we want to exclude any other possibilities.
An example failure mail today:
{
"GET": {},
"COOKIES": {},
"ERROR": "Referer checking failed - no Referer.",
"USER": "AnonymousUser",
"META": {
"REMOTE_ADDR": "127.0.0.1",
"mod_wsgi.version": "(4, 5, 20)",
"DOCUMENT_ROOT": "/usr/local/apache2/htdocs",
"SERVER_ADDR": "127.0.0.1",
"HTTP_ACCEPT_ENCODING": "gzip, deflate, br",
"wsgi.multithread": "True",
"HTTP_FORWARDED_REQUEST_URI": "/",
"CONTEXT_DOCUMENT_ROOT": "/usr/local/apache2/htdocs",
"wsgi.file_wrapper": "<class 'mod_wsgi.FileWrapper'>",
"mod_wsgi.path_info": "/",
"HTTP_ORIGIN": "chrome-extension://aegnopegbbhjeeiganiajffnalhlkkjb",
(...)
},
"POST": {}
}
Especially the HTTP_ORIGIN looks "interesting": why is this Chrome extension scraping/bullying us?
So essentially: Do we need to be worried about this?
Thanks!
This looks like an oddly coded "feature" in the "Browser Safety" Chrome extension. It tries to check if a URL is valid by sending an empty POST request to it (why?!).
var checkUrlState = function (url) {
var urlState = null;
if (blacklists.indexOf(domainFromUrl((url).toString())) < 0) {
var xhr = new XMLHttpRequest();
try {
xhr.open("POST", url, true);
xhr.timeout = 5000; // time in milliseconds
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
urlState = xhr.status;
} else {
urlState = null;
}
}
xhr.ontimeout = function () {
}
xhr.send();
} catch (e) {
onErrorReceived.call(xhr);
}
}
return urlState;
}
I'm also seeing this on my sites. I would recommend filtering it on the frontend based on the Origin header.

How do you pass Authorization header through API Gateway to HTTP endpoint?

I have an API behind an AWS API Gateway that needs to use the Authorization header for processing. I have unfortunately been unable to pass this to the backend for processing.
I have tried creating the Authorization HTTP Request Header in my Method Request and then creating the corresponding Authorization HTTP Header in my Integration Request (Authorization is mapped from method.request.header.Authorization in this case). I log all of the headers that the backend receives, and from the log, I can see other headers that I have listed in the Integration Request but not Authorization.
I have also tried creating a mapping template with Content-Type application/json and the template defined as
{
"AccountID": "$context.identity.accountId",
"Caller": "$context.identity.caller",
"User": "$context.identity.user",
"Authorization": "$input.params().header.get('Authorization')",
"UserARN": "$context.identity.userArn"
}
Yet, the backend logs show that there is still no Authorization header nor any Authorization field in the JSON body. I also cannot see the user's ARN. I have seen other examples and threads where users have mentioned accessing the Authorization field on the event object that is passed into a Lambda function, but I am not using a Lambda function.
I have made sure to Deploy the API Gateway in both scenarios.
Does anyone know if there is some way I can pass the Authorization header through the API Gateway to my HTTP endpoint? Is there an alternative way to access the API caller's user name or ID?
Edit - Here's a snippet of the code I'm using to hit the API Gateway:
String awsAccessKey = "myaccesskey";
String awsSecretKey = "mysecretkey";
URL endpointUrl;
try {
endpointUrl = new URL("https://<host>/<path>/<to>/<resource>?startDate=20151201&endDate=20151231");
} catch(Exception e) {
throw new RuntimeException("Unable to parse service endpoint: " + e.getMessage());
}
Date now = new Date();
SimpleDateFormat sdf1 = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
sdf1.setTimeZone(new SimpleTimeZone(0, "UTC"));
String dateTS = sdf1.format(now);
String headerNames = "host;x-amz-date";
String queryParameters = "endDate=20151231&startDate=20151201";
String canonicalRequest = "GET\n" +
"/<path>/<to>/<resource>\n" +
queryParameters + "\n" +
"host:<host>\n" +
"x-amz-date:" + dateTS + "\n" +
"\n" +
headerNames + "\n" +
"<sha256 hash for empty request body>";
System.out.println(canonicalRequest);
SimpleDateFormat sdf2 = new SimpleDateFormat("yyyyMMdd");
sdf2.setTimeZone(new SimpleTimeZone(0, "UTC"));
String dateStr = sdf2.format(now);
String scope = dateStr + "/us-east-1/execute-api/aws4_request";
String stringToSign =
"AWS4-HMAC-SHA256\n" +
dateTS + "\n" +
scope + "\n" +
"hex encoded hash of canonicalRequest";
System.out.println(stringToSign);
byte[] kSecret = ("AWS4" + awsSecretKey).getBytes();
byte[] kDate = HmacSHA256(dateStr, kSecret);
byte[] kRegion = HmacSHA256("us-east-1", kDate);
byte[] kService = HmacSHA256("execute-api", kRegion);
byte[] kSigning = HmacSHA256("aws4_request", kService);
byte[] signature = HmacSHA256(stringToSign, kSigning);
String credentialsAuthorizationHeader = "Credential=" + awsAccessKey + "/" + scope;
String signedHeadersAuthorizationHeader = "SignedHeaders=" + headerNames;
String signatureAuthorizationHeader = "Signature=" + "hex encoded signature";
String authorization = "AWS4-HMAC-SHA256 "
+ credentialsAuthorizationHeader + ", "
+ signedHeadersAuthorizationHeader + ", "
+ signatureAuthorizationHeader;
Map<String, String> headers = new HashMap<String, String>();
headers.put("x-amz-date", dateTS);
headers.put("Host", endpointUrl.getHost());
headers.put("Authorization", authorization);
headers.put("Content-Type", "application/json");
HttpURLConnection connection = null;
try {
connection = (HttpURLConnection) endpointUrl.openConnection();
connection.setRequestMethod("GET");
for (String headerKey : headers.keySet()) {
connection.setRequestProperty(headerKey, headers.get(headerKey));
}
connection.setUseCaches(false);
connection.setDoInput(true);
connection.setDoOutput(true);
InputStream is;
try {
is = connection.getInputStream();
} catch (IOException e) {
is = connection.getErrorStream();
}
BufferedReader rd = new BufferedReader(new InputStreamReader(is));
String line;
StringBuffer response = new StringBuffer();
while ((line = rd.readLine()) != null) {
response.append(line);
response.append('\r');
}
rd.close();
System.out.println(response.toString());
} catch (Exception e) {
throw new RuntimeException("Error: " + e.getMessage(), e);
} finally {
if (connection != null) {
connection.disconnect();
}
}
This is good enough to authenticate successfully and hit the HTTP endpoint on the backend.
As noted in comments, the Authorization header includes incomplete information for you to establish who the user is, so I wouldn't recommend going this route. Additionally, if AWS_IAM auth is enabled, the Authorization header will be consumed by API Gateway.
If AWS_IAM auth is enabled and the signature is supplied correctly, the $context.identity parameters should reflect the credentials used to sign the request.
If you use the test invoke feature in the console, do you see the context fields being filled in?
Update:
I'm unable to reproduce this issue.
I have an API with the following mapping template:
#set($path = $input.params().path)
#set($qs = $input.params().querystring)
{
"resource-path": "$context.resourcePath",
"http-method": "$context.httpMethod",
"identity": {
#foreach($key in $context.identity.keySet())
"$key": "$context.identity.get($key)"
#if($foreach.hasNext), #end
#end
},
"params": {
#foreach($key in $path.keySet())
"$key": "$path.get($key)"
#if($foreach.hasNext), #end
#end
},
"query": {
#foreach($key in $qs.keySet())
"$key": "$qs.get($key)"
#if($foreach.hasNext), #end
#end
},
"body": $input.json('$')
}
And a lambda function that simply spits back the input as output. When I sign the request and invoke the API, I get back the expected results:
{
"resource-path":"/iam",
"http-method":"GET",
"identity":{
"cognitoIdentityPoolId":"",
"accountId":"xxxxxxxx",
"cognitoIdentityId":"",
"caller":"AIDXXXXXXXXXXX,
"apiKey":"",
"sourceIp":"54.xx.xx.xx",
"cognitoAuthenticationType":"",
"cognitoAuthenticationProvider":"",
"userArn":"arn:aws:iam::xxxxxxxx:user/hackathon",
"userAgent":"Java/1.8.0_31",
"user":"AIDXXXXXXXXXXXXXX"
},
"params":{},
"query":{},
"body":{}
}
Currently the Authorization header can only be forwarded for methods that do not require AWS authentication. The SigV4 signing process relies on the Authorization header and we do not expose this for security purposes. If you have data you need to send (besides the SigV4 signature), you would need to send in another header.
In AWS API Gateway, Request Body is not supported for GET methods.
In Integration Request convert your GET to POST by specifying POST as your HTTP method. Then proceed with specifying the Body Mapping Template as proposed by #BobKinney
This way the request body will propagate properly, but the client will still be making a GET request as expected