socketio-jwt disconnect expired tokens - node.js

I am able to authenticate using socketio-jwt and everything is working great. The problem I'm running into is if I set an expiration to a minimum time and let it expire I can continue to emit and receive messages on the connection. As long as I don't refresh the page the connection persists. Once I refresh the connection is disconnected and I am required to reconnect. Is it possible to have the server check for expired tokens and disconnect the connection?

The library does not support this feature, you can validate the token on each socket io event that you want.
In a Github Issue a contributor answered with this analogy:
The id_token is like your national
passport, both have an expiration, you can enter a country as long as your
passport is not expired and most countries will not keep track of
expiration to hunt you down.
You can handle this manually using a socketio middleware for example:
const timer = require('long-timeout')
function middleware (socket, next) {
const decodedToken = socket.user // Assuming the decoded user is save on socket.user
if (!decodedToken.exp) {
return next()
}
const expiresIn = (decodedToken.exp - Date.now() / 1000) * 1000
const timeout = timer.setTimeout(() => socket.disconnect(true), expiresIn)
socket.on('disconnect', () => timer.clearTimeout(timeout))
return next()
}

Related

Is it secure to use jwt payload for some authorization?

For example, when I log in I create an access token with payload { 'userId': 1, '2fa': false} (/login route), then I do another route '/login/auth' that checks for example if a one time password is correct, if it is I created another jwt but this time with 2fa set to true. Then proceeding routes will check whether 2fa is true to run. If not, it will error. Is this a proper way to do this or hacky?
// middleware
const auth = (req, res, next) => {
const token = req.header('access-token');
if (!token) return res.status(401).json('Not Authorized');
try {
const payload = jwt.verify(token, 'secret1');
req.payload = payload
next();
} catch (ex) {
res.status(400).json('Invalid.');
}
};
// routes
router.post(
'/login',
async (req, res) => {
try {
/* login database stuff goes here if successful creates access token
*/
const token = jwt.sign(
{ userId, twoFactorAuthenticated: false },
'secret1',
);
res.status(200).json(token);
} catch (e) {
console.log(e);
return res.status(500).json(e);
}
},
);
router.post(
'/login/auth2',
auth,
async (req, res) => {
try {
/* verifying two factor auth logic goes here
* if succesful approves the 2fa
*/
const token = jwt.sign(
{ userId, twoFactorAuthenticated: true },
'secret1',
);
res.status(200).json(token);
} catch (e) {
console.log(e);
return res.status(500).json(e);
}
},
);
router.post(
'/some-route',
auth,
async (req, res) => {
try {
if(!req.payload.twoFactorAuthenticated)
return res.status(400).json('user has not completed second factor auth')
} catch (e) {
console.log(e);
return res.status(500).json(e);
}
},
);
My use case doesn't really require login authorization but for something different but same concept applies.
The process you are suggesting will work, but it can be improved.
Set JWT claims
Since the payload of JWT is not encrypted, it is only meant for storing non sensitive that is usefull in the verifing process. Although the payload is not encrypted, it is signed and cannot be changed without knowing the secret or private key.
There are a number of standard claims which might be useful, besides your own custom twoFactorAuthenticated claim. You always should set an expiration time (exp) on your tokens. For the userId you may use the subject claim (sub).
Use middleware for authentication of routes
Once the two factor authentication is done, you want to check the JWT on all protected routes. Now, you have put the check in the ‘some-route’ controller itself, but it is preferable to define middleware for this if you have more then one protected route.
Use a http only cookie to store JWT with a short life time
In your solution you send the token to your client and let the client add it to the authentication header. This probably means that the token will be stored in local storage. It may be more secure to use a secure http cookie for this, with a short life time, like 10 minutes or so. This means that if the token is compromised, your API is vulnerable for a maximum of 10 minutes. For a user, having to authenticate every 10 minutes is a horrible UX. So that’s why you may want to implement refresh tokens.
Use refresh token and use it only once
In your example, after the twofactor authentication is verified, you send a token back with the twoFactorAuthenticated set to true. As suggested before, you can send this in a secure cookie with a short life time instead. At the same time, you generate a refresh token with a much longer expire time, e.g. 4 hours. When the cookie expires after 10 minutes, the client can use the refresh token once to get a new cookie and a new refresh token.
Important:
The refresh token can only be used to get a new cookie, not on other routes. It can be stored in local storage and send in the authentication header as a bearer token.
The refresh token may only be used once, so it needs to have a unique id and you need to store the id on the server side. If a refresh token is used you set it to invalid.
If a refresh token is used, you check if it is not used before. If it is used for the second time, something strange is happening, because with a new cookie, the user also gets a new refresh token. At that moment you just set all valid refresh tokens of the user to invalid. Than yo are sure they have to log in again on all devices, after their cookie expires.
This requires some implementation on client and server side, but I think it makes your authentication process much more solid.
You can use '2fa' boolean value in payload, it does not show any secret information. It is impossible to create this token without the JWT secret key which is private to you. But you can do this with only 'userId' as well, store '2fa' value in user table and check the value when you verify the token.

How to safely get the current user id in socket.io/Node.JS?

I am developing a simple API for a chat application on Node.js Express, and by assignment, it is required to make it possible to communicate between two users using socket.іо. I am faced with the problem that I cannot "safely" transfer information about the current user to the socket in any way. Information about the user with whom the correspondence is conducted can be specified in the socket parameters when connecting, which I do, but what about the current user (me)?
For example, I can do this:
const {receiverId, myId} = socket.handshake.query;
That is, specify both ids when connecting. But this is very unsafe because anyone who will join the socket can specify any id and write anything on behalf of someone else (for example, through Postman WebSockets).
Another option I was considering is making a post request in which a connection to the socket will be created using request.user.id and the request parameter. Then the post request will look like this:
router.post('/chat/:receiver', function (req,res){
const {receiver} = req.params
const socket = io.connect('/')
socket.emit('initMyUserId', {
myId: req.user,
});
})
But this option also did not work, because the file where this function is located and the file where the io variable is initialized are different, and I am not sure that it is generally advisable to transfer and use it in this way. Moreover, this approach will not allow me to check the operation of sockets via Postman, because the connection will occur in a post request, and not manually.
Are there working options to safely transfer the current user id with the ability to test it normally in postman? Or at least just safely pass the current user id if it doesn't work with Postman.
Here is the full code snippet for the socket events handlers:
module.exports = function(io) {
io.on('connection', (socket)=>{
const {id} = socket;
console.log(Socket connected: ${id});
const {idUser} = socket.handshake.query;
console.log(Socket idUser: ${idUser});
socket.on('message-to-user', (msg) => {
msg.type = user: ${idUser};
socket.to(idUser).emit('message-to-user', msg);
socket.emit('message-to-user', msg);
});
socket.on('disconnect', () => {
console.log(Socket disconnected: ${id});
});
});
}

ExpressJS - Proper way to regenerate JWT after user info changes

Let's say that after initial launch/login, the backend sends a token to frontend containing user info such as username, email, and other credentials. This token resides in user's client and gets sent back with every API call for authentication.
At one point, the user might update their email. From then on, JWT should be regenerated so that it contains new email instead of the old one.
I can achieve this by fetching most recent data from the DB and generating a new token on every 'verifyAuth' call and it works mostly fine, but I believe a more efficient flow can be implemented.
The 'verifyAuth' middleware that I use is almost a global middleware, it is executed with almost every request and multiple times on that, so fetching data on every call significantly increases response times.
How can I make sure that JWTs are up-to-date efficiently without repeated DB queries?
const verifyAuth = async (req, res, next) => {
const { authorization } = req.headers;
if (!authorization) {
return res
.status(status.unauthorized)
.send({ ...error, message: 'No auth.' });
}
try {
const token = authorization && authorization.split(' ')[1];
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const { userId } = decoded;
const dbResp = await db.oneOrNone(
`SELECT id AS user_id, username, email
FROM users
WHERE id = $1`,
[userId]
);
req.user = {
accessToken: token,
userId,
username: dbResp.username,
email: dbResp.email,
};
next();
} catch (e) {
return res
.status(status.unauthorized)
.send({ ...error, message: 'No auth.' });
}
};
router.use('/api/app', verifyAuth, AppRouter);
router.use('/api/user', verifyAuth, UserRouter); // and more routers like this.
How can I make sure that JWTs are up-to-date efficiently without repeated DB queries?
The short answer is - you can't. Once a JWT is issued, the only way to check whether it contains stale data is to verify that data with a source. But there are ways of getting around this problem:
You can keep the expiration of your tokens short. If your tokens expire after 5 min then this is the maximum amount of time that you will have to deal with stale data. Maybe you're ok with that.
Cache your calls to the DB. If you call the DB a few times during one request with the same query you can cache the results. You probably don't even need some fancy caching mechanisms, you can just keep the result of the query in memory and use it for subsequent calls to the DB.
You can implement a feature where you will keep a track of when a user's data has changed. For example, you set up a redis database (which is a fast key-value store) where you keep information "userId - timestamp of last data change". Then when you validate a token you can check whether it was issued before that entry you have in Redis. If true, then you call your DB for new data. Calling Redis will be much more lightweight than calling your SQL DB.

How to get current socket object or id with in a sails controller?

I would like to access the currently connected socket id with in a sails.js (v0.12 ) controller function.
sails.sockets.getId(req.socket); is showing undefined since this is not a socket request
My objective is to set the online status of my user in the database when he logged in successfully
login: function (req, res) {
Util.login(req, function(){
var socketId = sails.sockets.getId(req.socket);
console.log('socketId ===', socketId); // return undefined
});
},
Basically i would like to access the current user's socket object in a controller or access current user's session object with in a socket on method
Also i'm not sure that how can i rewrite my old sockets.onConnect
handler
onConnect: function(session, socket) {
// Proceed only if the user is logged in
if (session.me) {
//console.log('test',session);
User.findOne({id: session.me}).exec(function(err, user) {
var socketId = sails.sockets.getId(socket);
user.status = 'online';
user.ip = socket.handshake.address;
user.save(function(err) {
// Publish this user creation event to every socket watching the User model via User.watch()
User.publishCreate(user, socket);
});
// Create the session.users hash if it doesn't exist already
session.users = session.users || {};
// Save this user in the session, indexed by their socket ID.
// This way we can look the user up by socket ID later.
session.users[socketId] = user;
// Persist the session
//session.save();
// Get updates about users being created
User.watch(socket);
// Send a message to the client with information about the new user
sails.sockets.broadcast(socketId, 'user', {
verb :'list',
data:session.users
});
});
}
},
You need to pass the req object to the method.
if (req.isSocket) {
let socketId = sails.sockets.getId(req);
sails.log('socket id: ' + socketId);
}
Since the request is not a socket request, you might need to do something like
Send back some identifier to the client once logged in.
Use the identifier to join a room. (One user per room. )
Broadcast messages to the room with the identifier whenever you need to send message to client.
https://gist.github.com/crtr0/2896891
Update:
From sails migration guide
The onConnect lifecycle callback has been deprecated. Instead, if you need to do something when a new socket is connected, send a request from the newly-connected client to do so. The purpose of onConnect was always for optimizing performance (eliminating the need to do this initial extra round-trip with the server), yet its use can lead to confusion and race conditions. If you desperately need to eliminate the server roundtrip, you can bind a handler directly on sails.io.on('connect', function (newlyConnectedSocket){}) in your bootstrap function (config/bootstrap.js). However, note that this is discouraged. Unless you're facing true production performance issues, you should use the strategy mentioned above for your "on connection" logic (i.e. send an initial request from the client after the socket connects). Socket requests are lightweight, so this doesn't add any tangible overhead to your application, and it will help make your code more predictable.
// in some controller
if (req.isSocket) {
let handshake = req.socket.manager.handshaken[sails.sockets.getId(req)];
if (handshake) {
session = handshake.session;
}
}

Using JWT tokens. Is there a better approach?

I'm using JWT tokens via nJWT package to authenticate my users to my Socket.io using socket.io-jwt package.
More or less, the code looks like this. User sends a POST reques to play/login via HTML form to generate a JWT token. Then, socket.io client initializes using that token.
/**
* Create Express server.
*/
const app = express();
const http = require('http').Server(app);
const io = require('socket.io')(http);
const socketioJwt = require('socketio-jwt');
app.set('jwt.secret', secureRandom(256, {
type: 'Buffer'
}));
app.post('/play/login', (req, res) => {
// validate user's req.body.email and req.body.password
const claims = {
iss: "http://app.dev", // The URL of your service
sub: "user-1", // The UID of the user in your system
scope: "game"
};
const jwt = nJwt.create(claims, app.get("jwt.secret"));
const token = jwt.compact();
new Cookies(req,res).set('access_token', token, {
httpOnly: true,
secure: process.env.ENVIRONMENT === "production"
});
tokenUserRelations[token] = req.body.email;
res.json({
code: 200,
token: token
});
});
/**
* Add Socket IO auth middleware
*/
io.set('authorization', socketioJwt.authorize({
secret: app.get("jwt.secret"),
handshake: true
}));
io.sockets.on('connection', function (socket) {
socket.on('chat message', function (req) {
io.emit("chat message emit", {
email: tokenUserRelations[socket.handshake.query.token],
msg: req.msg
});
});
socket.on('debug', function (req) {
io.emit("debug emit", {
playersOnline: Object.keys(tokenUserRelations).length
});
});
socket.on('disconnect', function (req) {
delete tokenUserRelations[socket.handshake.query.token];
});
});
io.listen(app.get('socket.port'), () => {
console.log('Started! Socket server listening on port %d in %s mode', app.get('socket.port'), app.get('env'));
});
Right now, it works properly, but in order to track emails from tokens, I had to do this:
tokenUserRelations[token] = req.body.email;
so I can relate which user the token points to.
I have a feeling that keeping token<->email relations in a global object is going to cause me headaches in the future, especially when tokens/cookies expires.
Is there any better way about this? I need to know which user that JWT token points to so I can do some business logic with them.
Thank you.
A token can contain information about anything you want, this information is encrypted along the token.
What you can do is encrypt a user id in the token, when you receive a request, decrypt the token (which is anyway done when you verify it), and use the user id as normal.
This way, if the token expire, the new token will have the same user id, and your code will not be impacted.
This is what I did in one of my web app, and it worked fine. However, I was using the official jwt module
You don't show anything in your code about how tokenUserRelations is created or maintained, but as soon as I hear "global" a red flag goes up in my head.
The JWT standard includes the concept of embedding 'claims' in the token itself; you're already doing so with your claims constant. That data format is arbitrary and can be trusted by your app so long as the overall JWT gets validated. Note that you'll want to verify JWT on every request. So, stuffing email into that claims object is not just fine, it's what most folks do.
As a sidenote, you should be careful about how you're setting your 'jwt.secret' right now. What you have now will generate a new one every time the app starts up, which means that a) all your users will be logged out and have to re-login every time the app restarts, and b) you can't make use of multiple processes or multiple servers if you need to in the future.
Better to pull that from the environment (e.g. an env var) than to generate it on app start, unless you're just doing so for debugging purposes.
Adding to the excellent answers above, it is also important that if you decide to store your jwt.secret in a file and pull that in when the code loads that you do not add that to your git repository (or whatever other VCS you are using). Make sure you include a path to 'jwt.secret' in your .gitignore file. Then when you are ready to deploy your production code you can then set that key as an environment variable as suggested. And you will have a record of that key in your local environment if you ever need to reset it.
Using JWTs is an excellent and convenient way of securing your api, but it is essential to follow best practice.

Resources