Related
Mod note: This question is about why XMLHttpRequest/fetch/etc. on the browser are subject to the Same Access Policy restrictions (you get errors mentioning CORB or CORS) while Postman is not. This question is not about how to fix a "No 'Access-Control-Allow-Origin'..." error. It's about why they happen.
Please stop posting:
CORS configurations for every language/framework under the sun. Instead find your relevant language/framework's question.
3rd party services that allow a request to circumvent CORS
Command line options for turning off CORS for various browsers
I am trying to do authorization using JavaScript by connecting to the RESTful API built-in Flask. However, when I make the request, I get the following error:
XMLHttpRequest cannot load http://myApiUrl/login.
No 'Access-Control-Allow-Origin' header is present on the requested resource.
Origin 'null' is therefore not allowed access.
I know that the API or remote resource must set the header, but why did it work when I made the request via the Chrome extension Postman?
This is the request code:
$.ajax({
type: 'POST',
dataType: 'text',
url: api,
username: 'user',
password: 'pass',
crossDomain: true,
xhrFields: {
withCredentials: true,
},
})
.done(function (data) {
console.log('done');
})
.fail(function (xhr, textStatus, errorThrown) {
alert(xhr.responseText);
alert(textStatus);
});
If I understood it right you are doing an XMLHttpRequest to a different domain than your page is on. So the browser is blocking it as it usually allows a request in the same origin for security reasons. You need to do something different when you want to do a cross-domain request.
When you are using Postman they are not restricted by this policy. Quoted from Cross-Origin XMLHttpRequest:
Regular web pages can use the XMLHttpRequest object to send and receive data from remote servers, but they're limited by the same origin policy. Extensions aren't so limited. An extension can talk to remote servers outside of its origin, as long as it first requests cross-origin permissions.
WARNING: Using Access-Control-Allow-Origin: * can make your API/website vulnerable to cross-site request forgery (CSRF) attacks. Make certain you understand the risks before using this code.
It's very simple to solve if you are using PHP. Just add the following script in the beginning of your PHP page which handles the request:
<?php header('Access-Control-Allow-Origin: *'); ?>
If you are using Node-red you have to allow CORS in the node-red/settings.js file by un-commenting the following lines:
// The following property can be used to configure cross-origin resource sharing
// in the HTTP nodes.
// See https://github.com/troygoode/node-cors#configuration-options for
// details on its contents. The following is a basic permissive set of options:
httpNodeCors: {
origin: "*",
methods: "GET,PUT,POST,DELETE"
},
If you are using Flask same as the question; you have first to install flask-cors
pip install -U flask-cors
Then include the Flask cors package in your application.
from flask_cors import CORS
A simple application will look like:
from flask import Flask
from flask_cors import CORS
app = Flask(__name__)
CORS(app)
#app.route("/")
def helloWorld():
return "Hello, cross-origin-world!"
For more details, you can check the Flask documentation.
Because
$.ajax({type: "POST" - calls OPTIONS
$.post( - calls POST
Both are different. Postman calls "POST" properly, but when we call it, it will be "OPTIONS".
For C# web services - Web API
Please add the following code in your web.config file under the <system.webServer> tag. This will work:
<httpProtocol>
<customHeaders>
<add name="Access-Control-Allow-Origin" value="*" />
</customHeaders>
</httpProtocol>
Please make sure you are not doing any mistake in the Ajax call.
jQuery
$.ajax({
url: 'http://mysite.microsoft.sample.xyz.com/api/mycall',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
type: "POST", /* or type:"GET" or type:"PUT" */
dataType: "json",
data: {
},
success: function (result) {
console.log(result);
},
error: function () {
console.log("error");
}
});
Note: If you are looking for downloading content from a third-party website then this will not help you. You can try the following code, but not JavaScript.
System.Net.WebClient wc = new System.Net.WebClient();
string str = wc.DownloadString("http://mysite.microsoft.sample.xyz.com/api/mycall");
Deep
In the below investigation as API, I use http://example.com instead of http://myApiUrl/login from your question, because this first one working. I assume that your page is on http://my-site.local:8088.
NOTE: The API and your page have different domains!
The reason why you see different results is that Postman:
set header Host=example.com (your API)
NOT set header Origin
Postman actually not use your website url at all (you only type your API address into Postman) - he only send request to API, so he assume that website has same address as API (browser not assume this)
This is similar to browsers' way of sending requests when the site and API has the same domain (browsers also set the header item Referer=http://my-site.local:8088, however I don't see it in Postman). When Origin header is not set, usually servers allow such requests by default.
This is the standard way how Postman sends requests. But a browser sends requests differently when your site and API have different domains, and then CORS occurs and the browser automatically:
sets header Host=example.com (yours as API)
sets header Origin=http://my-site.local:8088 (your site)
(The header Referer has the same value as Origin). And now in Chrome's Console & Networks tab you will see:
When you have Host != Origin this is CORS, and when the server detects such a request, it usually blocks it by default.
Origin=null is set when you open HTML content from a local directory, and it sends a request. The same situation is when you send a request inside an <iframe>, like in the below snippet (but here the Host header is not set at all) - in general, everywhere the HTML specification says opaque origin, you can translate that to Origin=null. More information about this you can find here.
fetch('http://example.com/api', {method: 'POST'});
Look on chrome-console > network tab
If you do not use a simple CORS request, usually the browser automatically also sends an OPTIONS request before sending the main request - more information is here. The snippet below shows it:
fetch('http://example.com/api', {
method: 'POST',
headers: { 'Content-Type': 'application/json'}
});
Look in chrome-console -> network tab to 'api' request.
This is the OPTIONS request (the server does not allow sending a POST request)
You can change the configuration of your server to allow CORS requests.
Here is an example configuration which turns on CORS on nginx (nginx.conf file) - be very careful with setting always/"$http_origin" for nginx and "*" for Apache - this will unblock CORS from any domain (in production instead of stars use your concrete page adres which consume your api)
location ~ ^/index\.php(/|$) {
...
add_header 'Access-Control-Allow-Origin' "$http_origin" always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
if ($request_method = OPTIONS) {
add_header 'Access-Control-Allow-Origin' "$http_origin"; # DO NOT remove THIS LINES (doubled with outside 'if' above)
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Max-Age' 1728000; # cache preflight value for 20 days
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'My-First-Header,My-Second-Header,Authorization,Content-Type,Accept,Origin';
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain charset=UTF-8';
return 204;
}
}
Here is an example configuration which turns on CORS on Apache (.htaccess file)
# ------------------------------------------------------------------------------
# | Cross-domain Ajax requests |
# ------------------------------------------------------------------------------
# Enable cross-origin Ajax requests.
# http://code.google.com/p/html5security/wiki/CrossOriginRequestSecurity
# http://enable-cors.org/
# <IfModule mod_headers.c>
# Header set Access-Control-Allow-Origin "*"
# </IfModule>
# Header set Header set Access-Control-Allow-Origin "*"
# Header always set Access-Control-Allow-Credentials "true"
Access-Control-Allow-Origin "http://your-page.com:80"
Header always set Access-Control-Allow-Methods "POST, GET, OPTIONS, DELETE, PUT"
Header always set Access-Control-Allow-Headers "My-First-Header,My-Second-Header,Authorization, content-type, csrf-token"
Applying a CORS restriction is a security feature defined by a server and implemented by a browser.
The browser looks at the CORS policy of the server and respects it.
However, the Postman tool does not bother about the CORS policy of the server.
That is why the CORS error appears in the browser, but not in Postman.
The error you get is due to the CORS standard, which sets some restrictions on how JavaScript can perform ajax requests.
The CORS standard is a client-side standard, implemented in the browser. So it is the browser which prevent the call from completing and generates the error message - not the server.
Postman does not implement the CORS restrictions, which is why you don't see the same error when making the same call from Postman.
Why doesn't Postman implement CORS? CORS defines the restrictions relative to the origin (URL domain) of the page which initiates the request. But in Postman the requests doesn't originate from a page with an URL so CORS does not apply.
Solution & Issue Origins
You are making a XMLHttpRequest to different domains, example:
Domain one: some-domain.com
Domain Two: some-different-domain.com
This difference in domain names triggers CORS (Cross-Origin Resource Sharing) policy called SOP (Same-Origin Policy) that enforces the use of same domains (hence Origin) in Ajax, XMLHttpRequest and other HTTP requests.
Why did it work when I made the request via the Chrome extension
Postman?
A client (most Browsers and Development Tools) has a choice to enforce the Same-Origin Policy.
Most browsers enforce the policy of Same-Origin Policy to prevent issues related to CSRF (Cross-Site Request Forgery) attack.
Postman as a development tool chooses not to enforce SOP while some browsers enforce, this is why you can send requests via Postman that you cannot send with XMLHttpRequest via JS using the browser.
For browser testing purposes:
Windows - Run:
chrome.exe --user-data-dir="C://Chrome dev session" --disable-web-security
The command above will disable chrome web security. So for example if you work on a local project and encounter CORS policy issue when trying to make a request, you can skip this type of error with the above command. Basically it will open a new chrome session.
You might also get this error if your gateway timeout is too short and the resource you are accessing takes longer to process than the timeout. This may be the case for complex database queries etc. Thus, the above error code can be disguishing this problem. Just check if the error code is 504 instead of 404 as in Kamil's answer or something else. If it is 504, then increasing the gateway timeout might fix the problem.
In my case the CORS error could be removed by disabling the same origin policy (CORS) in the Internet Explorer browser, see How to disable same origin policy Internet Explorer. After doing this, it was a pure 504 error in the log.
To resolve this issue, write this line of code in your doGet() or doPost() function whichever you are using in backend
response.setHeader("Access-Control-Allow-Origin", "*");
Instead of "*" you can type in the website or API URL endpoint which is accessing the website else it will be public.
Your IP address is not whitelisted, so you are getting this error.
Ask the backend staff to whitelist your IP address for the service you are accessing.
Access-Control-Allow-Headers
For me I got this issue for different reason, the remote domain was added to origins the deployed app works perfectly except one end point I got this issue:
Origin https://mai-frontend.vercel.app is not allowed by Access-Control-Allow-Origin. Status code: 500
and
Fetch API cannot load https://sciigo.herokuapp.com/recommendations/recommendationsByUser/8f1bb29e-8ce6-4df2-b138-ffe53650dbab due to access control checks.
I discovered that my Heroku database table does not contains all the columns of my local table after updating Heroku database table everything worked well.
It works for me by applying this middleware in globally:
<?php
namespace App\Http\Middleware;
use Closure;
class Cors {
public function handle($request, Closure $next) {
return $next($request)
->header('Access-Control-Allow-Origin', '*')
->header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
->header('Access-Control-Allow-Headers', "Accept,authorization,Authorization, Content-Type");
}
}
Edit
I can't really find a way to generate a secure URL from route name.
To get a full URL, I use
echo route('my_route_name');
But what to do, if I want a URL with https?
UPDATE: As pointed out in the comments, a simpler way of doing this would be adding URL::forceSchema('https'); for Laravel version between 4.2-5.3 or URL::forceScheme('https'); for version 5.4+ in the boot method of your AppServiceProvider file.
Old answer:
It's actually entirely possible and there's only one line of code needed to accomplish that.
Laravel doesn't check the presence of SSL by itself, it depends on Symfony. And there goes our key to making it believe that the current request is secure.
The thing is, we have to set the HTTPS server param to true and the easiest method is to paste the following code in the boot method of your AppServiceProvider:
$this->app['request']->server->set('HTTPS', true);
In my very case, I only need to force SSL in production, the local env should still work on http. This is how I force SSL only on production:
$this->app['request']->server->set('HTTPS', $this->app->environment() != 'local');
By the way, mind those terms, you may need them in the future.
Laravel 8
I recently resolved this by modifying this file:
app/Providers/AppServiceProvider.php
in the method boot() add the following:
URL::forceScheme('https');
Add the use in the top:
use Illuminate\Support\Facades\URL;
to work in your local environment you can leave it like this:
public function boot()
{
if(env('APP_ENV') !== 'local') {
URL::forceScheme('https');
}
}
Note: Don't forget to set your env variable APP_ENV with prod for the production file.
APP_ENV=prod
Actually turns out, that laravel doesn't care if url is secure or not, because it generates based on the current url. If you're on https page, route() will return secure url. If on http, then http:// url
The problem was, that Laravel didn't detect that https was enabled, which was due to faulty server configuration.
You can check if Laravel sees the current connection as https by calling Request::isSecure();
As I mentioned in a relevant question, I found 5 ways of how to generate secure URLs.
Configure your web server to redirect all non-secure requests to https. Example of a nginx config:
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name example.com www.example.com;
return 301 https://example.com$request_uri;
}
Set your environment variable APP_URL using https:
APP_URL=https://example.com
Use helper secure_url() (Laravel5.6)
Add following string to AppServiceProvider::boot() method (for version 5.4+):
\Illuminate\Support\Facades\URL::forceScheme('https');
Implicitly set scheme for route group (Laravel5.6):
Route::group(['scheme' => 'https'], function () {
// Route::get(...)->name(...);
});
At the moment this way is not documented, but it works well.
I think there is only one way to do this.
To generate the secure URL to your named routes, you might want to pass in your route into the secure_url helper function.
secure_url(URL::route('your_route_name', [], false));
You can't really use the route helper function because it generates absolute URL (with http://) by default and it's http not the https version that you wanted
Laravel 5.x will generate secure URL via route() helper if it detects the incoming connection is secure. Problem usually happen if the app is hidden behind load balancer or proxy (e.g. Cloudflare) since the connection between app server and load balancer/proxy might not be secure.
I am using Laravel Forge + Cloudflare now and this is the easiest way I could find to enable app thinking incoming connection is secure (not sure about other proxy).
Generate self signed certificate (see https://www.digitalocean.com/community/tutorials/openssl-essentials-working-with-ssl-certificates-private-keys-and-csrs or http://www.selfsignedcertificate.com/)
In Forge panel, insert your private key and cert via Sites > your-site > SSL Certificates > Install Existing Certificate.
Activate
In CloudFlare panel, Crypto > SSL, choose “Full” (not strict)
Done (it will take few minutes for the change to get propagated)
In short, connection between client and Cloudflare is secured by Cloudflare's own SSL. Connection between app server and Cloudflare is protected via your generated cert (thus the app is seeing 'connection' as secure.
You can apply the same principle with other stacks.
Use secure_url:
secure_url(URL::route('your_route_name', [], false));
You will need to set URL::route to false in order to not return a full URL. Then use secure_url function generates a fully qualified HTTPS URL to the given path.
From the UrlGenerator interface you can use URL::route
string route(string $name, mixed $parameters = array(), bool $absolute = true)
Get the URL to a named route.
Parameters
string $name
mixed $parameters
bool $absolute
Return Value
string
https://laravel.com/api/5.4/Illuminate/Contracts/Routing/UrlGenerator.html
In most cases routes should be generated with the same scheme your site was loaded with. Laravel automatically detects if request has X-Forwarded-Proto header and uses it to decide which scheme to use in generated route URLs. If your site is behind reverse proxy then you should add reverse proxy IP address to list of trusted proxies. https://github.com/fideloper/TrustedProxy package helps to do this. It's included in Laravel 5.5. For example, my config/trustedproxy.php looks like:
<?php
return [
'proxies' => '*',
'headers' => [
]
];
I use it with nginx reverse proxy that has the following configuration:
server {
listen 80;
server_name example.com;
access_log /var/log/nginx/example.com_access.log;
error_log /var/log/nginx/example.com_error.log;
client_max_body_size 50m;
location / {
proxy_pass http://localhost:8002;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
if ($scheme != "https") {
return 301 https://$host$request_uri;
}
}
Replace example.com with your domain. SSL certificates was provided by Let's Encrypt with certbot.
On laravel 5.5.*
You only need to add https on your .env file
as AppServiceProvider already had function that checks if your APP_URL or app.url on your config has https on it.
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
\URL::forceRootUrl(\Config::get('app.url'));
if (str_contains(\Config::get('app.url'), 'https://')) {
\URL::forceScheme('https');
}
}
This is certainly old, but someone like me will dump over here one day.
In your .env file define the APP_URL to use https instead of using http. Because all laravel url are generated based on this variable.
APP_URL=https://example.com
and wherever you want you can just say
{{ URL::route('my.route', params) }}
Or
{{ route('my.route', params) }}
With make sure all the routes are generated with secure protocol, add in the boot method of AppServiceProvider class:
<?php
namespace App\Providers;
use Illuminate\Routing\UrlGenerator;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot(UrlGenerator $url)
{
if (config('app.production')) {
$url->forceScheme('https');
}
}
Just add your application domain with the https protocol in the APP_URL of your .env file.
APP_URL=https://example.com
Then run route:cache
For reference of future visitors:
The secure_url function doesn't correctly handle GET parameters. So, for example, if you want to convert the url that the user has visited into a secure url while retaining the GET fields, you need to use this:
secure_url(Request::path()).'?'.http_build_query(Input::all());
Particularly note the use of path() rather than url() - if you give it a full url, it doesn't replace the http at the start, making it efectively useless.
I came across this issue while trying to generate a route as form action in Blade using Laravel 5.4.
Then I hit upon secure_url(), so I tried
{{ secure_url(route('routename', $args)) }}
This still returned a non-secure URL. :-(
After digging through the code and adding some debug logs, I finally figured out that secure_url does not change the incoming url argument, if it's already an absolute URL (including the scheme).
Fortunately route has an absolute flag as the third argument, and returns a relative URL if $absolute is passed as false.
Assuming /a/{id}/b is a named route "a.b"
route('a.b', 1) : will return http://[domain]/a/1/b
route('a.b', 1, false) : will return /a/1/b
Joining the two I arrived at :
{{ secure_url(route('routename', $args, false)) }}
As expected it generated https://[domain]/routeXXX
:-)
I had a problem with redirect trailing slashes after 2 hours of looking for a bug, just need to remove
.htaccess
<IfModule mod_rewrite.c>
<IfModule mod_negotiation.c>
Options -MultiViews
</IfModule>
RewriteEngine On
# Redirect Trailing Slashes If Not A Folder...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)/$ /$1 [L,R=301]
# Handle Front Controller...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]
</IfModule>
to
<IfModule mod_rewrite.c>
<IfModule mod_negotiation.c>
Options -MultiViews
</IfModule>
RewriteEngine On
# Handle Front Controller...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]
</IfModule>
If you are using Load Balancer, Laravel will never have the actual schema available.
So use https://stackoverflow.com/a/65691937/6489768. Working with Laravel - 9.x
Place this in your filters.php file and everywhere will be forced to https while retaining URL parameters:
//force ssl
App::before(function () {
if(!Request::secure() && App::environment() != 'local')
{
$baseHost = Request::getHttpHost();
$requestUri = Request::getRequestUri();
$newLink = 'https://'.$baseHost.$requestUri;
return Redirect::to($newLink);
}});
According to the laravel documentation on the url() helper method.
If no path is provided, a Illuminate\Routing\UrlGenerator instance is
returned
So you can use the secure method of the UrlGenerator class in the following way:
echo url()->secure('my_route_name');
To generate a secure (https) route use the following built-in 'before' filter called 'auth':
For example:
Route::get('your-route', ['before' => 'auth', 'uses' => YourController#yourAction']);
Now when you output your link it will be prepended with 'https'
I have this basic setup:
Http Request -> Hardware OpenWRT Router -> Apache -> IIS. Between Apache and IIS there's a simple mod_proxy config like:
<VirtualHost *:80>
ServerAdmin me#mail.com
ServerName steam.domain.com
ProxyPass / http://192.168.1.9/ timeout=600 Keepalive=On
ProxyPassReverse / http://192.168.1.9/
</VirtualHost *:80>
The application itself hosted on 192.168.1.9 IIS works flawlessly except for one thing - when I've followed the guide from website then I'm having my Steam OpenID return url set to 192.168.1.9 instead of steam.domain.com. Host is in the OpenID query string, so I suppose that it can be changed somehow?
Edit: The issue is when I click Steam button on login view I end up with SteamCommunity login screen with Sign In through Steam at 192.168.1.69 instead of Sign In through Steam at steam.domain.com as per picture:
The return url is build on the request launched when the user click on the link to authenticate himself via Steam:
private string BuildReturnTo(string state)
{
return Request.Scheme + "://" + Request.Host +
RequestPathBase + Options.CallbackPath +
"?state=" + Uri.EscapeDataString(state);
}
In OpenIDAuthenticationHandler.cs.
So I think that the problem is just in dev (I assume the website is still in dev cause of the ip address 192.168, but I know nothing about Hardware OpenWRT Router and mod_proxy).
I'm learning Laravel 4 and I can't use .htaccess in my hosting space, so I need to have index.php constantly in my url or I'll end up having a 404.
Is there a way to let laravel 4 know that I need index.php in my url ( in laravel 3 there was a a key in the config file application.php, I think, that seem to be gone now)
Example:
* if I'm in the url localhost :
URL::to('logout') => localhost/logout (404)
* but in the url localhost/index.php :
URL::to('logout') => localhost/index.php/logout (ok)
For now, as a quick hack, I use a before filter to redirect the the /index.php page if not already.
Is there any other better way to do it ?
Thanks
In /app/config/app.php change the 'url' value to your domain + index.php
I have added force_ssl in my ApplicationController, and deleted later, but now, every request is still prompted to https. I have tried to add config.force_ssl = false to all the configuration files in application.rb and environments/development.rb, etc., but it doesn't work. When I reboot the server, the requests are still converted to https. Any clue?
Updates:
This happens only when I request the root of the application, e.g. http://localhost:3000/, however in my config/routes.rb file I have specified the url for the root clearly: root :to => 'home#index'
You're seeing the effects of HTTP Strict Transport Security's max-age, which is set by Rack::SSL (which config.force_ssl = true sets up) to something high.
In addition to rebooting your app, you also have to clear the browser cache.
For those who it's still unclear, here is what I did to do the trick.
In application_controller.rb :
before_filter :expire_hsts
[...]
private
def expire_hsts
response.headers["Strict-Transport-Security"] = 'max-age=0'
end
In production.rb
config.force_ssl = false
Clear the cache of your web browser and that's it !
yfeldblum is absolutely correct. Disabling it and making chrome forget the header can be a pain.
Here's what I ended up putting in my config/application.rb
config.middleware.insert_before(Rack::Lock, Rack::SSL, hsts: false, exclude: proc { |env|
!env['PATH_INFO'].start_with?('/manage')
})
** note A: hsts: false is the critical bit
** note B: I'm using 1.9, so my hash syntax might be different than yours.
Beyond that, I had to open this url in Chrome chrome://net-internals/#hsts and remove the domains that had this header set.
Thankfully this didn't make it to production, because Rack::SSL sets a very long expires on this header.
if are you using nginx see option:
proxy_set_header X-Forwarded-Proto https;
and disable it!