Call API Platform default operation from a custom one? - php

I'm working on security zone. After spending days on Symfony and API Platform docs, i realised that my concern is particula.
Talking about security in Symfony, I think it's cool for small or internal projects. We can add/remove profiles/roles like we want.
I want to do something special. I want user or client (who is not dev) to be able via the back office, add new profiles and assign rights. I use security zone instead of roles directly. I want to protect my actions(controller) under security zone.
Each controller gets "getSecurityZone()" method which can allow me to check that first (onkernel event) to see if the current user has rights (depending on their -unique- role/profile) on the ressource.
public function securityZone(): string
{
return 'BO_ZONE';
}
My concern: I don't want to rewrite the logic to retrieve data since API Platform already does it. So i tried to use event to run my security check. It seems working but How to call default operation (like GET) from my action ?
My entity :
* },
* collectionOperations={
* "get"={
* "method"="GET",
* "path"="/users",
* "controller"=UserListAction::class,
* "defaults"={"_api_receive"=false},
* "swagger_context"={
* "parameters"={
*
* }
* },
* },
* "post"
* }
* )
* #ORM\Entity(repositoryClass="App\Repository\UserRepository")
* #ORM\Table(name="users")
*/
class User implements UserInterface
{
My eventlistener :
public static function getSubscribedEvents()
{
return [
KernelEvents::VIEW => ['encodePassword', EventPriorities::PRE_WRITE],
KernelEvents::CONTROLLER => ['micheckSecurity', EventPriorities::PRE_READ],
];
}
//..
public function micheckSecurity(FilterControllerEvent $event)
{
$controller = $event->getController();
$method = $event->getRequest()->getMethod();
if (Request::METHOD_GET !== $method || $controller->getSecurityZone() !== Constants::SECZONE_BO_ZONE)
throw new AccessDeniedException("You are not author", 500);
return;
}
And my action(controller) :
class UserListAction
{
public function __invoke()
{
//Call default operation which return collection
}
public function securityZone(): string
{
return 'BO_ZONE';
}
}
In my _invoke() method, I want to call the read method of API Platform which will return the collection automatically.
Thank you for your attention.

Related

How to check (in template) if $user->can('access', $request) in CakePHP 4?

I've created a RequestPolicy in src/Policy/RequestPolicy.php to allow access to all actions of my SuperRubriquesController only to a "super-admin" user :
namespace App\Policy;
use Authorization\Policy\RequestPolicyInterface;
use Cake\Http\ServerRequest;
use Authorization\IdentityInterface;
class RequestPolicy implements RequestPolicyInterface
{
/**
* Method to check if the request can be accessed
*
* #param \Authorization\IdentityInterface|null $identity Identity
* #param \Cake\Http\ServerRequest $request Server Request
* #return bool
*/
public function canAccess($identity, ServerRequest $request)
{
if ($request->getParam('controller') === 'SuperRubriques' && $identity) {
return $identity->role === 'super-admin';
}
return true;
}
}
It works fine when I go to "/super-rubriques/index" or others actions of SuperRubriquesController but I'm wondering if there's a way to check if a user can access to a request from a template.
For example, I'd like to check if user can access to action index of SuperRubriquesController before to display the link.
if ($this->request->getAttribute('identity')->can('access', $requestToSuperRubriquesIndex)) {
echo $this->Html->link('Super Rubriques', ['controller' => 'SuperRubriques', 'action' => 'index']);
}
How can I build $requestToSuperRubriquesIndex ?
One way would be to use the with* methods of the current request object to create a clone with modified data:
$requestToSuperRubriquesIndex = $this->request
->withParam('controller', 'SuperRubriques')
->withParam('action', 'index');
See also
API > \Cake\Http\ServerRequest::withParam()

How to extend or make custom PasswordBroker sendResetLink() method in Laravel 5.8?

Currently the logic behind Resetting Password is that user must provide valid/registered e-mail to receive password recovery e-mail.
In my case I don't want to validate if the e-mail is registered or not due to security concerns and I want to just do the check in back-end and tell user that "If he has provided registered e-mail, he should get recovery e-mail shortly".
What I've done to achieve this is edited in vendor\laravel\framework\src\Illuminate\Auth\Passwords\PasswordBroker.php sendResetLink() method from this:
/**
* Send a password reset link to a user.
*
* #param array $credentials
* #return string
*/
public function sendResetLink(array $credentials)
{
// First we will check to see if we found a user at the given credentials and
// if we did not we will redirect back to this current URI with a piece of
// "flash" data in the session to indicate to the developers the errors.
$user = $this->getUser($credentials);
if (is_null($user)) {
return static::INVALID_USER;
}
// Once we have the reset token, we are ready to send the message out to this
// user with a link to reset their password. We will then redirect back to
// the current URI having nothing set in the session to indicate errors.
$user->sendPasswordResetNotification(
$this->tokens->create($user)
);
return static::RESET_LINK_SENT;
}
to this:
/**
* Send a password reset link to a user.
*
* #param array $credentials
* #return string
*/
public function sendResetLink(array $credentials)
{
// First we will check to see if we found a user at the given credentials and
// if we did not we will redirect back to this current URI with a piece of
// "flash" data in the session to indicate to the developers the errors.
$user = $this->getUser($credentials);
// if (is_null($user)) {
// return static::INVALID_USER;
// }
// Once we have the reset token, we are ready to send the message out to this
// user with a link to reset their password. We will then redirect back to
// the current URI having nothing set in the session to indicate errors.
if(!is_null($user)) {
$user->sendPasswordResetNotification(
$this->tokens->create($user)
);
}
return static::RESET_LINK_SENT;
}
This hard-coded option is not the best solution because it will disappear after update.. so I would like to know how can I extend or implement this change within the project scope within App folder to preserve this change at all times?
P.S. I've tried solution mentioned here: Laravel 5.3 Password Broker Customization but it didn't work.. also directory tree differs and I couldn't understand where to put new PasswordBroker.php file.
Thanks in advance!
Here are the steps you need to follow.
Create a new custom PasswordResetsServiceProvider. I have a folder (namespace) called Extensions where I'll place this file:
<?php
namespace App\Extensions\Passwords;
use Illuminate\Auth\Passwords\PasswordResetServiceProvider as BasePasswordResetServiceProvider;
class PasswordResetServiceProvider extends BasePasswordResetServiceProvider
{
/**
* Indicates if loading of the provider is deferred.
*
* #var bool
*/
protected $defer = true;
/**
* Register the service provider.
*
* #return void
*/
public function register()
{
$this->registerPasswordBroker();
}
/**
* Register the password broker instance.
*
* #return void
*/
protected function registerPasswordBroker()
{
$this->app->singleton('auth.password', function ($app) {
return new PasswordBrokerManager($app);
});
$this->app->bind('auth.password.broker', function ($app) {
return $app->make('auth.password')->broker();
});
}
}
As you can see this provider extends the base password reset provider. The only thing that changes is that we are returning a custom PasswordBrokerManager from the registerPasswordBroker method. Let's create a custom Broker manager in the same namespace:
<?php
namespace App\Extensions\Passwords;
use Illuminate\Auth\Passwords\PasswordBrokerManager as BasePasswordBrokerManager;
class PasswordBrokerManager extends BasePasswordBrokerManager
{
/**
* Resolve the given broker.
*
* #param string $name
* #return \Illuminate\Contracts\Auth\PasswordBroker
*
* #throws \InvalidArgumentException
*/
protected function resolve($name)
{
$config = $this->getConfig($name);
if (is_null($config)) {
throw new InvalidArgumentException(
"Password resetter [{$name}] is not defined."
);
}
// The password broker uses a token repository to validate tokens and send user
// password e-mails, as well as validating that password reset process as an
// aggregate service of sorts providing a convenient interface for resets.
return new PasswordBroker(
$this->createTokenRepository($config),
$this->app['auth']->createUserProvider($config['provider'] ?? null)
);
}
}
Again, this PasswordBrokerManager extends the base manager from laravel. The only difference here is the new resolve method which returns a new and custom PasswordBroker from the same namespace. So the last file we'll create a custom PasswordBroker in the same namespace:
<?php
namespace App\Extensions\Passwords;
use Illuminate\Auth\Passwords\PasswordBroker as BasePasswordBroker;
class PasswordBroker extends BasePasswordBroker
{
/**
* Send a password reset link to a user.
*
* #param array $credentials
* #return string
*/
public function sendResetLink(array $credentials)
{
// First we will check to see if we found a user at the given credentials and
// if we did not we will redirect back to this current URI with a piece of
// "flash" data in the session to indicate to the developers the errors.
$user = $this->getUser($credentials);
// if (is_null($user)) {
// return static::INVALID_USER;
// }
// Once we have the reset token, we are ready to send the message out to this
// user with a link to reset their password. We will then redirect back to
// the current URI having nothing set in the session to indicate errors.
if(!is_null($user)) {
$user->sendPasswordResetNotification(
$this->tokens->create($user)
);
}
return static::RESET_LINK_SENT;
}
}
As you can see we extend the default PasswordBroker class from Laravel and only override the method we need to override.
The final step is to simply replace the Laravel Default PasswordReset broker with ours. In the config/app.php file, change the line that registers the provider as such:
'providers' => [
...
// Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
App\Extensions\Passwords\PasswordResetServiceProvider::class,
...
]
That's all you need to register a custom password broker. Hope that helps.
The easiest solution here would be to place your customised code in app\Http\Controllers\Auth\ForgotPasswordController - this is the controller that pulls in the SendsPasswordResetEmails trait.
Your method overrides the one provided by that trait, so it will be called instead of the one in the trait. You could override the whole sendResetLinkEmail method with your code to always return the same response regardless of success.
public function sendResetLinkEmail(Request $request)
{
$this->validateEmail($request);
// We will send the password reset link to this user. Once we have attempted
// to send the link, we will examine the response then see the message we
// need to show to the user. Finally, we'll send out a proper response.
$response = $this->broker()->sendResetLink(
$request->only('email')
);
return back()->with('status', "If you've provided registered e-mail, you should get recovery e-mail shortly.");
}
You can just override the sendResetLinkFailedResponse method in your ForgetPasswordController class.
protected function sendResetLinkFailedResponse(Request $request, $response)
{
return $this->sendResetLinkResponse($request, Password::RESET_LINK_SENT);
}
We'll just send the successful response even if the validation failed.

Custom User Provider Laravel 5.6

Hi I'm trying to add a permissions check from the laravel-permissions library to the web guarded routes.
I have tried to change the user provider so that i can override the method validateCredentials to check the eloquent credentials and then check if there are permissions.
However it seems that you can't change the driver in config/auth.php from database or eloquent, i just get an exception due to this check in
Illuminate/Auth/CreatesUserProviders.php line 24
/**
* Create the user provider implementation for the driver.
*
* #param string|null $provider
* #return \Illuminate\Contracts\Auth\UserProvider|null
*
* #throws \InvalidArgumentException
*/
public function createUserProvider($provider = null)
{
if (is_null($config = $this->getProviderConfiguration($provider))) {
return;
}
if (isset($this->customProviderCreators[$driver = ($config['driver'] ?? null)])) {
return call_user_func(
$this->customProviderCreators[$driver], $this->app, $config
);
}
switch ($driver) {
case 'database':
return $this->createDatabaseProvider($config);
case 'eloquent':
return $this->createEloquentProvider($config);
default:
throw new InvalidArgumentException(
"Authentication user provider [{$driver}] is not defined."
);
}
}
How are you meant to override this validateCredentials call? / allow for only users with a certain permission from laravel-permissions to login to the web guard

Testing Stripe in Laravel

I'm creating a subscription-based SaaS platform in Laravel, where Laravel Cashier does not suit my needs. Therefore I need to implement the subscription-engine myself using the Stripe library.
I found it easy to implement the connection between Laravel and Stripe via hooking into the creation and deletion events of a Subscription class, and then create or cancel a Stripe subscription accordingly.
The Stripe library is unfortunately largely based on calling static methods on some predefined classes (.. like \Stripe\Charge::create()).
This makes it hard for me to test, as you normally would allow dependency injection of some custom client for mocking, but since the Stripe library is referenced statically, there is no client to inject. Is there any way of creating a Stripe client class or such, that I can mock?
Hello from the future!
I was just digging into this. All those classes extend from Stripe's ApiResource class, keep digging and you'll discover that when the library is about to make an HTTP request it calls $this->httpClient(). The httpClient method returns a static reference to a variable called $_httpClient. Conveniently, there is also a static method on the Stripe ApiRequestor class called setHttpClient which accepts an object which is supposed to implement the Stripe HttpClient\ClientInterface (this interface only describes a single method called request).
Soooooo, in your test you can make a call to ApiRequestor::setHttpClient passing it an instance of your own http client mock. Then whenever Stripe makes an HTTP request it will use your mock instead of its default CurlClient. Your responsibility is then have your mock return well-formed Stripe-esque responses and your application will be none the wiser.
Here is a very dumb fake that I've started using in my tests:
<?php
namespace Tests\Doubles;
use Stripe\HttpClient\ClientInterface;
class StripeHttpClientFake implements ClientInterface
{
private $response;
private $responseCode;
private $headers;
public function __construct($response, $code = 200, $headers = [])
{
$this->setResponse($response);
$this->setResponseCode($code);
$this->setHeaders($headers);
}
/**
* #param string $method The HTTP method being used
* #param string $absUrl The URL being requested, including domain and protocol
* #param array $headers Headers to be used in the request (full strings, not KV pairs)
* #param array $params KV pairs for parameters. Can be nested for arrays and hashes
* #param boolean $hasFile Whether or not $params references a file (via an # prefix or
* CURLFile)
*
* #return array An array whose first element is raw request body, second
* element is HTTP status code and third array of HTTP headers.
* #throws \Stripe\Exception\UnexpectedValueException
* #throws \Stripe\Exception\ApiConnectionException
*/
public function request($method, $absUrl, $headers, $params, $hasFile)
{
return [$this->response, $this->responseCode, $this->headers];
}
public function setResponseCode($code)
{
$this->responseCode = $code;
return $this;
}
public function setHeaders($headers)
{
$this->headers = $headers;
return $this;
}
public function setResponse($response)
{
$this->response = file_get_contents(base_path("tests/fixtures/stripe/{$response}.json"));
return $this;
}
}
Hope this helps :)
Based off Colin's answer, here is an example that uses a mocked interface to test creating a subscription in Laravel 8.x.
/**
* #test
*/
public function it_subscribes_to_an_initial_plan()
{
$client = \Mockery::mock(ClientInterface::class);
$paymentMethodId = Str::random();
/**
* Creates initial customer...
*/
$customerId = 'somecustomerstripeid';
$client->shouldReceive('request')
->withArgs(function ($method, $path, $params, $opts) use ($paymentMethodId) {
return $path === "https://api.stripe.com/v1/customers";
})->andReturn([
"{\"id\": \"{$customerId}\" }", 200, []
]);
/**
* Retrieves customer
*/
$client->shouldReceive('request')
->withArgs(function ($method, $path, $params) use ($customerId) {
return $path === "https://api.stripe.com/v1/customers/{$customerId}";
})->andReturn([
"{\"id\": \"{$customerId}\", \"invoice_settings\": {\"default_payment_method\": \"{$paymentMethodId}\"}}", 200, [],
]);
/**
* Set payment method
*/
$client->shouldReceive('request')
->withArgs(function ($method, $path, $params) use ($paymentMethodId) {
return $path === "https://api.stripe.com/v1/payment_methods/{$paymentMethodId}";
})->andReturn([
"{\"id\": \"$paymentMethodId\"}", 200, [],
]);
$subscriptionId = Str::random();
$itemId = Str::random();
$productId = Str::random();
$planName = Plan::PROFESSIONAL;
$plan = Plan::withName($planName);
/**
* Subscription request
*/
$client->shouldReceive('request')
->withArgs(function ($method, $path, $params, $opts) use ($paymentMethodId, $plan) {
$isSubscriptions = $path === "https://api.stripe.com/v1/subscriptions";
$isBasicPrice = $opts["items"][0]["price"] === $plan->stripe_price_id;
return $isSubscriptions && $isBasicPrice;
})->andReturn([
"{
\"object\": \"subscription\",
\"id\": \"{$subscriptionId}\",
\"status\": \"active\",
\"items\": {
\"object\": \"list\",
\"data\": [
{
\"id\": \"{$itemId}\",
\"price\": {
\"object\": \"price\",
\"id\": \"{$plan->stripe_price_id}\",
\"product\": \"{$productId}\"
},
\"quantity\": 1
}
]
}
}", 200, [],
]);
ApiRequestor::setHttpClient($client);
$this->authenticate($this->user);
$res = $this->putJson('/subscribe', [
'plan' => $planName,
'payment_method_id' => $paymentMethodId,
]);
$res->assertSuccessful();
// Actually interesting assertions go here
}

Laravel, WebSockets - Verify user on server

I currently ran into the problem of handling the authentification of a user on the server, using Laravel and RachetPHP.
What I tried so far:
I changed the driver type of the session to database, giving me an id and payload column. Using \Session::getId() returns a 40 character string.
The cookie information, sent by the WebSocket-Connection does contain a XSRF-TOKEN and a laravel_session, both containing > 200 characters string. The database ID of the users session differs from the id, returned by \Session::getId().
I am already sending the current CSRF-token via the websocket message, but I have no clue how to verify it (the built-in verifier uses requests - which I don't have in the websocket server scope).
Generic Use case:
A User posts a comment in thread. The payload of the sent object would then be:
Something to verify the user (an ID or a token).
The comment itself
If you were to send the user ID, anyone could temper the packet and send the message under another ones user.
My use case:
A user can have n-characters. A character has an avatar, an id, a name, etc.
The user is only used to:
authenticate at the server.
access his characters, and thus perform basic CRUD operations on his characters.
I also have a table locations - a "virtual place", a character can be in... so I got a one-to-one relationship between character and location. The user (character) can then send messages in a location via websocket. The message is inserted at the database on the server. At this point, I need to know:
If the user is authenticated (csrf-token ?)
If the user is the owner of the character (it's very simple to spoof the request with another user's character id)
If you need more information, please let me know.
So this is how I solved this a while ago. In my example, I'm working with Socket.IO, but I'm pretty sure you can easily rewrite the Socket.IO part to get it to work with RachetPHP as well.
Socket Server
The socket server depends on the files cookie.js and array.js, and the node modules express, http, socket.io, request and dotenv. I'm not the original author of cookie.js, but there is no author mentioned in the comments, so I'm not able to give any credits for this, sorry.
This is the server.js file which starts the server. It is a simple socket server that tracks who is currently online. The interesting part however is when the server makes a POST request to socket/auth on the Laravel application:
var express = require('express');
var app = express();
var server = require('http').Server(app)
var io = require('socket.io')(server);
var request = require('request');
var co = require('./cookie.js');
var array = require('./array.js');
// This loads the Laravel .env file
require('dotenv').config({path: '../.env'});
server.listen(process.env.SOCKET_SERVER_PORT);
var activeSockets = {};
var disconnectTimeouts = {};
// When a client connects
io.on('connection', function(socket)
{
console.log('Client connected...');
// Read the laravel_session cookie.
var cookieManager = new co.cookie(socket.handshake.headers.cookie);
var sess = cookieManager.get("laravel_session"); // Rename "laravel_session" to whatever you called it
// This is where the socket asks the Laravel app to authenticate the user
request.post('http://' + process.env.SOCKET_SERVER_HOST + '/socket/auth?s=' + sess, function(error, response, body)
{
try {
// Parse the response from the server
body = JSON.parse(body);
}
catch(e)
{
console.log('Error while parsing JSON', e);
error = true;
}
if ( ! error && response.statusCode == 200 && body.authenticated)
{
// Assign users ID to the socket
socket.userId = body.user.id;
if ( ! array.contains(activeSockets, socket.userId))
{
// The client is now 'active'
activeSockets.push(socket.userId);
var message = body.user.firstname + ' is now online!';
console.log(message);
// Tell everyone that the user has joined
socket.broadcast.emit('userJoined', socket.userId);
}
else if (array.hasKey(disconnectTimeouts, 'user_' + socket.userId))
{
clearTimeout(disconnectTimeouts['user_' + socket.userId]);
delete disconnectTimeouts['user_id' + socket.userId];
}
socket.on('disconnect', function()
{
// The client is 'inactive' if he doesn't reastablish the connection within 10 seconds
// For a 'who is online' list, this timeout ensures that the client does not disappear and reappear on each page reload
disconnectTimeouts['user_' + socket.userId] = setTimeout(function()
{
delete disconnectTimeouts['user_' + socket.userId];
array.remove(activeSockets, socket.userId);
var message = body.user.firstname + ' is now offline.';
console.log(message);
socket.broadcast.emit('userLeft', socket.userId);
}, 10000);
});
}
});
});
I added some comments to the code, so it should be pretty self-explanatory. Please note that I added SOCKET_SERVER_HOST and SOCKET_SERVER_PORT to my Laravel .env-file in order to be able to change the host and port without editing the code and run the server on different environments.
SOCKET_SERVER_HOST = localhost
SOCKET_SERVER_PORT = 1337
Authenticating a user by a session cookie with Laravel
This is the SocketController which parses the cookie and responds whether the user could be authenticated or not (JSON response). Its the same mechanism that you described in your answer. It's not the best design to handle the cookie parsing in the controller, but it should be OK in this case, because the controller only handles that one thing and its functionality isn't used at another point in the application.
/app/Http/Controllers/SocketController.php
<?php namespace App\Http\Controllers;
use App\Http\Requests;
use App\Users\UserRepositoryInterface;
use Illuminate\Auth\Guard;
use Illuminate\Database\DatabaseManager;
use Illuminate\Encryption\Encrypter;
use Illuminate\Http\Request;
use Illuminate\Routing\ResponseFactory;
/**
* Class SocketController
* #package App\Http\Controllers
*/
class SocketController extends Controller {
/**
* #var Encrypter
*/
private $encrypter;
/**
* #var DatabaseManager
*/
private $database;
/**
* #var UserRepositoryInterface
*/
private $users;
/**
* Initialize a new SocketController instance.
*
* #param Encrypter $encrypter
* #param DatabaseManager $database
* #param UserRepositoryInterface $users
*/
public function __construct(Encrypter $encrypter, DatabaseManager $database, UserRepositoryInterface $users)
{
parent::__construct();
$this->middleware('internal');
$this->encrypter = $encrypter;
$this->database = $database;
$this->users = $users;
}
/**
* Authorize a user from node.js socket server.
*
* #param Request $request
* #param ResponseFactory $response
* #param Guard $auth
* #return \Symfony\Component\HttpFoundation\Response
*/
public function authenticate(Request $request, ResponseFactory $response, Guard $auth)
{
try
{
$payload = $this->getPayload($request->get('s'));
} catch (\Exception $e)
{
return $response->json([
'authenticated' => false,
'message' => $e->getMessage()
]);
}
$user = $this->users->find($payload->{$auth->getName()});
return $response->json([
'authenticated' => true,
'user' => $user->toArray()
]);
}
/**
* Get session payload from encrypted laravel session.
*
* #param $session
* #return object
* #throws \Exception
*/
private function getPayload($session)
{
$sessionId = $this->encrypter->decrypt($session);
$sessionEntry = $this->getSession($sessionId);
$payload = base64_decode($sessionEntry->payload);
return (object) unserialize($payload);
}
/**
* Fetches base64 encoded session string from the database.
*
* #param $sessionId
* #return mixed
* #throws \Exception
*/
private function getSession($sessionId)
{
$sessionEntry = $this->database->connection()
->table('sessions')->select('*')->whereId($sessionId)->first();
if (is_null($sessionEntry))
{
throw new \Exception('The session could not be found. [Session ID: ' . $sessionId . ']');
}
return $sessionEntry;
}
}
In the constructor you can see that I refer to the internal middleware. I added this middleware to only allow the socket server to make requests to socket/auth.
This is what the middleware looks like:
/app/Http/Middleware/InternalMiddleware.php
<?php namespace App\Http\Middleware;
use Closure;
use Illuminate\Routing\ResponseFactory;
class InternalMiddleware {
/**
* #var ResponseFactory
*/
private $response;
/**
* #param ResponseFactory $response
*/
public function __construct(ResponseFactory $response)
{
$this->response = $response;
}
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
if (preg_match(env('INTERNAL_MIDDLEWARE_IP'), $request->ip()))
{
return $next($request);
}
return $this->response->make('Unauthorized', 401);
}
}
To get this middleware to work, register it in the Kernel and add the INTERNAL_MIDDLEWARE_IP property - that is just a regular expression defining which IP addresses are allowed - to your .env-file:
Local testing (any IP):
INTERNAL_MIDDLEWARE_IP = /^.*$/
Production env:
INTERNAL_MIDDLEWARE_IP = /^192\.168\.0\.1$/
I'm sorry I could not help you out with RachetPHP, but I think you get a good idea how this can be solved.
I think I found a solution. Although not very clean, it does what it's supposed to do (I guess...)
The WebSocket-Server gets started by an Artisan Command (by mmochetti#github). I inject these classes into the Command:
Illuminate\Contracts\Encryption\Encrypter
App\Contracts\CsrfTokenVerifier - a custom CsrfTokenVerifier, that simply compares 2 strings (going to put more of the follow logic code in there)
I pass these instances from the command to the server. On the onMessage method, I parse the message sent, containing:
The CSRF-Token of the user
The character-id of the user
I then check if the token is valid, and if the user is the owner of the character.
public function onMessage(ConnectionInterface $from, NetworkMessage $message) {
if (!$this->verifyCsrfToken($from, $message)) {
throw new TokenMismatchException;
}
if (!$this->verifyUser($from, $message)) {
throw new \Exception('test');
}
...
}
private function verifyUser(ConnectionInterface $conn, NetworkMessage $message) {
$cookies = $conn->WebSocket->request->getCookies();
$laravel_session = rawurldecode($cookies['laravel_session']);
$id = $this->encrypter->decrypt($laravel_session);
$session = Session::find($id);
$payload = unserialize(base64_decode($session->payload));
$user_id = $payload['user_id'];
$user = User::find($user_id);
$characters = $this->characterService->allFrom($user);
$character_id = $message->getHeader()['character_id'];
return $characters->contains($character_id);
}
private function verifyCsrfToken($from, NetworkMessage $message) {
$header = $this->getHeaderToken($from);
return $this->verifier->tokensMatch($header, $message->getId());
}
The code could be cleaner for sure, but as a quick hack, it works. I think, instead of using a model for the Session, I should use the Laravel DatabaseSessionHandler.
For Laravel > 5 i use this code:
$cookies = $conn->WebSocket->request->getCookies();
$laravel_session = rawurldecode($cookies['laravel_session']);
$id = $this->encrypter->decrypt($laravel_session);
if(Config::get('session.driver', 'file') == 'file')
{
$session = File::get(storage_path('framework/sessions/' . $id));
}
$session = array_values(unserialize($session));
return $session[4]; // todo: Hack, please think another solution
To get cookies from client through websocket you must change domain in session config and change everywhere websocket host to your domain:
'domain' => 'your.domain.com',

Categories