The problem:
I am making a get request to my Laravel API and getting the following error
Cross-Origin Request Blocked: The Same Origin Policy disallows reading
the remote resource at http://www.example.com/exceptions-company-reports.
(Reason: CORS header 'Access-Control-Allow-Origin' missing)
I have followed these instructions on local and then on dev server, but I cannot figure out why Im getting this problem only on the dev server. I have even confirmed that php_zip and php_xml are enabled.
I am not getting errors in my logs.
Client side Angular code
getExceptionsReport: function getExceptionsReport() {
var apiBase = apiUrl + 'exceptions-company-reports';
var config = {
responseType: 'blob'
};
return $http.get(apiBase, config);
}
Server side:
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Requests\PublishCompanyreportingRequest;
use DB;
use Auth;
use Excel;
class CompanyreportingController extends Controller {
public function __construct() {
$this->middleware( 'jwt.auth' );
$this->middleware( 'role:company-reports' );
}
public function exceptionsCompanyReports( PublishCompanyreportingRequest $requestData ) {
$list = DB::table( 'exceptions_reports' )->select('created_at','account_number','customer_name','fp','seriel_number','comment','grade','item_number','description')->get();
$rows = array();
foreach($list as $item) {
$rows[] = array(
"Received" => $item->created_at,
"Account Number"=> $item->account_number,
"Customer Name" => $item->customer_name,
"FP"=> $item->fp,
"Serial Number" => $item->seriel_number,
"Comment" => $item->comment,
"Grade" => $item->grade,
"Item Number" => $item->item_number,
"Description" => $item->description,
);
}
Excel::create('Filename2', function($excel) use($rows) {
// Set the title
$excel->setTitle('Company| Company Report');
// Chain the setters
$excel->setCreator('Company')
->setCompany('Company');
$excel->sheet('Exceptions Report', function($sheet) use($rows) {
$sheet->fromArray($rows);
$sheet->row(1, function($row) {
// call cell manipulation methods
$row->setBackground('#DDDDDD');
$row->setFontFamily('Calibri');
$row->setFontSize(14);
});
$sheet->setStyle(array(
'font' => array(
'name' => 'Calibri',
'size' => 14
)
));
});
// Call them separately
$excel->setDescription('A demonstration to change the file properties');
})->download('xlsx');
}
}
Laravel-Excel will not add the headers for you. So, in order to avoid CORS issues, add this header:
Excel::create('Contactos', function($excel) use ($results) {
...
})->export('xlsx', ['Access-Control-Allow-Origin'=>'*']);
I came up with a work around of sorts, I guess a little better than a workaround as there is nothing wrong with the code.
I decided to save the file to the server instead and then send that file as a response instead of relying on the extension to do it. Still quite frustrating because I will never really know what the error was. What makes it more frustrating is that I know it can work as it does on my local. I wont mark this answer as correct until in case someone has a better one.
Excel::create('Filename2', function($excel) use($rows) {
// other code
})->save('xlsx');
return response()->file(storage_path().'/exports/Filename2.xlsx');
I am also deleting the file immediately after with a DELETE request
public function destroy( $id ) {
\File::Delete(storage_path().'/exports/Filename2.xlsx');
return response()->json( [ 'success' => 'Report has been removed from server' ], 200 );
}
In my case it happened because it did not have a zip extension installed.
It was showing me Cross-Origin Request Blocked but that was not the error
Related
I am creating a simple API with CakePHP 4, and I am having some issues with some CORS requests.
Access to XMLHttpRequest at 'http://localhost/myapp.api/elaborations/add.json' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Everything works with every other request and after some digging I've found that the error was an undefined index in my controller. If I fix that, the CORS error disappears. I just didn't see it in my log files, it's my bad.
It's a bit confusing seeing a CORS error because of a coding error though. I guess the issue could be in my CORS configuration, and hence this question.
This is what I've ended with, after a little bit of web search, trials and errors. I know it's ugly but I couldn't find anything better that actually worked.
How can I avoid having CORS errors for coding issues? I guess there is some redirect action somewhere, but I can't figure out how to avoid it.
<?php
namespace App\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Http\Server\MiddlewareInterface;
class CorsMiddleware implements MiddlewareInterface
{
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface
{
// Calling $handler->handle() delegates control to the *next* middleware
// In your application's queue.
$response = $handler->handle($request);
if ($request->getHeader('Origin')) {
$allowedDomains = [
'https://myapp.it',
'https://www.myapp.it',
'http://localhost:3000',
];
$origin = $_SERVER['HTTP_ORIGIN'];
if (in_array($origin, $allowedDomains)) {
header('Access-Control-Allow-Origin: ' . $origin);
}
header('Access-Control-Allow-Methods: POST, GET, PUT, PATCH, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: *');
if (strtoupper($request->getMethod()) === 'OPTIONS') {
exit(0);
}
}
return $response;
}
}
First of all, do not access superglobals in CakePHP directly, always use the abstracted APIs! Also you shouldn't echo data manually, that includes headers, again, use the abstracted APIs! Using superglobals and echoing data will only get you in trouble, it messes up the testing environment, it can lead to data not being sent (properly), etc.
That being said, for proper CORS coverage you also need to modify error handling, as the error controller will create a new response instance. You should be able to process the request/response in a custom exception renderer, at ExceptionRenderer::_getController(), like so:
// in src/Error/AppExceptionRenderer.php
namespace App\Error;
use App\Http\Middleware\CorsMiddleware;
use Cake\Controller\Controller;
use Cake\Error\ExceptionRenderer;
class AppExceptionRenderer extends ExceptionRenderer
{
protected function _getController(): Controller
{
$controller = parent::_getController();
$cors = new CorsMiddleware();
$response = $cors->setHeaders(
$controller->getRequest(),
$controller->getResponse()
);
return $controller->setResponse($response);
}
}
// in config/app.php
'Error' => [
'exceptionRenderer' => \App\Error\AppExceptionRenderer::class,
// ...
],
Your CORS middleware would supply the setHeaders() method accordingly, where you set the CORS headers if required, based on your example it could look something like this:
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface
{
$response = $handler->handle($request);
$response = $this->setHeaders($request, $response);
return $response;
}
public function setHeaders(
ServerRequestInterface $request,
ResponseInterface $response
): ResponseInterface
{
if ($request->getHeader('Origin')) {
$allowedDomains = [
'https://myapp.it',
'https://www.myapp.it',
'http://localhost:3000',
];
$origins = $request->getHeader('Origin');
$lastOrigin = end($origins);
if (in_array($lastOrigin, $allowedDomains, true)) {
$response = $response
->withHeader('Access-Control-Allow-Origin', $lastOrigin);
}
if (strtoupper($request->getMethod()) === 'OPTIONS') {
$response = $response
->withHeader(
'Access-Control-Allow-Methods',
'POST, GET, PUT, PATCH, DELETE, OPTIONS'
)
->withHeader('Access-Control-Allow-Headers', '*');
}
}
return $response;
}
See also
Cookbook > Error & Exception Handling > Custom ExceptionRenderer
Cookbook > Request & Response Objects
Cookbook > Middleware > Creating Middleware
I'm trying to make a reactjs application where an user can only login to one device at the time with the same user credentials. Unfortunately it isn't working.
I'm trying to authenticate a presence channel with reactjs to laravel but I get a 302 response.
reactjs:
Pusher.logToConsole = true;
var pusher = new Pusher("9028d58568392772df59", {
cluster: "eu",
forceTLS: true,
authEndpoint: '/broadcasting/auth',
auth: {
headers: {
'X-CSRF-Token': csrf_token
}
}
});
var channel = pusher.subscribe("presence-HandleCredentials");
channel.bind("sameCredentials", function(data) {
console.log(data);
alert(JSON.stringify(data));
});
channel:
Broadcast::channel('App.User', function ($user, $id = 1) {
return (int) $user->id === (int) $id;
});
broadcast:
public function boot()
{
Broadcast::routes(['middleware' => ['auth:web']]);
require base_path('routes/channels.php');
}
When I added this ['middleware' => ['auth:web']] I got the error. Before I added that I got a 403 error.
in the config\app.php I uncommented App\Providers\BroadcastServiceProvider::class,
Are there any tutorials out there that are build with laravel and reactjs for a presence channel?
does anyone know how to get past this 302 redirect?
Recently had the same issue with my laravel-websockets and laravel echo.
In my case I was unable to solve the 302, as Broadcast was unable to authenticate my logged in user. I was trying to subscribe to my private channel. So the workaround i found was that i manually created a POST route in web.php as "/broadcasting/auth". This is what my front-end requests to. So The updated code in web.php is as follows.
Route::post('/broadcasting/auth', function(Request $request){
$pusher = new Pusher\Pusher(
env('PUSHER_APP_KEY'),
env('PUSHER_APP_SECRET'),
env('PUSHER_APP_ID'),
array(
'cluster' => env('PUSHER_APP_CLUSTER'),
'useTLS' => false,
'host' => env('APP_URL'),
'port' => 6001,
'scheme' => 'http',
)
);
return $pusher->socket_auth($request->request->get('channel_name'),$request->request->get('socket_id'));
});
I was creating my own websocket that is why i had to mention the host & port within the options, you don't need to use it if you are Using Pusher. You can also add other middlewares to the routes if needed.
You have to comment out the following line in app/providers/BroadcastServiceProvider:
public function boot()
{
// Broadcast::routes();
require base_path('routes/channels.php');
}
so that the request can reach my broadcasting/auth route in web.php.
Try this. now this should return a 200 when the broadcasting/auth is requested by your client end with response of an auth code. Do let me know if this solves your problem.
I Have an issue with my code. My axios delete request doesn't delete data from database. I get response 200 but it's all.
Component
deleteComment(index){
axios.delete(this.uri + this.comments[index].id)
.catch(error=>{
console.log(error)
});
}
Controller
public function destroy(BlogComments $cid){
$comments->delete();
return response()->json([
'comments'=> $id,
'Message'=> "OK!"
]);
}
Console -> Network
Request URL: http://127.0.0.1:8000/api/blog-comments/4
Request Method: DELETE
Status Code: 200 OK
Remote Address: 127.0.0.1:8000
Referrer Policy: no-referrer-when-downgrade
When I make response form controller, I have only empty arrays
comments[]
UPDATE
I fixed it.
I replaced
Route::resources('/blog-comments', 'DashboardBlogCommentsController');
By
Route::delete('/blog-comments/{id}', 'DashboardBlogCommentsController#destroy');
Why resources doesn't work ? I used the same method in my other page and it's worked
Route::resource('/advantages', 'DashboardAdvantagesController');
public function destroy(HomepageAdvantages $advantage){
$advantage->delete();
return response()->json([
'advantage'=>$advantage,
'message'=> 'task created'
]);
}
Maybe a answer for:
Why resources doesn't work ?
Route
This is maybe a typo in your edit and not in your code, but is not resources:
Route::resources('/blog-comments', 'DashboardBlogCommentsController');
is resource:
Route::resource('/blog-comments', 'DashboardBlogCommentsController');
And based on the code before the UPDATE of your question:
Controller DashboardBlogCommentsController
public function destroy(BlogComments $comment){
// use delete() on the variable where you assigned the object
$comment->delete();
return response()->json([
// 'comments'=> $id, this no needed, the comment doesn't exist anymore
'message' => 'OK!' ], 202);
}
Hope it's helps
I am using Laravel 5.8.10, React 16.8, Laravel Echo Server 1.5.2, Redis 3.2.9, and Socket.io 2.2.0.
I am NOT using Pusher and don't want to use Pusher.
I am trying to create a basic chat system for site users. They log in normally using session authentication with email and password - all of that works fine.
There are 2 types of users: Brands and Influencers. Each has its own custom guard (web-brands & web-influencers). All session guards work normally.
I'm building the chat page using React. I can successfully join a public channel and receive messages on that public channel. However, the problem is when I try to make the channel private.
When I try to join a private channel, Laravel Echo Server sends an authentication request to: http://localhost:8000/broadcasting/auth.
But that returns the following 401 error:
{"message":"Unauthenticated."}
Client can not be authenticated, got HTTP status 401
Right now, I am trying to authenticate requests to /broadcasting/auth using a simple 'api_token' that is stored in the users tables (brands and influencers are the 2 users tables). This is a unique 60-character string.
I am trying this 'api_token' strategy because it sounds easier than setting up Laravel Passport, but perhaps I am wrong about that.
This is the constructor method from my React page:
import React, { Component } from 'react';
import Echo from "laravel-echo";
import Socketio from "socket.io-client";
constructor(props) {
super(props);
this.state = {
currentConversationId: conversations[0].id,
data: '',
};
this.selectConversation = this.selectConversation.bind(this);
let echo = new Echo({
broadcaster: 'socket.io',
host: 'http://localhost:6001',
client: Socketio,
auth: {
headers: {
// I currently have CSRF requirements disabled for /broadcasting/auth,
// but this should work fine once it is enabled anyway
'X-CSRF-Token': document.head.querySelector('meta[name="csrf-token"]'),
// I have the api_token hard-coded as I am trying to get it to work,
// but I have also used the javascript variable 'token' below
'api_token':'uUOyxRgCkVLKvp7ICZ0gXaELBPPbWEL0tUqz2Dv4TsFFc7JO4gv5kUi3WL3Q',
'Authorization':'Bearer: ' +'uUOyxRgCkVLKvp7ICZ0gXaELBPPbWEL0tUqz2Dv4TsFFc7JO4gv5kUi3WL3Q',
//'api_token':token,
//'Authorization':'Bearer: ' + token,
}
}
});
// Note that the ID of 1 is hardcoded for now until I get it to work
echo.private('brand.1')
.listen('SimpleMessageEvent', event => {
console.log('got something...');
console.log(event);
this.state.data = event;
});
}
Here you can see the in $php artisan route:list, the route is using auth:api middleware:
| GET|POST|HEAD | broadcasting/auth | Illuminate\Broadcasting\BroadcastController#authenticate | auth:api
Here is my BroadcastServiceProvider.php:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Broadcast;
class BroadcastServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
Broadcast::routes(['middleware' => ['auth:api']]);
require base_path('routes/channels.php');
}
}
Here is my auth.php:
<?php
return [
'defaults' => [
'guard' => 'web-brands',
'passwords' => 'brands',
],
'guards' => [
'web-brands' => [
'driver' => 'session',
'provider' => 'brands',
],
'web-influencers' => [
'driver' => 'session',
'provider' => 'influencers',
],
'api' => [
'driver' => 'token',
'provider' => 'brands2',
],
],
'providers' => [
'brands' => [
'driver' => 'eloquent',
'model' => App\Brand::class,
],
'influencers' => [
'driver' => 'eloquent',
'model' => App\Influencer::class,
],
'brands2' => [
'driver' => 'database',
'table' => 'brands',
],
],
'passwords' => [
'brands' => [
'provider' => 'brands',
'table' => 'password_resets',
'expire' => 60,
],
'influencers' => [
'provider' => 'influencers',
'table' => 'password_resets',
'expire' => 60,
],
],
];
Here is my channels.php:
Broadcast::channel('brand.{id}',true);
Note that I have the brand.{id} set it to return true by default. I have also tried this for channels.php:
Broadcast::channel('brand.{id}', function ($brand,$id) {
return $brand->id === Brand::find($id)->id;
});
I have already tried testing the simple api_token method by using a dummy route:
Route::get('test-test-test',function(){return 'asdf';})->middleware('auth:api');
This test works:
http://localhost:8000/test-test-test results in redirect
http://localhost:8000/test-test-test?api_token=123 results in redirect
http://localhost:8000/test-test-test?api_token=[the actual correct 60-character token] results in 'asdf'
Here is some info from my .env:
BROADCAST_DRIVER=redis
QUEUE_DRIVER=redis
CACHE_DRIVER=file
QUEUE_CONNECTION=database
SESSION_DRIVER=file
SESSION_LIFETIME=120
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
Here is my laravel-echo-server.json:
{
"authHost": "http://localhost:8000",
"authEndpoint": "/broadcasting/auth",
"clients": [],
"database": "redis",
"databaseConfig": {
"redis": {},
"sqlite": {
"databasePath": "/database/laravel-echo-server.sqlite"
}
},
"devMode": true,
"host": null,
"port": "6001",
"protocol": "http",
"socketio": {},
"sslCertPath": "",
"sslKeyPath": "",
"sslCertChainPath": "",
"sslPassphrase": "",
"subscribers": {
"http": true,
"redis": true
},
"apiOriginAllow": {
"allowCors": false,
"allowOrigin": "",
"allowMethods": "",
"allowHeaders": ""
}
}
Perhaps I am not sending the api_token correctly in the header of the laravel echo request?
UPDATE/EDIT:
Now I have tried removing the auth:api middleware for the /broadcasting/auth route. I'm not sure if that was the correct thing to do.
That now produces a 403 error:
Client can not be authenticated, got HTTP status 403
UPDATE 2 - IMPORTANT
So I know this is not recommended, but I started changing some things inside of the laravel source files... I got it to work finally and now that I have figured it out, I would like to override the source files that I changed instead of actually changing them. I did save the originals so I can easily revert back.
One big challenge was that while changing the source files, I was not able to use the where() method, only the find() method to lookup users.
The key function that needed changing was retrieveUser() (which is located inside of Illuminate/Broadcasting/Broadcasters/Broadcaster.php.
The problem was that it kept trying to run:
return $request->user();
...but that user() function never worked, which is why it always returned a 403 forbidden error. I think it is because the actual Laravel Echo request was sent from React (in javascript frontend), so there was no user object attached to the request. In other words, it was like a guest making the request. That explains why the public channels worked, but the private ones didn't.
I never did figure out how to get the user information to be sent with the request through React, but I did figure out a workaround.
Basically what I had to do:
In my controller, encrypt the ID of the user and pass it to javascript as a variable.
Pass the encrypted ID variable through the Echo request as part of the header.
Modify the retrieveUser() function to use find(Crypt::decrypt($id)) to lookup the user instead of ->user() (or where() which was strangely not allowed).
From what I can tell, this seems like a decent strategy from a security perspective, but perhaps readers could point out if that is actually correct.
To hack your way into a private channel, you would have to know the ID of the channel you want to listen to, then pass it as an encrypted variable in the header of the request.
Maybe a potential hacker could say that he/she wants to listen to private channel 'brand.1' and all they would have to do is encrypt the number 1 and pass it through the header. I guess I don't know how that works enough to know whether that is possible.
Anyway my goals now are:
converting this into an override setup instead of explicitly changing the Laravel source code.
figuring out if passing the encrypted ID through the request header is secure enough for production.
It does seem like the encrypted ID in the header (which does change every time you run the request) is more secure than simply passing through an 'api_token' which would be a value stored in the users table and is what most people seem to do.
On local host request to my WordPress app that is using the "wp rest api v2" is working as expected with no issues. Here is an example of my local host post request:
POST "http://127.0.0.1/plugin namespace/plugin-name/wp-json/plugin namespace/rest of the api url"
But on my hosted site the request is returning an error, similar request to my hosted site:
POST "http://my-domain.com/plugin-name/wp-json/plugin namespace/rest of the api url"
And I get on chrome console "400 (Bad Request)"
Object {code: "rest_invalid_param", message: "Invalid parameter(s): id", data: Object}
Another note the payload is identical in both requests:
$http.post(url, { id : 1, shortMessage:"a b c"}, {headers: {'X-WP-Nonce': "my nonce value"}}); // I am using angular $http to make the post request
I wrote how the url's look like above.
In my plugin php code I wrote:
function myplugin_register_endpoints(){
register_rest_route(
'plugin namespace',
'/rest of the api url',
array(
'methods' => 'POST',
'callback' => 'my_end_point',
'args' => array(
'id' => array(
'required' => true,
'validate_callback' => function($param, $request, $key) {
return is_numeric( $param ) and ! is_null(get_post($param));//numeric post id value and there is valid post for this id
},
'sanitize_calback' => 'absint'
)
),
'permission_callback' => function (WP_REST_Request $request) {
if(!is_user_logged_in()){
return new WP_Error('login error',__('You are not logged in','blabla'));
}
return true;
}
)
);
}
add_action('rest_api_init','myplugin_register_endpoints');
function my_end_point(WP_REST_Request $request) {
global $current_user;
$current_user = wp_get_current_user();
if($my_var){
return array('message' => $message,'items' => $items,'item'=>$item);
} else {
return new WP_Error('add friend error',__('message'),$request['id']);
}
}
I need to understand why the request is failing on my hosted site.
Thanks
K
I found my mistake.
it was on this line:
return is_numeric( $param ) and ! is_null(get_post($param));//numeric post id value and there is valid post for this id
I changed it to:
return is_numeric( $param );
It happened because I copy paste the register_rest_route(...){...} part without modifying the 'validate_callback' logic.
the param in this 'validate_callback' is user ID not post ID. so the part checking if the param is a post ID( ... and ! is_null(get_post($param)); ... ) is not relevant for this end point.
Therefore after omitting this check the end point passed the 'validate_callback' and stopped returning an error.