CORS preflight mysteriously failing with gorilla/handlers - go

I am publishing a Golang API for my application through Heroku. Not able to get my webapp (Flutter/Dart stack) to actually get a successful response from my api. However I am able to get a successful response using curl commands from local. I've read through several posts about altering the Go mux server and adding the correct headers but this has not worked for me. I even see these headers returned back during my curl requests. Could really use some help as this is slowing me down.
essentially this is my main class which creates the server
import (
"api/preventative_care"
"api/user"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"log"
"net/http"
"os"
)
func main() {
log.SetFlags(log.LstdFlags | log.Llongfile)
router := mux.NewRouter()
// Where ORIGIN_ALLOWED is like `scheme://dns[:port]`, or `*` (insecure)
headersOk := handlers.AllowedHeaders([]string{"*"})
methodsOk := handlers.AllowedMethods([]string{"GET", "HEAD", "POST", "PUT", "OPTIONS"})
originsOk := handlers.AllowedOrigins([]string{"*"})
router.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
log.Println("Up and running!")
})
router.HandleFunc("/api/login", user.LoginHandler).Methods("GET")
router.HandleFunc("/api/recommendations", preventative_care.RecommendationHandler).Methods("GET")
var port = os.Getenv("PORT")
log.Printf("Starting application on port %s\n", port)
//log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), router))
log.Fatal(http.ListenAndServe(":" + os.Getenv("PORT"), handlers.CORS(originsOk, headersOk, methodsOk)(router)))
}
And the dart code which calls this API looks like this:
Map<String, String> headers = {
"content-type": "application/json",
"username": username,
"password": password
};
final response = await http.get(Uri.parse(uri), headers: headers);
I'm hosting both the webapp and the API in 2 separate Heroku Dynos. When I hit the API from my local using curl I see below:
$ > curl -iXGET https://my-app-api.herokuapp.com/api/login -H "username:hello" -H "password:pizza"
HTTP/1.1 200 OK
Server: Cowboy
Connection: keep-alive
Content-Type: application/json
Date: Thu, 18 Nov 2021 23:39:56 GMT
Content-Length: 160
Via: 1.1 vegur
I thought I was supposed to see see the Header Access-Control-Allow-Origin: * added there but it's not yet I still get 200 success back. However when I try to use my Webapp to hit the API from a login screen using Google Chrome I see this error:
Access to XMLHttpRequest at 'https://my-app-api.herokuapp.com/api/login' from origin 'https://my-app-staging.herokuapp.com' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
No idea - it's like the header is being removed by Chrome or something maybe?
EDIT: I've also tried sending a preflight request using CURL and I am seeing the correct headers - however it's still showing me 4XX error.
$ > curl -H "Access-Control-Request-Method: GET" -H "Origin: https://my-app-staging.herokuapp.com" --head https://my-app-api.herokuapp.com/api/login
HTTP/1.1 405 Method Not Allowed
Server: Cowboy
Connection: keep-alive
Access-Control-Allow-Origin: *
Date: Fri, 19 Nov 2021 00:51:52 GMT
Via: 1.1 vegur
So now I'm REALLY not sure

TL;DR
gorilla/handlers doesn't (yet?) support the wildcard for Access-Control-Allow-Headers. You must specify all the allowed headers explicitly. In your case, instead of
handlers.AllowedHeaders([]string{"*"})
you should have
handlers.AllowedHeaders([]string{"content-type", "username", "password"})
More details
The Fetch standard added support (in the case of non-credentialed requests) for the wildcard in the Access-Control-Allow-Headers header back in 2016. Most modern browsers now support this feature.
However, gorilla/handlers doesn't seem to have caught up with the spec yet. If you inspect the source code for the handlers.AllowedHeaders function and for the *cors.ServeHTTP method, you'll see that there's no special handling of the "*" value: it's treated literally. As a result, the CORS middleware detects a mismatch between the request headers supplied by your preflight request (content-type, username, and password) and your allowed headers (*, taken literally) and responds with a 403 without even setting the Access-Control-Allow-Origin header, thereby causing the access-control check to fail.

Related

Indy 10 in Lazarus RingCentral API RingOut TIdHTTP

Lazarus 1.8.4
Indy 10 (March 2020)
Windows Server 2016
I'm trying to build a little "Dialer" module for a contact list I've built in Lazarus. After fiddling for months, I was finally able to translate all the RingCentral API settings into Postman, then into Indy HTTP settings, to accomplish obtaining a token. That part works reliably. However, I am unable to do a RingOut API. I always get 404 not found. The examples in other programming languages given by RingCentral in their docs don't translate well into Indy.
if htpCall.Connected then
htpCall.Request.Connection := 'close'; // Close the socket after using it to obtain the token
htpCall.Request.Clear;
htpCall.Request.Accept := 'application/json';
htpCall.Request.ContentType := 'application/json';
htpCall.Request.Username := '<redacted>'; // Client ID
htpCall.Request.Password := '<redacted>'; // Client Secret
htpCall.Request.CustomHeaders.Clear;
htpCall.Request.CustomHeaders.FoldLines := False;
htpCall.Request.BasicAuthentication := False;
htpCall.Request.CustomHeaders.AddValue('Authorization', 'Bearer ' + acc_tkn);
prms_lst.Add('from=<redacted>');
prms_lst.Add('to=<redacted>');
prms_lst.Add('callerId=<redacted>');
prms_lst.Add('country=Canada');
prms_lst.Add('playPrompt=true');
// Remove the text ".devtest" for the production version of your code, and replace the phone account with the real main phone number
htpCall.Post('https://platform.devtest.ringcentral.com/restapi/v1.0/account/<redacted>/extension/101/ring-out', prms_lst);
UPDATE:
If I leave the TIdHTTP open, then run the code above, I get a different error.
Sent:
POST /restapi/v1.0/account//extension/101/ring-out HTTP/1.1
Content-Type: application/json
Content-Length: 82
Authorization: Basic
Host: platform.devtest.ringcentral.com
Accept: application/json
User-Agent: Mozilla/3.0 (compatible; Indy Library)
from=&to=&callerId=&country=Canada&playPrompt=true
Rcvd:
HTTP/1.1 401 Unauthorized
Server: nginx
Date: Tue, 25 Aug 2020 01:50:24 GMT
Content-Type: application/json
Content-Length: 175
Connection: keep-alive
WWW-Authenticate: Bearer realm="RingCentral REST API"
RCRequestId: 57c97008-e675-11ea-87cf-005056bb81b6
{
"errorCode" : "AGW-402",
"message" : "Invalid Authorization header",
"errors" : [ {
"errorCode" : "AGW-402",
"message" : "Invalid Authorization header"
} ]
}

Access-Control-Allow-Origin in preflight response doesn't enable cross-domain access

I am trying to send a CORS request using AJAX to a nodeJS server. I want to return some JSON data. I've found numerous tutorials online that all say the same thing, which I've tried, but I can't get this to work. Here's the AJAX request:
$.ajax({
url: "http://some.other.url.com:8880",
type: "GET",
crossDomain: true,
contentType: 'application/json'
}).then(function(response) {
$scope.allData = jQuery.parseJSON( response );
console.log($scope.allData);
}).fail(function(response) {
});
And here is the code on the server:
var path = url.parse(req.url).pathname,
match = router.match(path),
rescode;
console.log("---: " + req.method);
if (req.method === 'OPTIONS') {
var headers = {};
headers["Access-Control-Allow-Origin"] = "*";
headers["Access-Control-Allow-Methods"] = "POST, GET, PUT, DELETE, OPTIONS";
headers["Access-Control-Allow-Credentials"] = false;
headers["Access-Control-Max-Age"] = '86400'; // 24 hours
headers["Access-Control-Allow-Headers"] = "X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept";
res.writeHead(200, headers);
return res.end();
}
I'v also tried it without the return on res.end() i.e. not returning the OPTIONS preflight request, and that doesn't work either.
--Edit--
Here is the actual error message in the console:
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://other.domain.com:8880/. This can be fixed by moving the resource to the same domain or enabling CORS.
The server is getting the requests. Both the OPTIONS and then GET requests are hitting the server and being responded to. In fact, in the console log for the page making the AJAX request, I can click on the CORS error and see the response, and it is the correct data. But I can't seem to get the javascript to continue.
In regards to .done vs .then, they seem to work interchangeable. Or at least, in this example, the .then and .fail are working just fine.
You're correctly setting CORS headers in your OPTIONS preflight response, but you also need to set Access-Control-Allow-Origin (either to your origin or *) on your actual GET response. The GET response should respond with the same CORS headers, regardless of whether there was a preflight response or not. This means that it must send the appropriate CORS headers, but it does not need to send anything except for Access-Control-Allow-Origin. (If other non-simple components like non-simple verbs or headers are involved, they will be allowed or denied in the preflight; the actual GET response does not need to worry about them.)
The Enable CORS site has a CORS testing tool to help you see the headers involved in a request that you specify. I've used that tool to set up a test similar to your case (GET with non-simple Content-Type header). If we examine the results of that test (careful -- the steps are presented little bit out of order, but they're all there), we see a preflight response:
Access-Control-Allow-Methods: POST, GET, PUT, DELETE, OPTIONS
...
Access-Control-Allow-Origin: http://client.cors-api.appspot.com
Access-Control-Allow-Headers: X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept
And the final CORS response:
Content-Length: 0
Content-Type: application/json
Access-Control-Allow-Origin: http://client.cors-api.appspot.com
Cache-Control: no-cache
As you can see, the GET response also has a Access-Control-Allow-Origin header and no other CORS headers. If you have any further uncertainties, feel free to tweak the settings on that tool to run a wide range of other test cases.

localhost request headers not sending cookies

I am building a nodeJS server which allows users to login using an AJAX post on a client application. The server responds with a cookie that, after successful login, keeps track of the user. I can't however seem to get the client application to send the cookie back to the server. It never seems to respond to the server including the cookie it just recieved.
On the fist call that's made to the server, I login with my credentials. the server responds with this:
In the response headers:
Set-Cookie:SID=0xtW36rYCiV; path=/; expires=Tue, 24-Jun-2014 14:14:51 GMT; secure
In the subsequent request headers:
No cookie is sent back to the server
In my client application I am using the following code:
jQuery.ajax( {
url: this.domain + this.url,
async: this.async,
type: 'POST',
dataType: this.dataType,
crossDomain: true,
cache: true,
accepts: 'text/plain',
data: this.postVars,
success: this.onData.bind( this ),
error: this.onError.bind( this ),
xhrFields: {
withCredentials: true
}
});
Note I am using the xhrFields.
And in my node server I am responding with this: (notice I am including all the CORRS variables)
if ( request.headers.origin )
{
if ( request.headers.origin.match( /mydomain\.net/ ) || request.headers.origin.match( /appname\.mydomain\.com/ ) || request.headers.origin.match( /localhost/ )
|| request.headers.origin.match( /localhost\.com/ )
|| request.headers.origin.match( /localhost\.local/ )
|| request.headers.origin.match( /localtest\.com/ ) )
{
response.setHeader( 'Access-Control-Allow-Origin', request.headers.origin );
response.setHeader( 'Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS' );
response.setHeader( 'Access-Control-Allow-Headers', 'Content-Type, Authorization, Content-Length, X-Requested-With' );
response.setHeader( "Access-Control-Allow-Credentials", "true" );
};
}
response.writeHead( 200, { "Content-Type": "application/json" });
response.end( JSON.stringify( json ) );
}
I have also edited my windows hosts file to include these test domains so that I don't have to use an IP or localhost iteself:
127.0.0.1 localhost
127.0.0.1 tooltest.com
127.0.0.1 localhost.com
127.0.0.1 localhost.local
But no matter what I do, or which of the above hosts I use, it never seems to work. It seems to only be related to localhost via ajax because if I go directly to the server url in question - it works.
EDIT 1
So for example - I open the client application and try to login to the server at foo.com/user/log-in. The headers for the request and response are as follows:
Request:
Remote Address:188.xxx.xxx.xx:7000
Request URL:https://foo.com:7000/user/log-in
Request Method:POST
Status Code:200 OK
Request Headersview source
Accept:undefined
Accept-Encoding:gzip,deflate,sdch
Accept-Language:en-GB,en-US;q=0.8,en;q=0.6
Connection:keep-alive
Content-Length:45
Content-Type:application/x-www-form-urlencoded; charset=UTF-8
Host:foo.com:7000
Origin:http://localhost
Referer:http://localhost/animate-ts/trunk/bin/
User-Agent:Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36
Form Dataview sourceview URL encoded
user:mat
password:testpassword
rememberMe:true
Response:
Response Headersview source
Access-Control-Allow-Credentials:true
Access-Control-Allow-Headers:Content-Type, Authorization, Content-Length, X-Requested-With
Access-Control-Allow-Methods:GET,PUT,POST,DELETE,OPTIONS
Access-Control-Allow-Origin:http://localhost
Connection:keep-alive
Content-Type:application/json
Date:Tue, 24 Jun 2014 14:14:21 GMT
Set-Cookie:SID=0xtW36rYCiV; path=/; expires=Tue, 24-Jun-2014 14:14:51 GMT; secure
Transfer-Encoding:chunked
As I undertand it, Foo.com has told the browser to store the cookie 0xtW36rYCiV. However when I make the very next request (foo.com/user/is-logged-in) to see if the user is logged in, no cookies are sent to foo. Foo.com looks for the cookie with the ID 0xtW36rYCiV but can't find anything. When I look at the 2nd request in dev tools I can see:
Does anyone have any other ideas? I thought I covered everything in the above, but im starting to think it just wont work :(
Thanks
Mat
My cookie isn't marked secure, and I'm facing this problem trying to send a GET request to my server (https://auth.mywebsite.com) from http://localhost:3000/.
I have the same auth flow as OP here.
First, sign in to auth.website.com, and receive back cookie in the response header:
set-cookie: Authorization=Bearer%20blahblah.blahblah.blahblah; Path=/; Expires=Wed, 05 Aug 2020 02:07:57 GMT; HttpOnly
Note, I don't have the Secure flag in set-cookie.
I noticed in Chrome devtools there was this warning that popped up when hovering over the set-cookie line:
"This Set-Cookie didn't specify a "SameSite" attribute and was defaulted to "SameSite=Lax," and was blocked because it came from a cross-site response which was not the response to a top-level navigation. The Set-Cookie had to have been set with "SameSite=None" to enable cross-site usage."
This might be the answer to my problem. But, I don't understand why this was working yesterday, but not today. No code has changed in between.
This issue occurs on both on Chrome and Safari.
The MDN docs has a good description of all the flags for Set-Cookie.
This is not a full answer, but perhaps some extra context for others facing similar issues.
I had this problem, but #aspillers commment lead to the solution for me. The cookie was marked secure, but I was doing the request over HTTP instead of HTTPS. Once I got my debugger switched over to HTTPS it started working.
For chrome:
Go to chrome://flags/
Disable "SameSite by default cookies"
Restart Chrome

Please help me understand Ajax request versus Backbone fetch()

My app can currently hit our API with a standard JQuery Ajax GET request and get good data back. CORS has been properly implemented on the remote server as far as I can see. Here are the response headers:
company_client_envelope_id: 88764736-6654-22e4-br344-a1w2239a892d
access-control-allow-headers: X-Requested-With, Cookie, Set-Cookie, Accept, Access-Control
Allow-Credentials, Origin, Content-Type, Request-Id , X-Api-Version, X-Request-Id,Authorization, COMPANY_AUTH_WEB
access-control-expose-headers: Location
response-time: 55
request-id: 88764736-6654-22e4-br344-a1w2239a892d
company_api_version: 0.01.09
server: localhost
transfer-encoding: chunked
connection: close
access-control-allow-credentials: true
date: Sun, 09 Feb 2014 14:44:05 GMT
access-control-allow-origin: *
access-control-allow-methods: GET, POST
content-type: application/json
However, using Backbone and calling the same GET request by using fetch() causes the following CORS error:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
I cannot figure out what the difference is. Both requests are running from localhost.
In the case of the AJAX query, the following is being sent as requested by the API guys:
headers: {
"accept":"application/json"
}
And in the case of the model and collection declaration I am sending the headers like so:
MyApp.someCollection = Backbone.Collection.extend(
{
model:MyApp.someModel,
headers: {
'Accept':'application/json',
'withCredentials': 'true'
},
url: MYCOMPANY_GLOBALS.API + '/endpoint'
});
and my fetch is simply:
someCollection.fetch();
===============================
Added in response to: #ddewaele
These are the headers from the network tab:
Request URL:http://api-blah.com:3000/
Request Headers CAUTION: Provisional headers are shown.
Accept:application/json
Cache-Control:no-cache
Origin:http://localhost
Pragma:no-cache
Referer:http://localhost/blah/blah/main.html
User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.107Safari/537.36
There is no pre-flight or remote headers from the API server:
many thanks,
Wittner
I've recommended to you rewrite Backbone.sync method, because in your app you have some security field for example and other reason.
var oldBackboneSync = Backbone.sync;
// Override Backbone.Sync
Backbone.sync = function (method, model, options) {
if (method) {
if (options.data) {
// properly formats data for back-end to parse
options.data = JSON.stringify(options.data);
}
// transform all delete requests to application/json
options.contentType = 'application/json';
}
return oldBackboneSync.apply(this, [method, model, options]);
}
You can add different headers as you want.

Restify Delete Method

CORS is starting to fry my brain a bit. Everything is good now, apart from one method. I'm building an app with backbone on the frontend and node.js/restify on the backend. The server.coffee looks like this:
server.get '/todos', todos.find_all
server.get '/todos/:id', todos.find_by_id
server.del '/todos/:id', todos.delete
Whenever a model in backbone calls destroy however I get this rather annoying error:
MLHttpRequest cannot load http://localhost:8080/todos/. Method DELETE is not allowed by Access-Control-Allow-Methods.
I read about this a bit and using restify done the following:
unknownMethodHandler = (request, response) ->
if(request.method.toLowerCase() == 'options')
allowHeaders = ['Accept', 'Accept-Version', 'Content-Type', 'Api-Version']
if(response.methods.indexOf('OPTIONS') == -1) then response.methods.push('OPTIONS')
response.header 'Access-Control-Allow-Credentials', true
response.header 'Access-Control-Allow-Headers', allowHeaders.join(', ')
response.header 'Access-Control-Allow-Methods', ['GET', 'DELETE', 'TEST!']
response.header 'Access-Control-Allow-Origin', request.headers.origin
response.send 204
else
response.send new restify.MethodNotAllowedError()
server.on 'MethodNotAllowed', unknownMethodHandler
But even still, I get this as the response header:
HTTP/1.1 204 No Content
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version
Access-Control-Allow-Methods: GET, OPTIONS
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: X-Api-Version, X-Request-Id, X-Response-Time
Connection: Keep-Alive
Date: Mon, 04 Feb 2013 12:24:25 GMT
Server: restify
X-Request-Id: fbd4e15a-a22e-48b6-bf5c-a46b94926748
X-Response-Time: 0
I just don't get what I'm doing wrong!
If you're expecting a response, you should use a '200' response code, not a 204 as that's a No Content response. See the W3C Spec for the details
9.7 DELETE
The DELETE method requests that the origin server delete the resource identified by the Request-URI. This method MAY be overridden
by human intervention (or other means) on the origin server. The
client cannot be guaranteed that the operation has been carried out,
even if the status code returned from the origin server indicates that
the action has been completed successfully. However, the server SHOULD
NOT indicate success unless, at the time the response is given, it
intends to delete the resource or move it to an inaccessible location.
A successful response SHOULD be 200 (OK) if the response includes an entity describing the status, 202 (Accepted) if the action has not
yet been enacted, or 204 (No Content) if the action has been enacted
but the response does not include an entity.
If the request passes through a cache and the Request-URI identifies one or more currently cached entities, those entries SHOULD
be treated as stale. Responses to this method are not cacheable.
You're seeing the Access-Control-Allow-Origin: * in the response header. This is coming from the .../restify/lib/router.js preflight() method. The comment states "user will need to defined their own .opts handler".
Use server.opts method to wirte your own handler for OPTIONS request.
Below is the example you can use.
Also tell me if you are using set-credentials flag to true while making request from the browser. This handle in that case would have to respond with access cookies.
In the example below, I am returning the allowed origin for exact match.
You can tweak it to be substring match also. But always return the exact value as found in request header origin in the response header 'Access-Control-Allow-Origin'. Its a good practice.
server.opts('/api/(.)*', (req, res) => {
const origin = req.header('origin');
const allowedOrigins = ['example.com', 'example.org'];
if (allowedOrigins.indexOf(origin) === -1) {
//origin is not allowed
return res.send(405);
}
//set access control headers to allow the preflight/options request
res.setHeader('Access-Control-Allow-Origin', header);
res.setHeader('Access-Control-Allow-Headers', 'Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version');
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS');
// Access-Control-Max-Age header catches the preflight request in the browser for the desired
// time. 864000 is ten days in number of seconds. Also during development you may want to keep
// this number too low e.g. 1.
res.setHeader('Access-Control-Max-Age', 864000);
return res.send(200);
});
Just set header res.setHeader('Access-Control-Allow-Methods', '*');
Here is the answer: https://github.com/mcavage/node-restify/issues/296#issuecomment-12333568

Resources