bind socket.io id to user id without sessions (express, passport JWT) - express

I'm developing RESTful API on express, with JWT and passport for authorization. I want to implement socket.io connection for notification and signalling purposes (WebRTC session establishing). I don't want to implement standard session management, don't want to deal with cookies, but somehow I have to be able to address particular user via socket. I have event handling in all my routes, so app is aware of auth-ed requests and corresponding user ids. One approach(probably) is to create socket io group with user id, add socket to this group and emit there. (Engaging reconnection handling and checking socket existence on every subsequent request - that's way overcomplicated). I guess there should be a better approach. I also use Redis, so I can leverage that in this scheme. Any suggestion is appreciated, thank you

Well, I managed to solve that in such a manner.
Server:
import jwt from 'jwt-simple'; //I'm using ES6/Babel
module.exports = function(app) {
var io = app.get('io'); //Import io any possible way,
//here I do it like so because I set
//app.set('io', io) in my index.js
var user_id;
io.on('connection', socket => {
// Recieve encoded token from client, decode and find user id
// To do - check againt database
socket.on('auth', token => {
if (token) {
var decoded = jwt.decode(token, 'secret')
user_id = decoded.sub
socket.emit('auth', user_id);
}
})
// Join room proposed by client - user id string
socket.on('room', room => {
socket.join(room)
console.log('Server joined room...', room)
//emit message to user id from anywhere in the app
io.sockets.in(user_id).emit('message', 'what is going on, party people?');
})
})
}
client:
var token = localStorage.getItem('token');
var socket = io();
socket.on('connect', data => {
socket.emit('auth', token);
socket.on('auth', user_id => {
socket.emit('room', user_id);
})
})
Now to address specific user I can always emit to room id equal to user id, provided that user has got credentials. Even after browser refresh.
Photo: the left client has valid token in localStorage, the right one doesn't

Related

Anyone have a solution for generating server-side tokens for the ESRI JSAPI SDK?

There are a number of solutions to this:
use the build-in dialog provided by esri/IdentityManager (https://developers.arcgis.com/javascript/3/jsapi/identitymanagerbase-amd.html)
use a server-side proxy (https://github.com/Esri/resource-proxy)
use the identity manager initialize() method (https://developers.arcgis.com/javascript/3/jsapi/identitymanagerbase-amd.html#initialize)
But there what is missing is the ability to hook into the request for a token. I am working with ArcGISDynamicMapServiceLayer and there is no way to know if the server return a 498/499, and no way to update the url to update the token.
I started hacking around in the API to try to hook into various events with no real promise of success. What seems to be missing:
a way to detect when a token is needed
a way to update the token
Closes I came up with is listening for "dialog-create" but there is no way to disable the dialog apart from throwing an exception, which disables the layer.
I tried replacing the "_createLoginDialog" method and returning {open: true} as a trick to pause the layers until I had a token ready but since there is no way to update the layer endpoint I did not pursue this hack. It seems the only way this might work is to use the initialize() method on the identity manager.
Does anyone have knowledge of options beyond what I have outlined?
EDIT: The goal is to provide a single-sign-on experience to users of our product.
"User" is already signed in to our application
"User" wishes to access a secure ESRI ArcGIS Server MapServer or FeatureServer services from the ESRI JSAPI
"User" is prompted for user name and password
The desired flow is to acquire a token on the users behalf using a RESTful services in our product and return the appropriate token that will allow the "User" to access the secure services without being prompted.
I do not wish to use a proxy because I do not want all that traffic routed through the proxy.
I do not wish to use initialize() because it is complicated and not clear how that works apart for re-hydrating the credentials.
I do wish for an API that simply allows me to set the token on any layer services that report a 499 (missing token) or 498 (invalid token), but I cannot find any such API. The solution I am focusing on hinges on being able to update the url of an ArcGISImageServiceLayer instance with a new token.
This answer lacks in satisfaction but delivers on my requirements. I will start with the code (client-side typescript):
class TokenProxy {
private tokenAssuranceHash = {} as Dictionary<Promise<{ token: string, expiration: string }>>;
private service = new TokenService();
private timeoutHandle = 0;
watchLayer(esriLayer: ArcGISDynamicMapServiceLayer) {
setInterval(async () => {
const key = esriLayer._url.path;
const token = await this.tokenAssurance(key);
esriLayer._url.query.token = token;
}, 5000);
}
updateRefreshInterval(ticks: number) {
clearTimeout(this.timeoutHandle);
this.timeoutHandle = setTimeout(() => {
Object.keys(this.tokenAssuranceHash).forEach(url => {
this.tokenAssuranceHash[url] = this.service.getMapToken({serviceUrl: url});
});
this.updateRefreshInterval(ticks);
}, ticks);
}
async tokenAssurance(url: string) {
if (!this.tokenAssuranceHash[url]) {
this.tokenAssuranceHash[url] = this.service.getMapToken({serviceUrl: url});
}
try {
const response = await this.tokenAssuranceHash[url];
await this.recomputeRefreshInterval();
return response.token;
} catch (ex) {
console.error(ex, "could not acquire token");
return null;
}
}
async recomputeRefreshInterval() {
const keys = Object.keys(this.tokenAssuranceHash);
if (!keys.length) return;
const values = keys.map(k => this.tokenAssuranceHash[k]);
const tokens = await Promise.all(values);
const min = Math.min(...tokens.map(t => new Date(t.expiration).getTime()));
if (Number.isNaN(min)) return; // error occured, do not update the refresh interval
const nextRefreshInTicks = min - new Date().getTime();
this.updateRefreshInterval(0.90 * nextRefreshInTicks);
}
}
And highlight the hack that makes it work:
const key = esriLayer._url.path;
const token = await this.tokenAssurance(key);
esriLayer._url.query.token = token;
The "_url" is a hidden/private model that I should not be using to update the token but it works.

Doubts on Authentication Techniques - Passport

So I was trying out the authentication techniques with passport and passport-jwt with the express server. Here is the code I've been working with
const JwtStrategy = require("passport-jwt").Strategy;
const ExtractJwt = require("passport-jwt").ExtractJwt;
const User = require("../models/user");
const opts = {};
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
opts.secretOrKey = "secret";
module.exports = passport => {
passport.use(
new JwtStrategy(opts, (jwt_payload, done) => {
User.findById(jwt_payload.id,(err,user)=>{
if(err){
return done(err,false);
}
if(user){
done(null,user);
}
else{
done(null,false);
}
})
})
)
};
So the all point of using this passport authorization is to minimize the number of times the database is accessed, right?
But in this code after extracting the token, the database is accessed through the findById method to find whether the user is in the database, so what's the point in all of this if the database is accessed during each authentication request?
I'm pretty sure I'm saying something wrong, some help in clarifying this matter is deeply appreciated.
The question is, why would you need to do User.findById on the middleware?
You don't have to access the database on the middleware to find whether user exists or not from the JWT payload. When the user is getting the jwt through the /login endpoint, you should've already checked whether the user exists or not
// just a logic example on the login enpoint
const user = User.findUserByEmail(req.body.email);
if (!user) res.sendStatus(401); //returns 401 if user not found
else {
if (verifyPassword(req.body.password, password)) {
res.send(generatedJwtWithUserIdOnThePayload)
} else {
res.sendStatus(401); //returns 401 if password invalid
}
}
The jwt that's passed when logging in to the client already had valid user id in it, therefore you dont need to get User document from User.findById everytime client sending a request to your other endpoint.
Since user id is already inside the payload, unless you need other data beside user id from User document, you don't really need to do User.findById on the middleware

Session Property not saving in session store when retrieving from socket.io

What I am trying to accomplish is to store the username of the current user in the session, such that it can be retrieved by socket.io.
Currently, I have the client sent its sessionId to the socket.io endpoint (via io.connect):
var socket = io.connect('http://localhost?sessionId=' + sessionid);
and then on the server side, I have:
io.use(function(socket, callback) {
if (socket && socket.handshake && socket.handshake.query && socket.handshake.query.sessionId) {
var sessionId = socket.handshake.query.sessionId;
sessionStore.get(sessionId, function(error, session) {
socket.handshake.sessionId = sessionId;
if (error) {
callback('Could not set session id with socket io authorization handshake!', false);
} else if (!session) {
callback('There was no session found during socket io authorization handshake!', false);
}
console.log(session);
socket.session = session;
callback(null, true);
});
} else {
callback('No sessionId value was provided in query string to socket io connect from client', false);
}
});
This correctly retrieves the session from the client-provided sessionid (assuming the id exists, of course).
Now, what I want to do is be able to is get the passportjs-managed username accessible from within the socketio handlers, via the session.
So, to start with, I have this code in the router controller for this endpoint:
router.get('/', Authentication.redirectIfNotAuthenticated, function(req, res) {
req.session.user = req.user;
console.log(req.session);
res.locals.sessionId = req.session.id;
res.render('mypage', { title: 'This is my page', user: req.user });
});
That console.log(req.session) does show me that the req.user has been correctly inserted into the session object and is visible. Authentication.redirectIfNotAuthenticated is using passportjs and is really simple (code included incase it is the issue):
redirectIfNotAuthenticated : function(req, res, next) {
if (req.isAuthenticated()) {
return next();
}
res.redirect('/');
}
So...we know that the user is being inserted as a property into the session object...but when I hit one of my socketio methods:
socket.on('init', function(info) {
console.log(socket.session);
...
});
That console.log(socket.session) does not show the session that I modified in the route, but shows the "original" session object - as though I never added anything to it at all! I do know that socket.on('init'... is being called after my io.use(function.... code as detailed above (where the session is pulled from the sessionStore and saved in the socket object).
I just haven't been able to track down why the session, as stored in socket.session (as retrieved from the session store), isn't mirroring the latest changes made to the session in the router.
If anyone has any suggestions, or reasons why what I expect to see is incorrect, I would be happy to hear them!
A session scopes a connection between a dedicated client browser tab and a server.
As you did not explain why you want to store the username in the session I can base my answer only on guesses:
If you want to remember the username for later use in the session store it on the client side.
If you want to declare the user name (and other variables) to other users in the communication space the easiest way to do that is to store the data in a database (mind that databases can be in-memory, which works out to be quick). You could also fittle with files - but if you run mutliple users you will have to switch to a database anyway...
In any way there is no need to extend the scope of a session over multiple clients.

Link socket.io ID with express and passport

The goal of my simple try is to display online user list. I mean display not socket.io ID but display user profile. When authorized user connects to my server, open socket.io channel, it is required to get his profile and send message to other connected user that new user (Name, email, etc) has being connected. I saw many examples how to do it within authorization, but it doesn't handle a disconnect. What i want to do and what i can't do in pseudocode:
var io = require("socket.io").listen(server);
io.set("authorization", function(data, callback){
// ... some code...
callback(null, true);
});
io.sockets.on('connection', function (socket) {
var UserProfile = passport.getUserProfile(socket.id)
io.sockets.emit('user_connected', {UserProfile: UserProfile, socketID: socket.id});
io.sockets.on('disconnect', function (socket) {
io.sockets.emit('user_disconnected', {socketID: socket.id});
});
});
This is a pseudocode!
My stack is overflowed. I just want to link socket.io ID and passport account together within connection. How can i do it?
I got the same problem, and my solution is the following (hopefully someone gets a better idea):
i add the username to the render call (using jade):
res.render('chatroom', {username: req.user.username});
right after connecting to socket on the client (io.connect), i emit a
message to the server, with the username as parameter, using the connect event on the client (socket.on('connect', function (data) { ... });
on the server, i store the username in an object (clients[socket.id]
= username)
after that, i get the username in every socket message by accessing
the clients object

socket.io authentication after socket established

I'm working on a small multiplayer game. I'd like to introduce authentication. I'm using Node.js and Socket.io.
When the user arrives that the main page - I want them to join the game whether they are logged in or not - but they will be unable to do anything within it (only watch).
How could I then go about authenticating the user on the already open socket?
Could I maintain the authentication still if they left the site and came back? Can you pass a cookie through a web socket?
EDIT
To further my question. One of the possible thoughts I've had is to provide the websocket connection, then when they try to login in, it passes username and password as a message to the websocket.
client.on('onLogin', loginfunction);
I could then take the username and password, check against the database, then take the session ID of the socket and pass it somewhere to say that session is authenticated to that user.
Is this secure? Could I still implement a cookie on the socket so they could come back? Is there any way within socket.io of stating that the socket is now authenticated instead of manually checking on each message received?
Cheers
This isn't actually too hard, but you're approaching it the wrong way. A couple things:
You cannot set a cookie with socket.io; you can, however, get the cookie values of any connected client at any time. In order to set a cookie, you will have to send a new http response, meaning the user must first send a new http request (aka refresh or go to a new page, which it sounds is not a possibility for you here).
Yes: socket.io is secure (to the extent that any transmitted data can be).
As such, you can do the following:
On the user's initial connection, create a cookie with a unique session ID, such as those generated from Express's session middleware. You will need to configure these not to expire on session end though (otherwise it will expire as soon as they close their browser).
Next you should create an object to store the cookie session IDs. Each time a new connect.sid cookie is set, store in your new object with a default value of false (meaning that the user has been authenticated by session, but not by logon)
On the user's login, send a socket emit to the server, where you can then authenticate the login credentials, and subsequently update the session id object you created to read true (logged in) for the current socket id.
Now, when receiving a new http request, read the cookie.sid, and check if its value in your object is true.
It should look something like the following:
var express = require('express'),
http = require('http'),
cookie = require('cookie');
var app = express();
var server = http.createServer(app);
var io = require('socket.io').listen(server);
app.use(express.cookieParser());
app.use(express.session({
secret: 'secret_pw',
store: sessionStore,
cookie: {
secure: true,
expires: new Date(Date.now() + 60 * 1000), //setting cookie to not expire on session end
maxAge: 60 * 1000,
key: 'connect.sid'
}
}));
var sessionobj = {}; //This is important; it will contain your connect.sid IDs.
//io.set('authorization'...etc. here to authorize socket connection and ensure legitimacy
app.get("/*", function(req, res, next){
if(sessionobj[req.cookies['connect.sid']]){
if(sessionobj[req.cookies['connect.sid']].login == true){
//Authenticated AND Logged in
}
else{
//authenticated but not logged in
}
}
else{
//not authenticated
}
});
io.sockets.on('connection', function(socket){
sessionobj[cookie.parse(socket.handshake.headers.cookie)['connect.sid'].login = false;
sessionobj[cookie.parse(socket.handshake.headers.cookie)['connect.sid'].socketid = socket.id;
socket.on('login', function(data){
//DB Call, where you authenticate login
//on callback (if login is successful):
sessionobj[cookie.parse(socket.handshake.headers.cookie)['connect.sid']] = true;
});
socket.on('disconnect', function(data){
//any cleanup actions you may want
});
});
Chris, I'm won't be able to answer since I'm not an expert on socket.io, but I can maybe try to point you in another direction that can help you - and take away some development time.
But first, a disclaimer: I work for Realtime.co and am not trying to do any sort of advertising. I work closely with developers and I'm just trying to help you by providing you an out-of-the-box solution for your problem. Also, being a gamer, I can't stay away from trying to help people getting their games out there!
Realtime uses an authentication/authorization layer in which you can provide user read/write permissions to channels. When users enters the website you can give them read only permissions to the game channel and once they login, you can then give them write permissions. This can be easily done by doing an authentication post and reconnecting to the server (it can all be done client side). I would do it server-side, though, to increase security.
Realtime has a Node.js API so you can easily integrate it with your server. Since it also has APIs for many other platforms (including mobile) and they all work the same way, you can actually have your game working in multiple platforms over the same communication layer, while having full control over channels.
Thanks for reading.
Edit:
You can read the documentation here for more info: http://docs.xrtml.org/
Socket.io has an option to pass extraHeaders. One can use that to pass a token from the client. The server would use the desired authentication algorithm to decrypt the token and get the user_id.
socket.js
import io from 'socket.io-client';
export const socket = io('https://remote-url');
export const socketAuth = () => {
socket.io.disconnect(); //This uses the same socket and disconnect with the server.
socket.io.opts.extraHeaders = {
'x-auth-token': JSON.parse(window.localStorage.getItem('auth-token')),
};
socket.io.opts.transportOptions = {
polling: {
extraHeaders: {
'x-auth-token': JSON.parse(window.localStorage.getItem('auth-token')),
},
},
};
socket.io.open(); //Opens up a new connection with `x-auth-token` in headers
};
client.js
import { socket, socketAuth } from 'utils/socket';
socket.on('unauthenticated-messages', () => {
someAction();
});
//After user logs in successfully
socketAuth();
socket.on('authenticated-messages', () => {
someAction();
});