Is it secure to use jwt payload for some authorization? - node.js

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.

Related

JWT Authentication with NextJS

I've searched around a bit, but have not found any clear, up-to-date, answers on this topic.
I'm trying to implement JWT authentication in my NextJS application. The following is what I have so far.
/login endpoint that will (1) check that the user/pass exists and is valid, and (2) create a JWT token based on a private RS256 key.
Created a middleware layer to verify the JWT
The creation of the JWT is fine - it works perfectly well reading the key from the file-system and signing a JWT.
However, I've run into the problem of the middleware being unable to use node modules (fs and path) because of the edge runtime (read here). This makes it so I'm unable to read the public key from the FS.
What is the proper way to verify a JWT token on every request? I've read that fetching from middleware is bad practice and should be avoided. All other reference on this topic (that I found) either uses a "secret" instead of a key (and can therefor be put into process.env and used in middleware) or glosses over the fact (1). Or should I just create a separate express application to handle JWT creation/verifying?
What I do is add a file (_middleware.tsx) within pages. This ensures the file runs each page load but is not treated as a standard page. In this file lives an Edge function. If the user is not signed in (no JWT in cookies) and tries to access a protected page, he is immediately redirected to /signin before the server is even hit.
import { NextResponse } from "next/server";
const signedInPages = ["/admin", "/members "];
export default function middleware(req) {
if (signedInPages.find((p) => p === req.nextUrl.pathname)) {
const { MY_TOKEN: token } = req.cookies;
if (!token) {
return NextResponse.redirect("/signin");
}
}
}
If you signed the token from
import jwt from "jsonwebtoken";
then you can use same jwt for verifying token inside a middleware function. you create _middleware.js inside pages directory and middleware will run for each request before request hits the endpoint:
import { NextResponse } from "next/server";
import jwt from "jsonwebtoken";
export async function middleware(req, ev) {
const token = req ? req.cookies?.token : null;
let userId=null;
if (token) {
// this is how we sign= jwt.sign(object,secretKey)
// now use the same secretKey to decode the token
const decodedToken = jwt.verify(token, process.env.JWT_SECRET);
userId = decodedToken?.issuer;
}
const { pathname } = req.nextUrl;
// if user sends request to "/api/login", it has no token. so let the request pass
if (
pathname.includes("/api/login") || userId
) {
return NextResponse.next();
}
if (!token && pathname !== "/login") {
return NextResponse.redirect("/login");
}
}
you send the token in the post request header.
{Authorization: 'Bearer ' + token}
And then verify that token on the server, before sending data back
To verify the token you create a middleware function, then use that function on any route you want to portect
router.get("/", middlewarefunction, function (req: any, res: any, next: any) {
res.send("/api")
})

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 keep a user logged in after page refresh

I have a simple web application that a user can create an account and login ... whenever a user log in i am generating a json-web-token for him and storing it on his database object ... im using that token to authenticate the user when he visits certain routes ( Bearer token on the header ) but the problem is that when the user refreshes the page i lose that token and he have to provide the email and password again to generate another token ... i was thinking about using localStorage or cookies but maybe there is a better / commonly used way for this ... any advises would be helpful thanks .
router.post('/user/login' ,async (req,res)=>{
try {
const user = await User.findByCredentials(req.body.email,req.body.password)
const token = await user.generateToken()
res.send({ user, token })
} catch(e) {
res.status(404).send(e)
}
})
axios({
method: "post",
url: "/api/user/login",
data: {
email: email,
password: password
}
})
The traditional way to use Persistent session(remember me feature) is to use cookies.
You can make set the max age lets say 30 days when logging in.
router.post('/user/login', function(req, res) {
....
req.session.cookie.maxAge = 30 * 24 * 60 * 60 * 1000; // Cookie expires after 30 days
....
});
The logical flow should be:
When the user successfully logs, a login cookie is issued.
It should contain some kind of token which you then need to HASH(hash(cookie)) and store it in DB.
When a non-logged-in user visits the site and presents a login cookie, the series identifier is looked up in the database.
If it finds the cookie it gets authenticated, otherwise asks to login.
Start the session on the first line of your page, and store the login credentials in it. You can do the same for all other pages to be accessed after login.

How to include access-token in the HTTP header when requesting a new page from browser

The similar question was asked by someone else (here) but got no proper answer. Since this is basic and important for me (and maybe for someone else as well), I'm trying to ask here. I'm using Node.js+Express+EJS on the server side. I struggled to make the token authentication succeeded by using jsonwebtoken at the server and jQuery's ajax-jsonp at the web browser. Now after the token is granted and stored in the sessionStorage at the browser side, I can initiate another ajax request with the token included in the request header, to get the user's profile and display it somewhere in the 'current' page. But what I want is to display a new web page to show the user's profile instead of showing it in the 'current' page (the main/index page of the website). The question is:
How to initiate such an HTTP GET request, including the token in the HTTP header; and display the response as a new web page?
How the Node.js handle this? if I use res.render then where to put the js logic to verify the token and access the DB and generate the page contents?
Or, should we say the token mechanism is more suitable for API authentication than for normal web page authentication (where the web browser provides limited API)?
I think the answer to this question is important if we want to use the token mechanism as a general authentication since in the website scenario the contents are mostly organized as web pages at the server and the APIs at the client are provided by the browser.
By pure guess, there might be an alternative way, which the ajax success callback to create a new page from the current page with the response from the server, but I have no idea of how to realize that as well.
By calling bellow code successfully returned the HTML contents in customer_profile.ejs, but the client side ajax (obviously) rejected it.
exports.customer_profile = function (req, res) {
var token = req.headers.token;
var public_key = fs.readFileSync(path.resolve() + '/cert/public_key.pem');
var decoded = jwt.verify(token, public_key);
var sql = 'SELECT * FROM customer WHERE username = "' + decoded.sub + '"';
util.conn.query(sql, function (err, rows) {
if (!err) {
for (var i = 0; i < rows.length; i++) {
res.render('customer_profile', {customer_profile: rows[i]});
break;
}
}
});
};
I am trying to find a solution to this as well. Please note, I am using Firebase for some functionality, but I will try to document the logic as best as I can.
So far what I was able to figure out is the following:
Attach a custom header to the HTTP request client-side
// landing.js - main page script snippet
function loadPage(path) {
// Get current user's ID Token
firebase.auth().currentUser.getIdToken()
.then(token => {
// Make a fetch request to 'path'
return fetch(`${window.location.origin}/${document.documentElement.lang}/${path}`, {
method: 'GET',
headers: {'X-Firebase-ID-Token': token} // Adds unverified token to a custom header
});
})
.then(response => {
// As noted below, this part I haven't solved yet.
// TODO: Open response as new webpage instead of displaying as data in existing one
return response.text();
})
.then(text => {
console.log(text);
})
.catch(error => {
console.log(error);
});
}
Verify the token according to your logic by retrieving the corresponding header value server-side
// app.js - main Express application server-side file
// First of all, I set up middleware on my application (and all other setup).
// getLocale - language negotiation.
// getContext - auth token verification if it is available and appends it to Request object for convenience
app.use('/:lang([a-z]{2})?', middleware.getLocale, middleware.getContext, routes);
// Receives all requests on optional 2 character route, runs middleware then passes to router "routes"
// middleware/index.js - list of all custom middleware functions (only getContext shown for clarity)
getContext: function(req, res, next) {
const idToken = req.header('X-Firebase-ID-Token'); // Retrieves token from header
if(!idToken) {
return next(); // Passes to next middleware if no token, terminates further execution
}
admin.auth().verifyIdToken(idToken, true) // If token provided, verify authenticity (Firebase is kind enough to do it for you)
.then(token => {
req.decoded_token = token; // Append token to Request object for convenience in further middleware
return next(); // Pass on further
})
.catch(error => {
console.log('Request not authorized', 401, error)
return next(); // Log error to server console, pass to next middleware (not interested in failing the request here as app can still work without token)
});
}
Render and send back the data
// routes/index.js - main router for my application mounted on top of /:lang([a-z]{2})? - therefore routes are now relative to it
// here is the logic for displaying or not displaying the page to the user
router.get('/console', middleware.getTranslation('console'), (req, res) => {
if(req.decoded_token) { // if token was verified successfully and is appended to req
res.render('console', responseObject); // render the console.ejs with responseObject as the data source (assume for now that it contains desired DB data)
} else {
res.status(401).send('Not authorized'); // else send 401 to user
}
});
As you can see I was able to modularize the code and make it neat and clear bu use of custom middleware. It is right now a working API returning data from the server with the use of authentication and restricted access
What I have not solved yet:
As mentioned above, the solution uses fetch API and result of the request is data from server (html) and not a new page (i.e when following an anchor link). Meaning the only way with this code now is to use DOM manipulation and setting response as innerHTML to the page. MDN suggests that you can set 'Location' header which would display a new URL in the browser (the one you desire to indicate). This means that you practically achieved what both, you and I wanted, but I still can't wrap my head around how to show it the same way browser does when you follow a link if you know what I mean.
Anyways, please let me know what you think of this and whether or not you were able to solve it from the part that I haven't yet

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