A few weeks ago I was struggling with setting up real time broadcasting with Laravel and socket.io. The biggest problem was security issue. So now I would like to show my solution of this problem. If you have any comments or want to suggest a more efficient way of solving this problem - please share. So let's get started.
Technology stack used in this example : Laravel 5.1, Angular 1.4, Redis, NodeJS, socket.io, JWT.
Laravel
I was using REST API and JWT auth in my app. So first thing to do is to set up config and JWT token in config controller.
composer require tymon/jwt-auth
Add provider :
Tymon\JWTAuth\Providers\JWTAuthServiceProvider::class
and aliases :
'JWTAuth' => Tymon\JWTAuth\Facades\JWTAuth::class,
'JWTFactory' => Tymon\JWTAuth\Facades\JWTFactory::class
And finally run this command to generate jwt config:
php artisan jwt:generate
use Illuminate\Contracts\Auth\Guard;
use Tymon\JWTAuth\JWTAuth;
use Carbon\Carbon;
class CfgController extends Controller
{
protected $JWTAuth;
protected $auth;
public function __construct(JWTAuth $JWTAuth, Guard $auth)
{
$this->JWTAuth = $JWTAuth;
$this->auth = $auth;
}
public function index()
{
$user = $this->auth->user();
$tokenId = base64_encode(mcrypt_create_iv(32, MCRYPT_DEV_URANDOM));
$issuedAt = Carbon::now()->timestamp;
$notBefore = $issuedAt; //Adding 10 seconds
$expire = $notBefore + 6*60*60; // Adding 6 hours
/*
* Create the token as an array
*/
$data = [
'iat' => $issuedAt, // Issued at: time when the token was generated
'jti' => $tokenId, // Json Token Id: an unique identifier for the token
'iss' => 'https://example.com', // Issuer
'nbf' => $notBefore, // Not before
'exp' => $expire, // Expire
'data' => [ // Data related to the signed user
'userId' => \Auth::id(), // userid from the users table
]
];
$response = [
'jwt' => $this->JWTAuth->fromUser($user, $data),
'node_url' => 'https://127.0.0.1:6001', //better to move this to .env file
];
return response()->json($response);
}
Now you can set up route for this action in routes.php file.
For example:
Route::get('/cfg', 'CfgController@index');
Also we should add Laravel event to perform broadcasting.
Example:
class NewMessage extends Event implements ShouldBroadcast
{
use SerializesModels;
public $user;
public $message;
private $recipientsIds;
public function __construct($message, array $recipients)
{
$ids = [];
foreach($recipients as $recipient) {
$ids[] = $recipient['user']['id'];
}
$this->message = $message;
$this->recipientsIds = $ids;
}
public function broadcastOn()
{
$events = [];
foreach($this->recipientsIds as $res) {
$events[] = 'user.' . $res;
}
return $events;
}
public function broadcastWith()
{
return ['message' => $this->message];
//this is not necessary. All public properties are automatically broadcast
}
}
AngularJS
You can reference config url in your angular application.
For example: template.blade.php
deferredBootstrapper.bootstrap({
element: document.documentElement,
module: 'app',
resolve: {
APP_CONFIG: ['$http', '$q', function ($http, $q) {
var deferred = $q.defer();
// GET api cfg token
$http.get('/cfg', {withCredentials: true}).success(function(resp){
deferred.resolve(resp);
}
});
return deferred.promise;
}]
}
});
To use deferred bootstrap you should install "angular-deferred-bootstrap" package.
Now, you can use APP_CONFIG in your controllers.
In my case I use it to store JWT token. In your angular controller or service you should subscribe to socket.io events. Example:
function RealTimeService(APP_CONFIG){
var socket = io(APP_CONFIG.node_url, {secure: true, query: 'jwt=' + APP_CONFIG.jwt});
socket.on('App\\Events\\NewMessage', function(data) {
console.log(data);
//here you can persist all your data passed in NewMessage event
});
}
Pay attention to this code:
query: 'jwt=' + APP_CONFIG.jwt
We will use this jwt token on nodeJS server to get user id.
And now the most important part of application.
socket.io + nodeJS server
First install needed dependencies via npm:
ioredis, jsonwebtoken, node-env-file, socket.io.
Of course, Redis server should be installed and running.
Also you can add to your .env file SOCKETIO_PORT, JWT_SECRET, REDIS_DB and ssl certificate urls if needed. Or simply hardcode in your js file. Example of server.js:
var ENV = require('node-env-file')(__dirname + '/../.env');
var fs = require('fs');
var Redis = require('ioredis');
var redis = new Redis({
db: ENV.REDIS_DB || 0
});
var jwt = require('jsonwebtoken');
//I’m using ssl, but this is not a must
var ssl_conf = {
key: (ENV.SOCKETIO_SSL_KEY_FILE ? fs.readFileSync(ENV.SOCKETIO_SSL_KEY_FILE) : null),
cert: (ENV.SOCKETIO_SSL_CERT_FILE ? fs.readFileSync(ENV.SOCKETIO_SSL_CERT_FILE) : null),
ca: (ENV.SOCKETIO_SSL_CA_FILE ? [fs.readFileSync(ENV.SOCKETIO_SSL_CA_FILE1), fs.readFileSync(ENV.SOCKETIO_SSL_CA_FILE2)] : null)
};
var app = require('https').createServer(ssl_conf, handler);
var io = require('socket.io').listen(app);
var cookie = require('cookie');
function handler(req, res) {
res.writeHead(200);
res.end("SomeMessage");
}
app.listen(ENV.SOCKETIO_PORT);
io.use(function(socket, next) {
var decoded;
try {
decoded = jwt.verify(socket.handshake.query.jwt, ENV.JWT_SECRET);
} catch (err) {
console.error(err);
next(new Error('Invalid token!'));
}
if (decoded) {
// everything went fine - save userId as property of given connection instance
socket.userId = decoded.data.userId; // save user id we just got from the token, to be used later
next();
} else {
// invalid token - terminate the connection
next(new Error('Invalid token!'));
}
});
io.on('connection', function(socket) {
socket.join('user.' + socket.userId);
});
redis.psubscribe('*', function(err, count) {
//
});
redis.on('pmessage', function(subscribed, channel, message) {
message = JSON.parse(message);
io.to(channel).emit(message.event, message.data);
});
console.log('Starting on port : '+ENV.SOCKETIO_PORT);
So, now when user is connecting to channel we are parsing its JWT and setting userId to socket.
After that, we are connecting user to his own room using socket.join(…).
And emitting this data to the client.
Using this approach, data is securely hidden from other clients. Also, if you are not using JWT for your authorization you can grab userId from cookie. But pay attention to your nodeJS server. It should run on the same domain as your app.