I am trying to use Socket.IO in Node.js, and am trying to allow the server to give an identity to each of the Socket.IO clients. As the socket code is outside the scope of the http server code, it doesn't have easy access to the request information sent, so I'm assuming it will need to be sent up during the connection. What is the best way to
1) get the information to the server about who is connecting via Socket.IO
2) authenticate who they say they are (I'm currently using Express, if that makes things any easier)
Use connect-redis and have redis as your session store for all authenticated users. Make sure on authentication you send the key (normally req.sessionID) to the client. Have the client store this key in a cookie.
On socket connect (or anytime later) fetch this key from the cookie and send it back to the server. Fetch the session information in redis using this key. (GET key)
Eg:
Server side (with redis as session store):
req.session.regenerate...
res.send({rediskey: req.sessionID});
Client side:
//store the key in a cookie
SetCookie('rediskey', <%= rediskey %>); //http://msdn.microsoft.com/en-us/library/ms533693(v=vs.85).aspx
//then when socket is connected, fetch the rediskey from the document.cookie and send it back to server
var socket = new io.Socket();
socket.on('connect', function() {
var rediskey = GetCookie('rediskey'); //http://msdn.microsoft.com/en-us/library/ms533693(v=vs.85).aspx
socket.send({rediskey: rediskey});
});
Server side:
//in io.on('connection')
io.on('connection', function(client) {
client.on('message', function(message) {
if(message.rediskey) {
//fetch session info from redis
redisclient.get(message.rediskey, function(e, c) {
client.user_logged_in = c.username;
});
}
});
});
I also liked the way pusherapp does private channels.
A unique socket id is generated and
sent to the browser by Pusher. This is
sent to your application (1) via an
AJAX request which authorizes the user
to access the channel against your
existing authentication system. If
successful your application returns an
authorization string to the browser
signed with you Pusher secret. This is
sent to Pusher over the WebSocket,
which completes the authorization (2)
if the authorization string matches.
Because also socket.io has unique socket_id for every socket.
socket.on('connect', function() {
console.log(socket.transport.sessionid);
});
They used signed authorization strings to authorize users.
I haven't yet mirrored this to socket.io, but I think it could be pretty interesting concept.
I know this is bit old, but for future readers in addition to the approach of parsing cookie and retrieving the session from the storage (eg. passport.socketio ) you might also consider a token based approach.
In this example I use JSON Web Tokens which are pretty standard. You have to give to the client page the token, in this example imagine an authentication endpoint that returns JWT:
var jwt = require('jsonwebtoken');
// other requires
app.post('/login', function (req, res) {
// TODO: validate the actual user user
var profile = {
first_name: 'John',
last_name: 'Doe',
email: 'john#doe.com',
id: 123
};
// we are sending the profile in the token
var token = jwt.sign(profile, jwtSecret, { expiresInMinutes: 60*5 });
res.json({token: token});
});
Now, your socket.io server can be configured as follows:
var socketioJwt = require('socketio-jwt');
var sio = socketIo.listen(server);
sio.set('authorization', socketioJwt.authorize({
secret: jwtSecret,
handshake: true
}));
sio.sockets
.on('connection', function (socket) {
console.log(socket.handshake.decoded_token.email, 'has joined');
//socket.on('event');
});
The socket.io-jwt middleware expects the token in a query string, so from the client you only have to attach it when connecting:
var socket = io.connect('', {
query: 'token=' + token
});
I wrote a more detailed explanation about this method and cookies here.
Here is my attempt to have the following working:
express: 4.14
socket.io: 1.5
passport (using sessions): 0.3
redis: 2.6 (Really fast data structure to handle sessions; but you can use others like MongoDB too. However, I encourage you to use this for session data + MongoDB to store other persistent data like Users)
Since you might want to add some API requests as well, we'll also use http package to have both HTTP and Web socket working in the same port.
server.js
The following extract only includes everything you need to set the previous technologies up. You can see the complete server.js version which I used in one of my projects here.
import http from 'http';
import express from 'express';
import passport from 'passport';
import { createClient as createRedisClient } from 'redis';
import connectRedis from 'connect-redis';
import Socketio from 'socket.io';
// Your own socket handler file, it's optional. Explained below.
import socketConnectionHandler from './sockets';
// Configuration about your Redis session data structure.
const redisClient = createRedisClient();
const RedisStore = connectRedis(Session);
const dbSession = new RedisStore({
client: redisClient,
host: 'localhost',
port: 27017,
prefix: 'stackoverflow_',
disableTTL: true
});
// Let's configure Express to use our Redis storage to handle
// sessions as well. You'll probably want Express to handle your
// sessions as well and share the same storage as your socket.io
// does (i.e. for handling AJAX logins).
const session = Session({
resave: true,
saveUninitialized: true,
key: 'SID', // this will be used for the session cookie identifier
secret: 'secret key',
store: dbSession
});
app.use(session);
// Let's initialize passport by using their middlewares, which do
//everything pretty much automatically. (you have to configure login
// / register strategies on your own though (see reference 1)
app.use(passport.initialize());
app.use(passport.session());
// Socket.IO
const io = Socketio(server);
io.use((socket, next) => {
session(socket.handshake, {}, next);
});
io.on('connection', socketConnectionHandler);
// socket.io is ready; remember that ^this^ variable is just the
// name that we gave to our own socket.io handler file (explained
// just after this).
// Start server. This will start both socket.io and our optional
// AJAX API in the given port.
const port = 3000; // Move this onto an environment variable,
// it'll look more professional.
server.listen(port);
console.info(`🌐 API listening on port ${port}`);
console.info(`🗲 Socket listening on port ${port}`);
sockets/index.js
Our socketConnectionHandler, I just don't like putting everything inside server.js (even though you perfectly could), especially since this file can end up containing quite a lot of code pretty quickly.
export default function connectionHandler(socket) {
const userId = socket.handshake.session.passport &&
socket.handshake.session.passport.user;
// If the user is not logged in, you might find ^this^
// socket.handshake.session.passport variable undefined.
// Give the user a warm welcome.
console.info(`⚡︎ New connection: ${userId}`);
socket.emit('Grettings', `Grettings ${userId}`);
// Handle disconnection.
socket.on('disconnect', () => {
if (process.env.NODE_ENV !== 'production') {
console.info(`⚡︎ Disconnection: ${userId}`);
}
});
}
Extra material (client):
Just a very basic version of what the JavaScript socket.io client could be:
import io from 'socket.io-client';
const socketPath = '/socket.io'; // <- Default path.
// But you could configure your server
// to something like /api/socket.io
const socket = io.connect('localhost:3000', { path: socketPath });
socket.on('connect', () => {
console.info('Connected');
socket.on('Grettings', (data) => {
console.info(`Server gretting: ${data}`);
});
});
socket.on('connect_error', (error) => {
console.error(`Connection error: ${error}`);
});
References:
I just couldn't reference inside the code, so I moved it here.
1: How to set up your Passport strategies: https://scotch.io/tutorials/easy-node-authentication-setup-and-local#handling-signupregistration
This article (http://simplapi.wordpress.com/2012/04/13/php-and-node-js-session-share-redi/) shows how to
store sessions of the HTTP server in Redis (using Predis)
get these sessions from Redis in node.js by the session id sent in a cookie
Using this code you are able to get them in socket.io, too.
var io = require('socket.io').listen(8081);
var cookie = require('cookie');
var redis = require('redis'), client = redis.createClient();
io.sockets.on('connection', function (socket) {
var cookies = cookie.parse(socket.handshake.headers['cookie']);
console.log(cookies.PHPSESSID);
client.get('sessions/' + cookies.PHPSESSID, function(err, reply) {
console.log(JSON.parse(reply));
});
});
use session and Redis between c/s
Server side
io.use(function(socket, next) {
// get here session id
console.log(socket.handshake.headers.cookie); and match from redis session data
next();
});
this should do it
//server side
io.sockets.on('connection', function (con) {
console.log(con.id)
})
//client side
var io = io.connect('http://...')
console.log(io.sessionid)
Related
I have an Apache Server and an OpenResty Server (like NGINX sort of) running on the same server with the same domain name but different ports (e.g. 8443 and 8000). I might also want to have them on different servers but for now the same server.
I tried using an encrypted cookie and that sort of works, but there are some drawbacks to that since it might persist if it is not programmatically expired, and there are some other issues with that, although it does mostly work when using the same FQDN, although I did run into some rare problems with it not being readable on the OpenResty server, which is why I'm looking for a way to use a shared SESSION or some sort of token in the headers or another mechanism.
With the Cookie I'm just using an href and GET to open the page on the OpenResty server and reading the Cookie. I could be the params to pass in the URL using a GET, but I would prefer to keep those invisible and somewhat secure. I could also probably POST to that page, but I would prefer to use some other mechanism if that is possible. It is an SSL connection.
I think you can configure Apache and OpenResty to use the same PHP library (not sure, but I think so), so that might help.
I am not too familiar with OAuth or other mechanisms for authentication. I want to have the access be transparent such that when I set a SESSION or Cookie to allow the user access to a particular resource on the OpenResty server that they automatically have access to the resource that is authorized. I actually have the auth mechanism to the resource sort of set up already because the resource actually resides on another app server that is set up as a reverse proxy behind OpenResty. When I need is way to transfer the auth credentials from the Apache server page to a page on the OpenResty server.
Thanks.
There isn't really that much code. I'm using PHP. On the Apache side there is actually an Controller class that sets the cookie. That is an AJAX call (could be a problem, but it seems to work). That just returns a link via AJAX, and that is opened up in an iFrame on the calling page. That part works, and actually the Cookie seems almost always. If it is encrypted and set to expire after a short amount of time that is a workaround for now:
Sets the Cookie, Apache Server
public function upload_study() {
$uploaderurl = "https://sias.dev:8443";
$myip = $_SERVER['REMOTE_ADDR']; // https://stackoverflow.com/questions/3003145/how-to-get-the-client-ip-address-in-php
$data = array("session_id" => session_id(), "userid" => $_SESSION['user_id'], "user_name" => $_SESSION['user_name'], "mrn" => $_POST['mrn'], "IPaddress" => $myip, "anon_normal" => "normal");
$curlResult = (new OrthancModel())->APICall ("upload_notice", $data);
//$data = Encryption::encrypt(json_encode($data));
$data = json_encode($data);
//setcookie("PACStoken", $data, time() + Config::get('SESSION_RUNTIME'), Config::get('COOKIE_PATH'), Config::get('COOKIE_DOMAIN'), Config::get('COOKIE_SECURE'), Config::get('COOKIE_HTTP'));
setcookie("PACStoken", $data, [
'expires' => time() + Config::get('SESSION_RUNTIME'),
'path' => Config::get('COOKIE_PATH'),
'domain' => Config::get('COOKIE_DOMAIN'),
'secure' => Config::get('COOKIE_SECURE'),
'httponly' => Config::get('COOKIE_HTTP'),
'samesite' => Config::get('COOKIE_SAMESITE'),
]);
DatabaseFactory::logVariable($curlResult);
$link = $uploaderurl;
echo '{"success":"done", "link":"' . $link . '"}';
}
JS on Apache Server:
$(".uploadstudy").on("click", function(e) {
e.preventDefault();
$.ajax({
type: "POST",
url: '/OrthancDev/upload_study',
dataType: "json",
data: {mrn: $("#data-mrn").val(), "csrf_token": $("#CsrftokenAJAX").data("csrf")},
context: $(this),
beforeSend: function(e) {
$("#spinner").css("display", "block");
},
})
.done(function(data, textStatus, jqXHR) {
if(data.success) {
$("#dynamiciframe").append($('<iframe style="width:100%;border:none;margin:0px;overflow:scroll;background-color:transparent;height: 100vh;" class="vieweroverlay-content" id="viewerframe" src="' + data.link + '"></iframe>'));
document.getElementById("myNav").style.width = "100%";
$("body").css("overflow", "hidden");
}
else alert("error");
})
.fail(function( jqXHR, textStatus, errorThrown) {
})
.always(function(jqXHR, textStatus) {
$("#spinner").css("display", "none");
});
});
There is more on the OpenResty Server, but this is a piece of the cookie part:
if (isset($_COOKIE['PACStoken'])) {
//print_r($_COOKIE['PACStoken']);
//$cookie = json_decode(Encryption::decrypt($_COOKIE['PACStoken']));
$cookie = json_decode($_COOKIE['PACStoken']);
$_SESSION['userid'] = $cookie->userid;
$_SESSION['user_name'] = $cookie->user_name;
$_SESSION['anon_normal'] = $cookie->anon_normal;
$_SESSION['mrn'] = $cookie->mrn;
$ip = $cookie->IPaddress;
$auth = true;
if ($ip != $currentip) {
$auth = false;
}
}
else {
$auth = false;
}
I should say that it is possible to extract the data from the cookie and put that into a SESSION once the user hits the OpenResty page. I need that data to authenticate access to the resources on the reverse proxy. I can immediately unset and expire the cookies. I can try that. Just wondering how secure that is if:
The Cookie is encrypted with the keys on the Apache and OpenResty servers.
The Cookie is unset and expired immediately. Realistically, it would exist for probably seconds at the most.
There is a library that does the encryption and decryption, e.g.
//$data = Encryption::encrypt(json_encode($data));
I Currently have a php website working. Here i can keep and use any session data to keep track of my user. however if i simply connect to my node.js game from this website using a simple hyper link such as...
<a href="http://localhost:8080">
This works and i do connect to my game running on a local host on that port here is the coded for setting up the node.js game.
const http = require('http');
const express = require('express');
const socketio = require('socket.io');
const TDSGame = require('./../tds-game');
const { log } = require('console');
const app = express();
// path for our client files
const clientPath = `${__dirname}/../client`;
console.log(`Serving static from ${clientPath}`);
// for static middleware from express
app.use(express.static(clientPath));
const server = http.createServer(app);
const io = socketio(server);
var waitingPlayers = [];
io.on('connection', (sock) => {
if(waitingPlayers.length == 3){
waitingPlayers.push(sock);
new TDSGame(waitingPlayers);
waitingPlayers = [];
}
else{
waitingPlayers.push(sock);
sock.emit('message', 'Waiting for opponent');
}
sock.on('message', (text)=>{
// io.emmit everyone connected to the server receives the message
io.emit('message', text);
});
});
server.on('error', (err)=>{
console.log('Server Error', err);
});
server.listen(8080, ()=>{
console.log('TDS started on 8080');
});
What would be a good way of passing the players i dunno hash and
username or something. to the the game so on connection i can get
these variables and check to see if my player exists in the database?
if so then pass these players and sockets to the game logic?
I am struggling any help would be much appreciated thank you :)
you can add extra params to the socket connection URL "http://localhost:8080?foo=bar&hi=hello" and through that, you can get the data when the socket-clint connects ( io.on('connection') event ).
And you can delete the data from the array (
waitingPlayers ) when it disconnects. Through this way you can manage the connections.
I do use the socket.io for my chat-app where I use redis instead of Array to store the connection id to send and receive messages.
In my web app (symfony3) I try to integrate a real time notification system.
I choose to done this with this configuration :
server NodeJS
server redis
socket.io
socket.io-php-emitter (php adaptater for emit notification directly from php)
app.js (server node)
var app = require('express')();
var http = require('http').Server(app);
var server = require('http').createServer();
var io = require('socket.io').listen(server);
var redis = require('socket.io-redis');
io.sockets.on('connection', function (socket) {
console.log('user connected!');
});
io.adapter(redis({ host: 'localhost', port: 6379 }));
server.listen(8081, 'myhost');
test controller
public function testAction(Request $request)
{
$notification = 'A message has been received by the server!<br />'
$form = $this->createFormBuilder()->getForm();
$form->handleRequest($request);
if ($form->isValid()) {
$redis = new \Redis();
$redis->connect('127.0.0.1', '6379');
$emitter = new SocketIO\Emitter($redis);
$emitter->emit('notification', $notification);
$response = new JsonResponse();
$response->setData(array(
'notification' => $notification
));
return $response;
}
return $this->render('#PMPlatform/Test/test.html.twig', array(
'form' => $form->createView()
));
}
client view
$(document).ready(function() {
// Getting socket
var socket = io('http://myhost:8081');
// Listening on notification from server
socket.on('notification', function (data) {
$('.server').append('qsfdqsfdqsdfqsfdqsfd');
});
// Listener on form submit event
$(document).on('submit', '.form', function(e) {
e.preventDefault();
$.ajax({
type: 'POST',
url: '/app_dev.php/test',
data: $(this).serialize(),
success : function(response){
$('.client').append('A message has been sent to server from this client!<br />');
},
error : function(response){
console.log('Something went wrong.');
},
cache: false
});
return false;
});
});
Ok this simple test works fine : when I submit the form from the proper view, a notification is displayed to all client which are on this page.
1- Now I want to one user be able to send a notification only to one another user (when a user comment a post, the author of the post recieved a notification). How I can do this ?
2- If the author is not connected, How I can to save the notification to show it when the author connects back ?
For your first question, in the server you maintain in some way, redis for example, a list of all your connected clients by their sockets. But, the key thing here is to pass an id of the user when it connects to the server. Now you have your users in your socket.io server perfectly identified by their id.
io.sockets.on('connection', function (socket) {
console.log(socket);
});
If you look at the socket object that comes with a connection, there you will have the id of the user that just got connected.
Then when you want to send a notfiication to that user you just have to search in redis for the corresponding socket to the user id you want to send notification. When you instantiate your emitter $emitter = new SocketIO\Emitter($redis); you will have to search in redis for that particular socket - search in redis by user id -
For the second question I won't go into websockets for this feature. Every time a user connects you will have to search in database if he has any pending notification, and send to him. This can be done for a small amount of users without incurring in a heavy computational usage, but this does not scale.
For this purpose I will:
Search in redis for the connected user
If not connected, save into database the new notification.
When the user connects again, make a standard HTTP request to the API and send to him all the pending notifications, handling them and making necessary changes.
Hope it helps!
EDIT
I forgot to explain how you pass data on the connection. On the client just do: var socket = io('http://216.157.91.131:8080/', { userId: "myUserId" });. Easy, isn't it?
I have an Angular app that consumes an API I built in Laravel, and I use jwt-auth for token management and satellizer on the front end to send the token with each request.
My live environment (for both the front end and the API - which will be moved to a different server once the app is finished) at the moment consists of 2 AWS EC2 instances running nginx with a load balancer. Both servers have the same jwt secret key.
However, and at the moment I can't work out any pattern to it, I randomly get 400 "token_invalid" errors returned from my api. It is not one particular api route, nor is it on every load of the app. When I get a 400 error, from my /clients endpoint for example, other requests will have returned 200's. Next time, all will return 200's. The time after that I may get 200 returned for /clients but a 400 error for /users.
Could this be an issue with me using a load balancer? The jwt secret key, as I said, is the same on both servers - as all the code is in GIT.
I am not using the jwt.refresh middleware.
One other thing to mention is that I don't ever get 400 errors returned when running the app locally via Homestead, ony in production.
EDIT - it seems as though logging out (which clears both my user object (basic details only) and the token from local storage, clearing my cache, then logging back in most often causes the error - is this helpful?
Below is an example of one of my api calls.
App.js
.service('ClientsService', function($http, $q, __env) {
this.index = function () {
var deferred = $q.defer();
$http.get(__env.apiUrl + '/clients')
.then(function successCallback(response) {
console.log(response.data);
deferred.resolve(response.data);
},
function errorCallback(response) {
console.log(response);
});
return deferred.promise;
}
})
ClientsController.js
.controller('ClientsController', function(ClientsService, $stateParams, $mdDialog, $mdToast) {
var vm = this;
ClientsService.index().then(function(clients) {
console.log('ClientsCtrl init');
vm.clients = clients.data;
});
// other controller code
})
I'm really struggling to debug this, so any help would be much appreciated. If any more info is needed, please let me know.
https://github.com/tymondesigns/jwt-auth/issues/1583
solution: use the same jwt secret in .env file
I've recently set up a nodejs chat server, the chat client is served by a php server. When users log in, their sessions will be stored in mysql of the php server, and a login cookie will append to browser.
I want to restrict users that only logged in users are able to chat. What is the best practice to archieve that ?
My quick thought:
When the chat client loaded, if user logged in, I'll send the login cookie information to nodejs verver via socket. Then create a nodejs session. When user chat, the message together with cookie information will be sent to nodejs server via socket. If the cookie information does not match the nodejs session, the message will not be broadcasted and client socket will be disconected.
A websocket is a permanent open connection. You only need to autheticate once when you connect to the websocket.
Simply send your login cookie to node.js once and store it on the server with a reference to the socket connection. Then only handle messages from authenticated users and only broadcast to authenticated users.
The problem is that client side users can easily fake this cookie as node does not talk to php to ensure that it's a valid login cookie.
An example using now.
warning pseudo code
// server.js
everyone.now.joinChat = function(cookie) {
chat.add(this, cookie);
}
everyone.now.serverMessage = function(message) {
if (chat.hasUser(this)) {
chat.broadcast(message);
}
}
chat = (function() {
var users = [];
return {
"add": function(client) {
users.push(client);
},
"hasUser": function(client) {
return users.some(function(user) {
return user === client;
});
},
"broadcast": function(message) {
users.each(function(user) {
user.clientMessage(message);
});
}
}
}());
// client.js
$(function() {
now.joinChat($.cookie("login"));
$("#send").click(function() {
now.serverMessage($(this).data("message"));
});
now.clientMessage = function(message) {
$("#messages").append($("<span></span>").text(message));
}
});
This is the answer i'm looking for nodeJS - How to create and read session with express