Missing CORS response headers to some browsers' pre-flight checks?
Opened this issue · 2 comments
Describe the issue
Response headers to pre-flight requests from some browsers when using javascript fetch()
when Bearer authentication is used do not include CORS headers, causing some browsers (Chrome, Safari, Edge) to reject the response. Firefox does not seem to do a pre-flight check so it is not affected.
To Reproduce
The example below is adapted from the examples in the docs.
library(RestRserve)
allowed_tokens = c(
"super_secure_token_1"
)
auth_fun = function(token) {
res = FALSE
try({
res = token %in% allowed_tokens
}, silent = TRUE)
return(res)
}
basic_auth_backend = AuthBackendBearer$new(FUN = auth_fun)
auth_mw = AuthMiddleware$new(
auth_backend = basic_auth_backend,
routes = "/secure/",
match = "partial",
id = "auth_middleware"
)
app1 = Application$new()
app2 = Application$new(
middleware = list(
auth_mw,
RestRserve::CORSMiddleware$new()
)
)
main_html = "
<html><body>
Testing.
<script>
// Insecure page
fetch('http://localhost:8080/hello2')
.then(response => {
return response.text()
})
.then(content => {
document.querySelector('#hello2').innerHTML = content
});
// Secure page
fetch('http://localhost:8080/secure/hello1',
{ headers: {'Authorization': 'Bearer super_secure_token_1'} }
)
.then(response => {
return response.text()
})
.then(content => {
document.querySelector('#hello1').innerHTML = content
});
</script>
<div id='hello1'>HELLO1</div>
<div id='hello2'>HELLO2</div>
</body>
</html>
"
app1$add_get("/", function(req, res) {
res$set_content_type("text/html")
res$set_body(main_html)
})
app2$add_get("/secure/hello1", function(req, res) {res$body = "OK"})
app2$add_get("/hello2", function(req, res) {res$body = "OK"})
app2$logger$set_log_level("debug")
backend = RestRserve::BackendRserve$new()
backend$start(app2, http_port = 8080, background = TRUE)
backend$start(app1, http_port = 8081)
After starting the script, this works in a terminal:
# curl http://localhost:8080/secure/hello1 -H 'Authorization: Bearer super_secure_token_1'
OK
Chrome, Safari, Edge
Under
- Chrome 113.0.5672.92 (Official Build) (arm64),
- Safari 16.4 (18615.1.26.110.1), and
- Edge 113.0.1774.35 (Official build) (arm64),
this does not work. The fetch
to the secure page fails, and I see
Testing.
HELLO1
OK
In the developers' console in Chrome, I see
Access to fetch at 'http://localhost:8080/secure/hello1' from origin 'http://localhost:8081' 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. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
GET http://localhost:8080/secure/hello1 net::ERR_FAILED
Uncaught (in promise) TypeError: Failed to fetch
Under "Headers" for the pre-flight, I see:
So indeed, the headers don't seem to include Access-Control-Allow-Origin: *
as I would expect, and as the request to /hello2
does.
Looking at the RestRserve logs confirms this:
{"timestamp":"2023-05-09 21:22:22.344223","level":"DEBUG","name":"Application","pid":65018,"msg":"","context":{"request_id":"3456b2d8-eea7-11ed-a99a-ae6922d47386","response":{"status_code":401,"headers":{"WWW-Authenticate":"Basic"}}}}
Expected behavior
Under Firefox 112.0.2 (64-bit) this works as expected.
When I load http://localhost:8081
in a browser, I expect to see
Testing.
OK
OK
The two fetch calls to the server at localhost:8080
should replace HELLO1
and HELLO2
with the secure content and the insecure content, respectively.
When I look at the RestRserve logs, it seems like Firefox does not make a pre-flight check; instead, it just sends over the request in a GET request with the token, and RestRserve obliges with a proper response with CORS headers:
{"timestamp":"2023-05-09 21:28:00.826798","level":"DEBUG","name":"Application","pid":65118,"msg":"","context":{"request_id":"fe17904c-eea7-11ed-8461-ae6922d47386","request":{"method":"GET","path":"/secure/hello1","parameters_query":{},"parameters_path":[],"headers":{"sec-fetch-site":"same-site","connection":"keep-alive","dnt":"1","origin":"http://localhost:8081","authorization":"Bearer super_secure_token_1","sec-fetch-dest":"empty","referer":"http://localhost:8081/","accept-encoding":["gzip","deflate","br"],"host":"localhost:8080","accept-language":["en-GB","en;q=0.5"],"sec-fetch-mode":"cors","accept":"*/*","user-agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/112.0"}}}}
{"timestamp":"2023-05-09 21:28:00.829188","level":"DEBUG","name":"Application","pid":65118,"msg":"","context":{"request_id":"fe17904c-eea7-11ed-8461-ae6922d47386","response":{"status_code":200,"headers":{"Server":"RestRserve/1.2.1; Rserve/1.8.11","Access-Control-Allow-Origin":"*"}}}}
Environment information
> sessionInfo()
R version 4.2.1 (2022-06-23)
Platform: aarch64-apple-darwin20 (64-bit)
Running under: macOS Ventura 13.3.1
Matrix products: default
LAPACK: /Library/Frameworks/R.framework/Versions/4.2-arm64/Resources/lib/libRlapack.dylib
locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
attached base packages:
[1] stats graphics grDevices utils datasets methods base
loaded via a namespace (and not attached):
[1] compiler_4.2.1 tools_4.2.1 renv_0.15.5
On further exploration, this is what I think is happening:
- Authentication middleware looks at the request, and since it doesn't have the
authorization
header in it (because it is a pre-flight request) it sets the status to 401 (when the token is parsed). - The CORS middleware will only add the headers if the status is <300, so it doesn't get any CORS headers (when the CORS middleware runs).
- RestRserve sends the response to the pre-flight with 401 and no CORS headers.
In order to (temporarily?) work around this, I wrote a middleware to send 200 for specific OPTIONS requests:
handle_preflight_401_mw = Middleware$new(
process_response = function(request, response){
if(
request$method == 'OPTIONS' &&
'authorization' %in% request$headers[['access-control-request-headers']] &&
request$headers[['access-control-request-method']] == 'GET'
)
response$set_status_code(200)
},
id = "handle_preflight_401"
) # Put between CORS and auth middlewares
I think the thing with Firefox was a red herring: a CORS cache issue. At any rate, with the above middleware it seems to work in Chrome. If this is a bad idea, or there is some other solution, I'd love to know.
Thanks for reporting, looks like we need to update authorization middleware to not check authorization header for OPTIONS requests - https://stackoverflow.com/a/52072116/1069256