ExpressJS - Proper way to regenerate JWT after user info changes - node.js

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.

Related

Associating user data between multiple passport strategies

I'm using node, express, express-session, mongoose and passport-discord to authenticate users. But I also wish to use passport-steam to optionally link a user's steam account to their discord account.
The problem is, in order to link their steam account, I would also need a way of knowing which discord user is trying to do so, but the authentication callback doesn't include any request/response headers to access the current session, and the database I'm storing discord users in doesn't have any steam data in it until steam's authentication callback is called, at which point I have no idea which discord user is trying to authenticate their steam account.
To help remedy this, I've setup 3 mongoose models to have 3 separate database collections:
DiscordUser (stores discord user information, including their discordId)
SteamUser (stores steam user information, including their steamId)
User (stores required discordId and optional steamId)
I then try to serialize and deserialize the User object, but the passport.deserializeUser(obj, done) method seems to pass in an id instead of a User object, and I can't be sure if it's a discordId or a steamId, or which discord user I'm deserializing a SteamUser for, so I end up back at square one.
What's the best way to go about associating these users?
If needed, I can supply some code snippets upon request.
I found an article which helped solve this problem.
Here's what I did to fix it:
Using passReqToCallback: true in the new Strategy(...) options
In both strategy callback functions, adding req as the first parameter(ie., async function callback(req, accessToken, refreshToken, profile, done) { ... })
And then changing this:
router.get('/steam', passport.authenticate('steam'), (req, res) => {
//...
});
To this:
// Note that the route here is the same as the callback/redirect
router.get('/steam/redirect', (req, res, next) => {
passport.authenticate('steam', (err, user, { nextRoute }) => {
if (err)
return next(err);
if (nextRoute) {
// user was authorized with provider?
// send em back to profile
return res.redirect(nextRoute);
} else {
// give em some cookies
req.logIn(user, err => {
if (err)
return next(err);
return res.redirect('/');
});
}
})(req, res, next)
});
And then finally, modifying the User model schema to include the sessionID, which can be accessed via req.sessionID (or req.session.sessionID) from within the callback functions of both strategies.
This allows me to update the database upon successful login with my primary strategy(s) (discord, in this case), and then reference it from secondary strategies (steam, in this case), providing enough time to update the User database to have linked information between each strategy for as long as the session is still valid. After which, you can always simply reference the User database anywhere in code to check for valid primary and secondary strategy ids, allowing you to cross-reference other database collections for information, update the session id if the current session id doesn't match (and possibly forcing the user to re-authenticate?), etc.

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 can I transfer data from one route to another

I implement user registration. I have two routes.
import express from 'express';
import User from '../models/user';
const router = express.Router();
router.post('/signup', async (req, res) => {
try {
...
const user = new User(
red.body.name,
red.body.username,
red.body.user_email,
red.body.user_password,
red.body.user_phone,
);
...
} catch (err) {
return res.status(500).json({
err: err
});
}
});
router.post('/verify', async (req, res) => {
try {
...
console.log(user);
...
} catch (err) {
return res.status(500).json({
err: err
});
}
});
I need to send data from the route, user registration, to the route, user confirmation.
How to send user data to the second route for confirmation?
There are 2 strategies to implement this feature (maybe more):
Strategy 1: Store user in server.
You can store user object and assign it a unique key (e.g. uuid) in server. The user data can be stored as global variable (in memory of Node.js process), or it can be stored in memory database (e.g. Redis) if you are using multiple Node.js process.
In POST /signup route handler, the user data can be written to server, and this newly created user id would be returned to browser. Then, in POST /verify route handler, server would retrieve corresponding user data via its id.
Strategy 2: Store user in browser.
Another strategy is returning the whole user data to browser in POST /signup route handler, and let browser send it back in the following POST /verify request. There are 2 ways to implement this design (maybe more):
Return user data to browser via Set-Cookie. Browser would send user data as cookie automatically.
Return user data to browser as plain response body. Browser take it and save it in localStorage or sessionStorage. Then, when sending POST /verify request, browser would read that data and put it as plain HTTP request body.
The best way to approach this would be to store the user data as a browser cookie. Storing user data in local storage is not recommended as it can be accessed by JavaScript and hence poses a security threat.

How to add current logged in status to Users Schema in MongoDB using Passport and node.js

I’m quite new to backend development…
With using my API I would like to be able to display a list of users and also indicate if they are currently logged in. I got the basic authentification working using passport and json web token
I’m not looking to get the current logged in user.
I want to be able to retrieve a list of users and see if they are logged in or not.
Like this:
var users = Users.find({});
// console.log(users) output:
{
name: 'foo'
password: ...
isLoggedIn: false
},
{
name: 'bar'
password: ...
isLoggedIn: true
},
{
name: 'baz'
password: ...
isLoggedIn: false
}
isLoggedIn would be set to true if the user is currently logged in and to falseif not.
How can I do that? Thank you!
It sounds like what you would like to do is update your MongoDB database based on login/logout events. To do this you could use something like mongoose to work with your Node backend to easily access your database in MongoDB.
You can include mongoose after installing with npm install mongoose like so:
var mongoose = require('mongoose');
var User = mongoose.model('User');
Note that User corresponds to whatever schema you create for storing user information.
Assuming you have some sort of router object for handling requests, you could construct route handlers for /logout and /login and use your imported mongoose User model to retrieve and then modify a specific User object as such:
// whenever user goes to '/login' (you can have, say, your 'login' button make a request to this URL
router.get('/login', function(req,res) {
// your authentication here; passport stores the currently authenticated user in req.user
var username = req.user.name; // here we assume the username is stored as 'name' as you have in your code but change this based on your schema
User.findOne({name: username}, function(err, user, data) {
if(err) res.send(err);
user.isLoggedIn = true;
user.save(function (err) {
if (err) {
console.log(err);
} else {
// redirect to some page here maybe
}
});
});
});
// whenever user goes to '/logout' (you can have a logout button make a request to this URL
router.get('/logout', function(req,res) {
// currently authenticated user is still in req.user
var username = req.user.name;
User.findOne({name: username}, function(err, user, data) {
if(err) res.send(err);
user.isLoggedIn = false;
user.save(function (err) {
if (err) {
console.log(err);
} else {
// redirect to login/register page maybe
}
});
});
});
So to summarize what this code would do:
based on the url a user would go to, our route handler would fetch one correct, unique User object from our database based on the name (username)
it would do so by accessing the username property of req.user which corresponds to the currently authenticated user with Passport, which, again will be different for all users
update the field that we use to keep track of login status (isLoggedIn)
and then save the changes, after which we are done updating the state to reflect whether the user is logged in or not, so we can now redirect to some other page or display other content
Finally then, you could retrieve a list of all users similarly to your code like so:
User.find({}, function(err, users, data) {
// all users from your database are in `users`
console.log(users);
});
Edit for expired sessions:
So, to track expired sessions, since you're using Passport, would in theory require functionality to signal with some sort of event / callback / message, etc. the moment the session is deemed invalid. Now that is tough to monitor and from my experience with Passport, stuff like that isn't implemented in all authentication strategies and might vary based on the strategy to be used by developers (think for instance if a browser window is closed, based on Passports authentication strategy, or just browser, it might destroy the cookie for the session right away and our server has no way of knowing about it). I do recommend checking out all the authentication strategies Passport offers in case there are some better ones here.
Now, if you would like to add functionality to track the users passive login/logout status with sessions yourself, you could use something related to cookies. Again, not necessarily one to use, but here's a couple handy Express modules: cookie-parser and cookie-session.
Then, you could set and read cookies like this, using cookie-parser:
var express = require('express');
var cookieParser = require('cookie-parser');
var app = express();
app.use(cookieParser());
You would put this code somewhere right after the user is authenticated:
// cookies are stored here
console.log(req.cookies);
// configure your cookie
var options = {
expires: 1000 * 60 * 60, // expires after one hour
httpOnly: true
}
// Set cookie
res.cookie('session', ('user-' + req.user.name), options);
And then, on the client side check if that cookie is valid continuously on some time interval, and if it expired Date.now() > cookie.expires then make a GET request to /logout, and there log out the user (currently still authenticated) by updating MongoDB and all.
However, since this would require making a mechanism to basically simulate an expired session, I would recommend using something analogous to a timeout, which would be much easier to implement. Just a note, this is sort of analogous to mechanisms on some pages you might have encountered where you get a pop-up saying 'You will be logged out due to inactivity'. In your main.js or whatever client-side script define a function to keep going on a time-out, unless the user does some action.
var inactivity = function () {
var t;
// user doing something on your page, so keep resetting time counter when events happen
document.onmousemove = resetTimer;
document.onkeypress = resetTimer;
// this is a callback function that will get called once a time-out countdown is done
function timeOut() {
// make a request to '/logout' here and logout the current user (you still will have access to req.user from Passport)
// also can redirect from back-end route handler to the login page for instance
}
// this gets called whenever an event happens, resetting the counter of sorts
function resetTimer() {
t = 0;
t = setTimeout(timeOut, 1000 * 60 ) // set this to however long you should wait to log out your user time (in milliseconds)
}
};
So basically what this approach would let you do, is automatically invalidate sessions yourself, which means you would have much greater control over updating the state of your database and logging users out.
Hope this helps!

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