I have been struggling with this for quite a while now, and other than decided that I hate CORS with a passion, I can also confirm that the internet is full of ideas, but very few working solutions to this problem so thought I would try to get the answer once and for all.
The objective is to write angular code that can do the following.
1. Set the headers to allow for CORS.
2. Log into a CORS enabled application (in this case media wiki)
3. Make an api call to receive some JSON data.
I will even set up a test mediawiki server (with semantic media wiki) and set up a test user just for this. And who knows, if people are interested enough I'll even attempt to use the answer to write the worlds simplest CORS module for Angular.
On the server I'll do three things.
Install mediawiki and semantic Media wiki
Change the settings (in LocalSettings.php)
$wgEnableAPI = true;
$wgCrossSiteAJAXdomains = array( 'http://127.0.0.1:30004' );
$wgAPIRequestLog = "$IP/log/wgAPIRequestLog.log";
And change .htaccess to set the headers.
# CORS Headers (add this)
<ifModule mod_headers.c>
Header always set Access-Control-Allow-Origin: "*"
Header always set Access-Control-Allow-Methods "POST, GET, PUT, DELETE, OPTIONS"
Header always set Access-Control-Allow-Headers "X-Requested-With, content-type"
</ifModule>
# BEGIN WordPress (you should have this in your original WP .htaccess)
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
# CORS OPTIONS (add this too)
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ $1 [R=200,L]
I think this is all I need to do, however, as I have only been able to make CORS requests in angular after using a chome extension to turn of origin headers, I don't know if this will work.
In the meanwhile, if anyone has any ideas or good resources for this. Please post.
====================== And now set up =====================
So have now set up a mediawiki for this purpose
Mediawiki url 'http://v-ghost.port0.org:8081/apiwiki'
User 'whowillhelp'
Password 'InternetWill'
If you want to try an API call:
http://v-ghost.port0.org:8081/apiwiki/api.php?action=ask&query=[[Msgtype::1]]|%3FMenuName&format=jsonfm
If you haven't logged in, it will return:
{
"error": {
"code": "readapidenied",
"info": "You need read permission to use this module",
"*": "See http://v-ghost.port0.org:8081/apiwiki/api.php for API usage"
}
}
Thats all OK.
If you run the same http.get('http://v-ghost.port0.org:8081/apiwiki/api.php?action=ask&query=[[Msgtype::1]]|%3FMenuName&format=json' in angular you will get a
XMLHttpRequest cannot load http://v-ghost.port0.org:8081/apiwiki/api.php?action=ask&query=[[MsgType::1]][[Parent::Support%20Functions]]|%3FMenuName&format=jsonfm. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://127.0.0.1:30004' is therefore not allowed access.
That's wrong on many levels:
first, it is horribly insecure to enable CORS without limitations on any service that is even remotely public.
second, you need to set the Access-Control-Allow-Credentials header for anything that CORS is actually useful for (if it's a public API, just use JSONP). MediaWiki does this for you, when properly configured.
third, $wgCrossSiteAJAXdomains takes domain names as its arguments, not URL prefixes, so that should be $wgCrossSiteAJAXdomains = array( '127.0.0.1:30004' );.
fourth, you need to set the origin parameter in the example, ie. the correct URL is 'http://v-ghost.port0.org:8081/apiwiki/api.php?action=ask&query=[[Msgtype::1]]|%3FMenuName&format=json&origin=http://127.0.0.1:30004.
Thank you Quentin.
After an awful lot of faffing about, it turns out that CORS is fully controlled bt the server, and at that the web server not the application.
All that was needed (once the API was on) would have been to add the stuff to the .htaccess file. Configure Apache to read it (http.conf).
You also need to ensure that mod_headers (called headers_module when httpd -t -D DUMP_MODULES) is loaded, but it should be default with Apache 2).
And if in doubt, there is a test service:)
http://client.cors-api.appspot.com/
================= Seems to work now ===================
And as I try to be a nice person that give back. Here is the factory I wrote. Technically it requires three calls (if login is required) one if you just want to run queries. Hope it helps someone
Might we worth mentioning, Mediawiki (and potentially all CORS implementations) require you to log in twice, once for authentication, once with the token it sent earlier. Guessing this is related to building the cookie.
.factory('CORSconnectFactory', function($http, $q){
//To call any of the http calls just do
// CORSconnectFactory.login(options )
// .then(function(data)
var CORSconnectFactory = {};
var CORScredentials = {};
var urlInit;
var deferred = $q.defer();
var datareq = {
method: 'POST',
//url: 'http://v-ghost.port0.org:8081/dbfswiki/api.php?action=login&format=json',
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8' //Set the coding, otherwise the server can't read you're request
}
//withCredentials: true,
/*
If you set Credentials your server gets rather picky oc CORS, more specifically Header always set Access-Control-Allow-Origin: needs to be set to exact address.
*/
//data: "lgname=APIread&lgpassword=apiread01"
}
CORSconnectFactory.initSettings = function(url, withCredentials) {
urlInit = url;
datareq.withCredentials= withCredentials;
}
CORSconnectFactory.login = function(id, password){
datareq.url = urlInit + "?action=login&format=json";
datareq.data= "lgname=" + id + "&lgpassword=" + password;
return $http(datareq)//You don't have to return this request, but it helps as it lets you use .login's promise in your code
.success(function(data) {
var results = data.login; //You might have to work around to find the right part of the structure
if ( results.result === "NoName"){
console.log("New request for Token")
} else if ( results.result === "NeedToken" ) {
console.log("Need a token this time") //Mediawiki API requires login then login with token to accept the connection.
CORScredentials = data.login
return results.result;
}
});
}
CORSconnectFactory.tokenReq = function(){
datareq.data = datareq.data = datareq.data + "&lgtoken=" + CORScredentials.token;
return $http(datareq)
.success(function (data) {
results= data.login;
CORScredentials=data.login;
console.log("And I am now " + results.result);
})
}
CORSconnectFactory.query = function(queryString) {
datareq.url = urlInit + "?action=ask&query="+ queryString + "&format=json";
datareq.data = ""; //Don't send data, it confuses the poor query (and your login details is in the token
return $http(datareq)
.success(function(data) {
var results = data.query.results;
return results;
});
}
return CORSconnectFactory;
});
Related
I have an app made with backbone. When collections try to sync, I get the following error:
XMLHttpRequest cannot load https://someserver/menu.json. The value of
the 'Access-Control-Allow-Origin' header in the response must not be
the wildcard '*' when the request's credentials mode is 'include'.
Origin 'http://www.otherdomain.com' is therefore not allowed access.
The credentials mode of requests initiated by the XMLHttpRequest is
controlled by the withCredentials attribute.
The collections just specify model and url, and have a simple parse.
I've try to override the sync method, as follows:
(function() {
var proxiedSync = Backbone.sync;
Backbone.sync = function(method, model, options) {
options || (options = {});
if (!options.crossDomain) {
options.crossDomain = true;
}
if (!options.xhrFields) {
options.xhrFields = {withCredentials:true};
}
return proxiedSync(method, model, options);
};
})();
I run this function when app initialize. However, the error remains the same. I've tried different combinations (withCredentials:false, removing the line withCredentials etc) with no success.
The services are configured to serve '*'.
Is there a different way to tackle this problem?
Reconfiguring services or disabling browser security is not an option.
Your code will only execute if xhrFields is empty, maybe it is not.
Try something like
if (!options.xhrFields) {
options.xhrFields = {};
}
options.xhrFields.withCredentials = false;
This will only solve the problem temporarily. In a real application you will need the credentials, and you should add a whitelist of domains in server to actually solve the problem
I am currently building an application using token based authentication with Angular and Laravel. I initially set things up just to test the API by creating a BookController . At first I was getting a Cross Origin Request Block error when I tried to call this data from Angular. However I managed to resolve this by adding the headers to my routes/web.php file. Here is the whole file. NB: After adding these headers I was succesfully able to use the API even from another domain
<?php
header('Access-Control-Allow-Origin: *');
header( 'Access-Control-Allow-Headers: Authorization, Content-Type' );
//Route::get('/', 'BookController#show');
//Route::resource('book/create', 'BookController#create');
Auth::routes();
Route::get('/', 'HomeController#index');
Route::resource('book', 'BookController');
Route::resource('authenticate', 'AuthenticateController', ['only' => ['index']]);
Route::post('authenticate', 'AuthenticateController#authenticate');
However I am currently following this tutorial to set up token based authentication. https://scotch.io/tutorials/token-based-authentication-for-angularjs-and-laravel-apps
To summarise , my issue is when I submit the form containing username and password I am getting the following errors. Below I will try elaborate a bit more but it is quite difficult as there is alot to it.
Cross-Origin Request Blocked: The Same Origin Policy disallows reading
the remote resource at http://www.example.local/authenticate/.
(Reason: CORS header 'Access-Control-Allow-Origin' missing).
And
Possibly unhandled rejection:
{"data":null,"status":-1,"config":{"method":"POST","transformRequest":[null],"transformResponse":[null],"jsonpCallbackParam":"callback","url":"http://www.example.local/authenticate/","data":{"email":"dasdas#Dasa.com","password":"fsdfd"},"withCredentials":false,"headers":{"Accept":"application/json,
text/plain,
/","Content-Type":"application/json;charset=utf-8"}},"statusText":""}
I am using Angular UI Router V 0.4.2 and satellizer. My Angular version is 1.6.2 It using a different domain than the API. Much like the working example above.
On the laravel side I also followed this tutorial to add middleware to attempt to resolve this but no luck.
http://en.vedovelli.com.br/2015/web-development/Laravel-5-1-enable-CORS/
I will also include my AuthenticateController.php file..
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Requests;
use App\Http\Controllers\Controller;
use JWTAuth;
use Tymon\JWTAuth\Exceptions\JWTException;
use App\User;
class AuthenticateController extends Controller
{
public function __construct()
{
// Apply the jwt.auth middleware to all methods in this controller
// except for the authenticate method. We don't want to prevent
// the user from retrieving their token if they don't already have it
$this->middleware('jwt.auth', ['except' => ['authenticate']]);
$this->middleware('cors');
}
public function index()
{
// Retrieve all the users in the database and return them
$users = User::all();
return $users;
}
public function authenticate(Request $request)
{
$credentials = $request->only('email', 'password');
try {
// verify the credentials and create a token for the user
if (! $token = JWTAuth::attempt($credentials)) {
return response()->json(['error' => 'invalid_credentials'], 401);
}
} catch (JWTException $e) {
// something went wrong
return response()->json(['error' => 'could_not_create_token'], 500);
}
// if no errors are encountered we can return a JWT
return response()->json(compact('token'));
}
}
My issue is I do not even know if the "possibly unhandled rejection" is related to the "Cross-Origin Request Blocked" error. But I have to assume it is.
Can you recognise anything from my routes files that may be allowing one and not another?
EDIT:
I have noticed the difference between one request and another is that one is a GET request while another is an OPTIONS request. This may be the cause.
I have since added Header set Access-Control-Allow-Origin "*" to both the virtual hosts config file in Apache and to a .htaccess file in the root of the Laravel project. Still no change.
I am wondering is this related something in Angular
Your server code needs to handle that OPTIONS request by sending a headers-only response to it that includes the Access-Control-Allow-Methods: GET, POST, PUT, DELETE header and Access-Control-Allow-Headers: Authorization, Content-Type header.
Or you can just try using https://github.com/barryvdh/laravel-cors which makes all this easier.
https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Preflighted_requests has general info you might want to read up on.
I've been trying to accomplish this task for quite some time but haven't got any breakthrough yet. I would be really thankful if anyone can help me out in this.
Current Situation:
I've two applications that I'm running in two sub-domains as
st.localhost:8080 and acm.localhost:8080
When a user tries to access either of the URLs, I search for a cookie named 'auth' using Angular $cookies service. If the cookie is defined or present, the user is redirected to original application. However, if the cookie is undefined, user is redirected to a login page(the login page resides in both the applications).
From the login page, after successful credentials check, I set the 'auth' cookie again with a random value. This cookie is supposed to be shared between the two sub-domains.
Express:
var express = require('express');
var httpProxy = require('http-proxy');
var vhost = require('vhost');
var app = express();
var proxy = httpProxy.createProxyServer();
app.get('/login', function(req, res) {
var randomNumber=Math.random().toString();
randomNumber=randomNumber.substring(2,randomNumber.length);
var expireDate = new Date();
expireDate.setDate(expireDate.getDate() + 1);
res.cookie('auth', randomNumber, { maxAge: 90000000, domain: 'localhost', httpOnly: false });
console.log('cookie created successfully');
res.send('Login successful');
});
app.use('/api', function(req, res) {
req.headers[ 'Authorization' ] = 'Basic dXNlcjpwYXNzd29yZA==';
console.log("Request cookies: " + req.cookie);
proxy.web(req, res, { target: 'restApiTarget' });
});
// ST application
app.use(vhost('st.localhost', express.static('./st')));
// ACM application
app.use(vhost('acm.localhost', express.static('./acm')));
app.listen(8080, function () {
console.log('Fweb server running on port 8080!');
});
Angular
Below login function is shared by both ST and ACM applications
$scope.login = function(formValid){
$scope.incorrectCredentials = false;
if(formValid){
$http.get('/login',
{
params: {
username: $scope.username,
password: $scope.password
},
headers : {
'Accept' : 'application/json'
}
}
).then(function(response){
$scope.incorrectCredentials = false;
var obj = $cookies.getObject('auth');
console.log("auth is: " + obj);
$state.go($stateParams.origin);
}, function(response){
$scope.incorrectCredentials = true;
});
}
}
Express is able to successfully create the cookie 'auth' as I can see the Set-Cookie header in the /login service response. However, the browser is not attaching this cookie to the subsequent API requests that I'm making from my application(say st.localhost). I'm not able to read this cookie through Angular as well.
var obj = $cookies.getObject('auth');
console.log("auth is: " + obj);
results in obj being undefined.
I've a feeling that there is something wrong in the way I'm setting the domain of the cookie as 'localhost' from one of the sub-domains.
Any suggestions on what I may be doing wrong?
Thanks in advance.
This is the second time I'm providing an answer to my own question. I guess I need to be more patient next time onwards before posting a question. Anyways, I hope this answer is helpful for people who are stuck in a similar situation like I was.
Firstly, I found out that it is possible to share a cookie between subdomains even if you create it in one of the subdomains. However, there were some posts/answers which said otherwise.
What one needs to do while creating a cookie in one of the subdomains is that the parameter 'domain' needs to be set as the parent domain value. For example, if you are creating a cookie in say st.testserver.com then while setting a sharable cookie in it, the 'domain' parameter must be set as '.testserver'.
However, if your parent domain is also the Top Level Domain(TLD), then you won't be able to create a shared cookie in the subdomain. This is exactly what was happening to me earlier when I posted this question.
When I was using st.localhost and trying to create a cookie with 'domain' as '.localhost', it wasn't allowing me to do so because localhost here is the TLD. But when I renamed my domain name to st.testserver.com, I was able to create the cookie with 'domain' as '.testserver.com' because it wasn't the TLD anymore.
I hope someone can validate this answer once and let me know if I provided any incorrect information.
Thanks.
Cookies is domain specific , if you want access across domain, you need to use some cross store like cross-storage etc.
i have try to access(get request) my laravel api from ionic(angularjs) app.
but it keep getting bellow error.
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://128.xxx.xxx.xx/mobi/check/?username=achchu&apikey=1N7GyYRfq8bQnrFCCGgL
please help me to fix this
This response is based on the fact that it appears you have control of serve-side code.
Have you added CORS support to the response? If you have not done so it could be like this.
You could add a handler thus:
Route::head("/<path to resource>", function () {
$r = Response::make("hello");
//Access-Control-Allow-Origin: http://api.bob.com
// Access-Control-Allow-Credentials: true
$r->header("Access-Control-Allow-Origin", "<*|client request domain>")
->("Access-Control-Allow-Credentials", "true")
->("Access-Control-Request-Method", "GET");
});
Perhaps you could set this headers on the response you are sending itself, I'm not sure. If that is possible and you might use this sort of thing on several routes it's better you prepare it as a filter. You can read more on CORS at http://www.html5rocks.com/en/tutorials/cors/ .
If what you want to send is json data think about serving your response to support jsonp. You could do something like:
$normalData = Model::all();
// if the client made a jsonp style request
if (Input::has("callback")) {
$data = "<script>" . Input::get("callback") . "(" . json_encode($normalData) . ")";
return Response::make($data)->header("Content-Type", "appplication/javascript");
}else {
//if not then return normally
return Response::json($normalData);
}
You are trying to access api from one domain to a different domain.
There are many ways to overcome this issue,but since it is a get Request,use **jsonp*.
Try something like this
var url = ' http://128.xxx.xxx.xx/mobi/check/?username=achchu&apikey=1N7GyYRfq8bQnrFCCGgL&callback=JSON_CALLBACK';
$http.jsonp(url)
.success(function(data) {
});
At this point I am at a complete loss. I have searched the SO and the documentation and it says that Angular's $http will not perform an OPTION request if you put anything but content-type: application/json
I have the code below:
$scope.login = function(){
authFactory.login($scope.username, $scope.password).success(function(){
alert("here");
}).error(function(){
alert("ERROR");
});
app.factory('authFactory', ['$http', function ($http) {
var factory = {};
factory.login = function (username, password) {
var data = new Object();
data.username = username;
data.password = password;
return $http({
method: 'POST',
url: wsURL + 'login',
data: data,
headers: {
'Content-Type': 'x-www-form-urlencoded'
}});
}
return factory;
}]);
I keep getting an OPTIONS request and I cannot do anything about it.
My server is an embedded Jetty who is lacking an web.xml so I had to go at great lengths to configure it. At some point I seemed to work and not send the options request but out of the blue (without changing anything it stopped again).
Any help?
After all, SoluableNonagon was right that because I was sending a custom header I could not avoid the preflight request to Jetty. However, after searching for a way to make it work.
I am posting here for helping anyone else with similar needs.
The way to make this work with Jetty is to configure the CrossOriginFilter appropriately so as to not chain the request to your application if you do not want this (as was my case). The way to do this is below.
FilterHolder holder = new FilterHolder(new CrossOriginFilter());
holder.setInitParameter(CrossOriginFilter.ACCESS_CONTROL_ALLOW_METHODS_HEADER, "GET,POST,HEAD,OPTIONS");
holder.setInitParameter(CrossOriginFilter.ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, "*");
holder.setInitParameter(CrossOriginFilter.ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER, "true");
holder.setInitParameter(CrossOriginFilter.ALLOWED_HEADERS_PARAM, "*");
holder.setInitParameter(CrossOriginFilter.CHAIN_PREFLIGHT_PARAM, "false");
holder.setInitParameter(CrossOriginFilter.ACCESS_CONTROL_EXPOSE_HEADERS_HEADER, "true");
holder.setInitParameter(CrossOriginFilter.EXPOSED_HEADERS_PARAM, "X-AUTH-TOKEN");
Notice the statements responsible for allowing the header to pass and stop chaining it to the application are holder.setInitParameter(CrossOriginFilter.ALLOWED_HEADERS_PARAM, "*"); and holder.setInitParameter(CrossOriginFilter.CHAIN_PREFLIGHT_PARAM, "false");.
The last two statements in the codeblock above are responsible for allowing you to retrieve the token from the custom header. Not entirely relevant to the question but will definitely be needed.