Passport JWT authentication extract token - node.js

I am using express & jwt-simple to handle login/register & authenticated requests as a middleware api. I'm trying to create a .well-known endpoint so other api's can authenticate request based on token send in.
Here's my strategy:
module.exports = function() {
const opts = {};
opts.jwtFromRequest = ExtractJwt.fromAuthHeader();
opts.secretOrKey = securityConfig.jwtSecret;
passport.use(new JwtStrategy(opts, function(jwt_payload, done) {
// User.where('id', jwt_payload.id).fetch({withRelated: 'roles'})
console.log('jwt_payload', jwt_payload)
User.where('id', jwt_payload.id).fetch()
.then(user => user ? done(null, user) : done(null, false))
.catch(err => done(err, false));
}));
};
Here's my login route:
router.post('/login', function(req, res) {
const {username, password} = req.body;
Promise.coroutine(function* () {
const user = yield User.where('username', username).fetch();
if(user) {
const isValidPassword = yield user.validPassword(password);
if (isValidPassword) {
let expires = (Date.now() / 1000) + 60 * 30
let nbf = Date.now() / 1000
const validatedUser = user.omit('password');
// TODO: Verify that the encoding is legit..
// const token = jwt.encode(user.omit('password'), securityConfig.jwtSecret);
const token = jwt.encode({ nbf: nbf, exp: expires, id: validatedUser.id, orgId: validatedUser.orgId }, securityConfig.jwtSecret)
res.json({success: true, token: `JWT ${token}`, expires_in: expires});
} else {
res.status(401);
res.json({success: false, msg: 'Authentication failed'});
}
} else {
res.status(401);
res.json({success: false, msg: 'Authentication failed'});
}
})().catch(err => console.log(err));
});
Here's my .well-known route:
router.get('/.well-known', jwtAuth, function(req, res) {
// TODO: look over res.req.user. Don't seem to be the way to get those parameters.
// We dont take those parameters from the decrypted JWT, we seem to grab it from the user in DB.
const { id, orgId } = res.req.user.attributes;
console.log("DEBUG: userId", id)
console.log("DEBUG: USER", res.req.user)
res.json({
success: true,
userId: id,
orgId
});
});
here's my jwtAuth() function:
const passport = require('passport');
module.exports = passport.authenticate('jwt', { session: false });
How would I actually get the token in the route function & decrypt it? All this does right now which works is that it authenticates if true however I need to be able to decrypt the token to send back the stored values. I'm not sure what res.req.user.attributes comes from, is this the token?

Take a look at passport-jwt and in your passport-config (or wherever you initialize passport) setup JWT Strategy:
const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;
const jwtAuth = (payload, done) => {
const user = //....find User in DB, fetch roles, additional data or whatever
// do whatever with decoded payload and call done
// if everything is OK, call
done(null, user);
//whatever you pass back as "user" object will be available in route handler as req.user
//if your user does not authenticate or anything call
done(null, false);
}
const apiJwtOptions: any = {};
apiJwtOptions.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
apiJwtOptions.algorithms = [your.jwt.alg];
apiJwtOptions.secretOrKey = your.jwt.secret;
//apiJwtOptions.issuer = ???;
//apiJwtOptions.audience = ???;
passport.use('jwt-api', new JwtStrategy(apiJwtOptions, jwtAuth));
If you want just decoded token, call done(null, payload) in jwtAuth.
Then in your route files when you want to protect endpoints and have info about user, use as:
const router = express.Router();
router.use(passport.authenticate('jwt-api', {session: false}));
And in handler you should have req.user available. It is configurable to what property of req you store data from auth, req.user is just default.

Related

Why the Postman shows "sending request` and the application gets stuck there?

I am using the Postman to test my NodeJS application. I have written an authentication code that gets stuck at the middle of the code with just showing sending request.
How can I find the problem and resolve it?
This is my auth code:
require('dotenv').config();
const passport = require('passport');
const passportLocal = require('passport-local');
const LocalStrategy = passportLocal.Strategy;
const passportJWT = require('passport-jwt');
const JWTStrategy = passportJWT.Strategy;
const ExtractJWT = passportJWT.ExtractJwt;
var FacebookTokenStrategy = require('passport-facebook-token');
constJWT = require('jsonwebtoken');
const User = require('../model/User');
passport.use(new LocalStrategy(User.authenticate()));
passport.serializeUser(User.serializeUser());
passport.deserializeUser(User.deserializeUser());
var accessToken = null;
var refreshToken = null;
const handleLogin = async (req, res) => {
const cookies = req.cookies; //['jwt'];
console.log(`cookie available at login: ${JSON.stringify(cookies)}`);
const { username, password } = req.body;
if (!username || !password) return res.status(400).json({ 'message': 'Username and password are required.' });
const foundUser = await User.findOne({ username: username }).exec();
if (!foundUser) { console.log("a problem here");
return res.sendStatus(401);} //Unauthorized
// evaluate password
// const match = await bcrypt.compare(password, foundUser.password);
if (password) {
foundUser.authenticate(password, async function (err, model, passwordError) {
if (passwordError) {
console.log(err)
res.send('The given password is incorrect!!');
} else if (model) {
console.log(`correct password ${model}`)
// res.send('You are authenticated');
const roles = Object.values(foundUser.roles).filter(Boolean);
// createJWTs
var accessToken = JWT.sign(
{
"UserInfo": {
"username": foundUser.username,
"roles": roles
}
},
process.env.ACCESS_TOKEN_SECRET,
{ expiresIn: '10m' }
);
newRefreshToken = JWT.sign(
{ "username": foundUser.username },
process.env.REFRESH_TOKEN_SECRET,
{ expiresIn: '90d' }
);
// Changed to let keyword
let newRefreshTokenArray =
!cookies?.jwt
? foundUser.refreshToken
: foundUser.refreshToken.filter(rt => rt !== cookies.jwt);
console.log("These are tokens" , accessToken , refreshToken, newRefreshTokenArray);
if (cookies?.jwt) {
/*
Scenario added here:
1) User logs in but never uses RT and does not logout
2) RT is stolen
3) If 1 & 2, reuse detection is needed to clear all RTs when user logs in
*/
refreshToken = cookies.jwt;
const foundToken = await User.findOne({ refreshToken }).exec();
// Detected refresh token reuse!
if (!foundToken) {
console.log('attempted refresh token reuse at login!')
// clear out ALL previous refresh tokens
newRefreshTokenArray = [];
}
res.clearCookie('jwt', { httpOnly: true, sameSite: 'None', secure: true });
}
// Saving refreshToken with current user
foundUser.refreshToken = [...newRefreshTokenArray, newRefreshToken];
const result = await foundUser.save();
console.log(result);
console.log(roles);
// Creates Secure Cookie with refresh token
res.cookie('jwt', newRefreshToken, { httpOnly: true, secure: true, sameSite: 'None', maxAge: 24 * 60 * 60 * 1000 });
// Send authorization roles and access token to user
res.json({ roles, accessToken });
}
});
} else {
res.send('Please input your account password!!');
}
}
And I can also see this result on the VSCOde console:
Connected to MongoDB
Server running on port 3000
POST /auth
cookie available at login: {}
correct password {
roles: { User: 2001 },
_id: new ObjectId("629e44ba19e7596e27156cbf"),
email: 'q#q.com',
username: 'q#q.com',
password: '111111',
refreshToken: [],
salt: '1add546c232166135e1b6b16187bf3e6e321a828ad3d869c6c77003e6b279acd',
hash: 'c27155c0aaf5f8ed241c07f9d16100b261fdc53db3641b7941ec45606b4d816880f03f4d0b35871a48277c2e9a768e2683e9a506d3ffc180d9a9f326d7ec984075662b1668b2720cd0dda4cf88f4442dee6f5c06a5b94c52638bb80b81117992d297e50d7709ccfcc4cded1cf3d8f6265e27ac09ad91b2bcbb562c6772ff99792104a647dfd1ffcbbb8fac6263d607a4116ac5271573874100fba4c827f78679758d6a62ee19b08a03026c0efde1c99c57aa1cc980eb53d1062d0d87c137a73673a1a8bfd0615d790935c3c82c080c875b118f9e20110e63d7fef7701af894aad34bccf366d46ac5ad4dd0214d19727fdb01a980912fe80462131a5da208832bfc458443ba34cfbdf7486bb9b3b9cda24c7126461c7ce1064d394b0246f9f4b8bb3882b938334f1c7838f299b8386da416ed36d9339b81dbcdb2c44e3d85860c09a0a86d14b08976ef927c6b3284f78c12d12ccbf1cf5aa4c74cdc5ff47dec8162946a681d5bf4ffc547d512fcbcedc5cd6b30148e0c77fbacf002b1cf380f4b27f250698cdc51a467704088339ae0911583a6c6d38793ae1a64f8f1355b51480a657f303970c2523df377b11730932e2d4579323acb96b1ae395adc95a32e2d813317d0fd57f0489f02d0e7fb48da64119a548c8cb1705f4a996430b14e087d58cc8549a8cf6f0d698ad0a7e8db17b46bb146f897ba8557a1ca75921d27a4c6',
__v: 0
}

Storing user._id in cookie for later use

I am writing a application that uses JWTs for user authentication. I am trying to allow the user to submit or edit resources associated ONLY to there ID which is generated when they create their account in MongoDB only after the user has logged in. My instructor suggested to store the user._id within a cookie but not familiar with that process. Any tips on how I could accomplish this?
Here is an example of how my users are registered within the server
app.get('/register', function (req, res) {
res.sendFile(__dirname + '/public/register.html');
});
app.post('/register', async function (req,res) {
try {
req.body.password = await bcrypt.hash(req.body.password, 10);
var newUser = new User(req.body);
newUser.save(function(error,user){
res.redirect('/login');
});
} catch{
res.redirect('/register');
}
// console.log(newUser);
});
Here is the auth process
const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;
const bcrypt = require('bcrypt');
const opts = {}
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
opts.secretOrKey = process.env.secret;
module.exports = new JwtStrategy(opts, async (jwt_payload,done)=> {
const user = await (req.body.email);
if( jwt_payload.email === req.body.email){
return done(null, true)
}
try {
if (await bcrypt.compare(password, user.password)){
return done(null, user);
} else {
return done(null, false, { message: "Password is incorrect"});
}
} catch (e) {
return done(e);
}
})
Here is logging in
app.post('/login', async (req, res) => {
const { username, password } = req.body
const user = await User.findOne({ username }).lean()
if (!user) {
return res.sendFile(__dirname + '/public/401.html');
}
if (await bcrypt.compare(password, user.password)) {
// the username, password combination is successful
const token = jwt.sign(
{
id: user._id,
username: user.username
},
process.env.secret
)
console.log(user._id);
//I need to find a way to send over the user._id to the client side so it can be stored later to allow the user to only update their resources and to view their resources
return res.redirect('/welcome.html');
}
res.sendFile(__dirname + '/public/401.html');
})
EDIT: I am not not wanting to store the token unless that is the only way to pass the user._id to the client side due to security reasoning.
Technologies I am using are, Node.js, Express.js, Mongoose, MongoDB, JWT, passport, jwt-simple, bcrypt, methodOverride, and secrets for resetting the users password and creating the user are stored in an .env.

Passport authenticate jwt not responding

I have a basic MERN login/signup app with a dummy protected route to be visible only to logged in users. I have set up everything related to login and sign up.
I am sending the jwt token from server in a cookie. The aprt up to this works fine. I can sign up and I can login.
But when I make a GET call to /protected route, passport.jwt doesn't seem to work. Nothing happens in there.
Here is my passport.js config:
import {Strategy} from 'passport-jwt';
// import {ExtractJwt} from 'passport-jwt';
import passport from 'passport';
import mongoose from 'mongoose';
import {UserSchema} from '../models/userModel';
import {config} from './config';
const User = mongoose.model('User', UserSchema);
const cookieExtractor = (req) => {
let token = null;
if(req && req.cookies) {
token = req.cookies['jwt'];
}
return token;
}
export const passportConfig = (passport) => {
const opts = {};
opts.jwtFromRequest = cookieExtractor;
opts.secretOrKey = config.key;
passport.use(
new Strategy(opts, (jwt_payload, done) => {
User.findById(jwt_payload.id)
.then((user) => {
if(user) {
return done(null, user);
}
return done(null, false);
})
.catch((err) => {
console.log('Error in finding user by id in jwt.');
});
})
);
};
Here is the userRoutes.js:
import { addNewUser, loginUser, verifyLogin } from '../controllers/userController';
export const routes = (app) => {
app.route('/users/register')
.post(addNewUser);
app.route('/users/login')
.post(loginUser);
app.route('/users/protected')
.get(verifyLogin);
}
Finally, here are the verifyLogin and loginUser controllers in my userController.js:
export const loginUser = (req, res) => {
console.log(req.body);
if(mongoSanitize.has(req.body)) {
return res.send(400).json({error: "Characters $ and . are prohibited. Use different values."});
}
const {errors, isValid} = validateLoginInput(req.body);
if(!isValid) {
return res.status(400).json(errors);
}
const userName = req.body.userName;
const password = req.body.password;
User.findOne({userName: userName})
.then((user) => {
if(!user) {
return res.status(404).json({userNotFound: "Username not found."});
}
else {
bcrypt.compare(password, user.password)
.then((isMatch) => {
if(isMatch) {
const payload = {
id: user.id,
name: user.fullName
};
const token = jwt.sign(payload, config.key, {expiresIn: 60});
res.cookie('jwt', token, {httpOnly: true, secure: false});
res.status(200).json({success: true, token: token});
}
else {
return res.status(400).json({incorrectPassword: "Password incorrect."});
}
})
}
});
};
export const verifyLogin = () => {
console.log('protected'); //'protected' is printed
passport.authenticate('jwt', {session: false}),
(req, res) => {
console.log('here'); //'here' is not printed
const { user } = req;
res.send(200).send({user});
};
};
The controller loginUser works just fine, I can see the token in response and also I have a cookie with the jwt key:value pair.
But the 3rd controller verifyLogin doesn't do anything after passport.authenticate call.
Any ideas where I might be doing something wrong ?
Okay so I was able to fix. Anyone else having the same problem, the issue was that I wasn't using passport.authenticate as a middleware, instead I was calling it as a function with a callback on successful authentication. Here are the change that I made:
userRoutes.js:
export const routes = (app) => {
app.route('/register')
.post(addNewUser);
app.route('/login')
.post(loginUser);
app.route('/protected')
.get(passport.authenticate('jwt', {session:false}), verifyLogin);
}
And then verifyLogin just sends the logged in user information back:
export const verifyLogin = (req, res) => {
console.log('here');
const { user } = req;
res.status(200).json({message:'Secure route', user: user});
};
Everything else is pretty much the same.
I had the same problem until I changed the time to 1 hour. It seems like the value you wrote is in milliseconds, so instead of 60 you should write 60000.

How to get session data for API requests?

I want to get authorized user data. But instead I get the data of a completely different user. How to write a function getProfile to display the data of the current user?
controllers/auth.js:
const bcrypt = require('bcryptjs')
const jwt = require('jsonwebtoken')
const db = require('../config/db.config.js')
const User = db.user
module.exports.login = async function(req, res) {
const candidate = await User.findOne({
where: {
username: req.body.username
}
})
if (candidate) {
const passwordResult = bcrypt.compareSync(req.body.password, candidate.password)
if (passwordResult) {
const token = jwt.sign({
username: candidate.username,
userId: candidate._id
}, process.env.SECRET_OR_KEY, {expiresIn: 60 * 60})
res.status(200).json({
token: `Bearer ${token}`
})
} else {
res.status(401).json({
message: 'Passwords do not match. Try again.'
})
}
} else {
res.status(404).json({
message: 'User with this login was not found.'
})
}
}
module.exports.getProfile = async function(req, res) {
try {
const user = await User.findOne({id: req.body.id})
res.status(200).json(user)
} catch(e) {
errorHandler(res, e)
}
}
routes/auth.js:
const express = require('express')
const router = express.Router()
const controller = require('../controllers/auth')
const passport = require('passport')
router.post('/login', controller.login)
router.get('/profile', passport.authenticate('jwt', {session: false}), controller.getProfile)
module.exports = router
You should attach a signed token in each HTTP req from client, either by custom HTTP header or set in cookie. This token is sent only after successful login which contains user's id and other info.
After you start receiving that token you can validate it (checking for expiry or some manual change) using a middleware and that token data will be the actual user data belongs to the user loggedin.
Now, you read that header/cookie to get requester user's info and you can then send their respective data only.
Let's say if client is sending you token info in header called tkn. Your token validation can be as follows:
var jwt = require('jsonwebtoken');
const SECRET = 'whatulike';
function verifyToken(req, res, next) {
var token = req.headers.tkn || "";
if (!token.length)
return unauthorised(res, 'Token absent');
jwt.verify(token, SECRET, function(err, decoded) {
if (err)
return unauthorised(res, 'Failed to authenticate token.');
req.tkn = decoded.id;
next();
});
}
function unauthorised(res, msg){
const sc = 401;
logger.warn(`${sc} - Unauthorised request ${res.req.originalUrl}`);
res.status(sc).send({msg});
}
module.exports.verifyToken = verifyToken;
And at handler side you can read tkn data like:
module.exports.getProfile = async function(req, res) {
try {
const user = await User.findOne({id: req.tkn.userId})
res.status(200).json(user)
} catch(e) {
errorHandler(res, e)
}
}

Unauthorized after login with passport-jwt

I used passport-jwt for login. it find the user and the user can login but when after logging in, I want to redirect to the user dashboard nothing happens and it shows Unauthorized the in browser when it redirect to localhost:8080/my/
can you find out what the problem is:
p.s: when I remove passport.authenticate('jwt', {session: false}) from my.js it works and it redirects to /my/. so I think the problem is with this part of code or JWTStrategy in auth.js. I don't know how to fix it!
here's my code : (sorry it's to much code)
auth.js:
//Create a passport middleware to handle User login
passport.use(new LocalStrategy({
usernameField: 'username',
passwordField: 'password'
}, async (username, password, done) => {
try {
//Find the user associated with the email provided by the user
await User.findOne({
username: username
}, async function (err, user) {
if (!user) {
//If the user isn't found in the database, return a message
return done(null, false, {
message: 'User not found'
});
}
//Validate password and make sure it matches with the corresponding hash stored in the database
//If the passwords match, it returns a value of true.
let validate = await user.isValidPassword(password);
if (!validate) {
return done(null, false, {
message: 'Wrong Password'
});
}
//Send the user information to the next middleware
return done(null, user, {
message: 'Logged in Successfully'
});
});
} catch (error) {
return done(error);
}
}));
passport.use(new JWTStrategy({
jwtFromRequest : ExtractJWT.fromAuthHeaderAsBearerToken(),
secretOrKey : 'secret_token'
}, function(jwt_payload, done) {
User.findOne({id: jwt_payload.sub}, function(err, user) {
if (err) {
return done(err, false);
}
if (user) {
return done(null, user);
} else {
return done(null, false);
// or you could create a new account
}
});
}));
authenticate.js :
router.post('/login', async (req, res, next) => {
passport.authenticate('local', async (err, user, info) => {
try {
if (err || !user) {
const error = new Error('An Error occured')
return next(error);
}
req.login(user, {
session: false
}, async (error) => {
if (error) return next(error)
//We don't want to store the sensitive information such as the
//user password in the token so we pick only the email and id
const body = {
_id: user._id,
username: user.username
};
//Sign the JWT token and populate the payload with the user email and id
const token = jwt.sign({
user: body
}, 'top_secret');
//Send back the token to the user
// return res.json({
// token
// });
res.redirect('/my/?token='+token);
});
} catch (error) {
return next(error);
}
})(req, res, next);
});
module.exports = router;
user.js
const mongoose = require('mongoose');
const Role = require('./role');
const bcrypt = require('bcrypt');
let Schema = mongoose.Schema;
let userSchema = new Schema({
.
.
.
});
userSchema.pre('save', async function(next){
//'this' refers to the current document about to be saved
const user = this;
//Hash the password with a salt round of 10, the higher the rounds the more secure, but the slower
//your application becomes.
const hash = await bcrypt.hash(this.password, 10);
//Replace the plain text password with the hash and then store it
this.password = hash;
//Indicates we're done and moves on to the next middleware
next();
});
userSchema.methods.isValidPassword = async function(password){
const user = this;
//Hashes the password sent by the user for login and checks if the hashed password stored in the
//database matches the one sent. Returns true if it does else false.
let compare= await bcrypt.compare(password , user.password);
return compare;
}
module.exports = mongoose.model('User' , userSchema);
my.js
//secure routes that only users with verified tokens can access.
//Lets say the route below is very sensitive and we want only authorized users to have access
//Displays information tailored according to the logged in user
router.get('/',passport.authenticate('jwt', {session: false}), (req, res) => {
//We'll just send back the user details and the token
res.json({
message : 'You made it to the secure route',
token : req.query.token
});
})
module.exports = router;
This problem usually occurs because of an invalid JWT. To authenticate a JWT with passport-jwt it has to be of the following format:
"JWT xxxx.xxxx.xxxx"
So maybe try changing the following line in your Authenticate.js file:
res.redirect('/my/?token='+token);
// CHANGE IT TO:
res.redirect('/my/?token=' + 'JWT ' + token);
I hope this solves your problem.

Resources