Facebook-passport with JWT - node.js

I've been using Passport on my server for user authentication.
When a user is signing in locally (using a username and password), the server sends them a JWT which is stored in localstorage, and is sent back to server for every api call that requires user authentication.
Now I want to support Facebook and Google login as well. Since I began with Passport I thought it would be best to continue with Passport strategies, using passport-facebook and passport-google-oauth.
I'll refer to Facebook, but both strategies behave the same. They both require redirection to a server route ('/auth/facebook' and '/auth/facebook/callback' for that matter).
The process is successful to the point of saving users including their facebook\google ids and tokens on the DB.
When the user is created on the server, a JWT is created (without any reliance on the token received from facebook\google).
... // Passport facebook startegy
var newUser = new User();
newUser.facebook = {};
newUser.facebook.id = profile.id;
newUser.facebook.token = token; // token received from facebook
newUser.facebook.name = profile.displayName;
newUser.save(function(err) {
if (err)
throw err;
// if successful, return the new user
newUser.jwtoken = newUser.generateJwt(); // JWT CREATION!
return done(null, newUser);
});
The problem is that after its creation, I don't find a proper way to send the JWT to the client, since I should also redirect to my app.
app.get('/auth/facebook/callback',
passport.authenticate('facebook', {
session: false,
successRedirect : '/',
failureRedirect : '/'
}), (req, res) => {
var token = req.user.jwtoken;
res.json({token: token});
});
The code above redirects me to my app main page, but I don't get the token.
If I remove the successRedirect, I do get the token, but I'm not redirected to my app.
Any solution for that? Is my approach wrong? Any suggestions will do.

The best solution I found for that problem would be to redirect to the expected page with a cookie which holds the JWT.
Using res.json would only send a json response and would not redirect. That's why the other suggested answer here would not solve the problem I encountered.
So my solution would be:
app.get('/auth/facebook/callback',
passport.authenticate('facebook', {
session: false,
successRedirect : '/',
failureRedirect : '/'
}), (req, res) => {
var token = req.user.jwtoken;
res.cookie('auth', token); // Choose whatever name you'd like for that cookie,
res.redirect('http://localhost:3000'); // OR whatever page you want to redirect to with that cookie
});
After redirection, you can read the cookie safely and use that JWT as expected. (you can actually read the cookie on every page load, to check if a user is logged in)
As I mentioned before, it is possible to redirect with the JWT as a query param, but it's very unsafe.
Using a cookie is safer, and there are still security solutions you can use to make it even safer, unlike a query param which is plainly unsecure.

Adding to Bar's answer.
I prepared a landing component to extract the cookie, save it to local storage, delete the cookie, then redirect to an authorized page.
class SocialAuthRedirect extends Component {
componentWillMount() {
this.props.dispatch(
fbAuthUser(getCookie("auth"), () => {
document.cookie =
"auth=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
this.props.history.push("/profile");
})
);
}
render() {
return <div />;
}
}

A proper solution would be to implement the redirection on the client side.
Simply use:
app.get('/auth/facebook/callback',
passport.authenticate('facebook', {
session: false,
failureRedirect: '/login'
}), (req, res) => {
res.json({
token: req.user.jwtoken
})
}
)
If you're client side receives the token, then redirect from there to home page, and in the case the login wasn't successful, it would be redirected by the server directly.
Or you can go for the full client side management as I would:
app.get('/auth/facebook/callback',
passport.authenticate('facebook', {
session: false
}), (req, res) => {
if (req.user.jwtoken) {
res.json({
success: true,
token: req.user.jwtoken
})
} else {
res.json({
success: false
})
}
}
)
If success === true, store JWT in LocalStorage, else redirect to login page.

Related

Can Hacker modify the request that is sent to the web server? I am authenticating the user based on a object in the request

I am a beginner to nodejs and I am creating my web app. I use passportJs for authentication. As it is mentioned in the documentation that when the user is successfully authenticated, req.user will be created and can be accessed in any route.
My admin.handlebars
router.get('/' , (req , res)=>{
const current_user = req.user
if (!req.user) {
res.redirect('/login')
} else{
res.render('admin/index',{user_data:current_user })
}
})
Authentication using passportJS
passport.use(new LocalStrategy({usernameField:'email'}, (email,password,done)=>{
userModel.findOne({email:email}).then((user)=>{
if (!user) return done(null,false,{message: "No User found"})
bcrypt.compare(password,user.password,(err,matched)=>{
if(err)return err
if(matched){
return done(null,user)
}else {return done(null,false,{message:"Incorrect Password"})}
})
})
}))
passport.serializeUser(function(user, done) {
done(null, user.id);
});
passport.deserializeUser(function(id, done) {
userModel.findById(id, function(err, user) {
done(err, user);
});
});
router.post('/login',
(req,res,next)=>{
passport.authenticate('local', { successRedirect: '/admin',
failureRedirect: '/login',
failureFlash: 'Invalid username or password.'}
)(req,res,next)}
)
Here is my question:
As you see user will be redirected to admin page only if the .user exists in the req. So can a hacker add an empty .user to the request and access my admin page?
Its kind of weird question tho. Is there any better way to do this? Thanks in advance :)
End-user(in your case hacker) can add any type of data to any request. So yes, end-user can modify requests to send req.user within it. However, they won't be able to access the data within it and their request will not be accepted on your "admin" endpoint if you use req.isAuthenticated().
This is because passport JS serialises the user and stores the information in session after encryption. So UNLESS the end-user (Hacker) has access to another user's machine and copies all the session details (Browser's don't allow other sites to access another sites session) from their browser and use it, they won't be able to use admin.
TLDR;
No they wont be able to access "admin" endpoint by simply adding req.user in their request.

ExpressJS/Passport-SAML Single Log Out re-logs in directly

Currently I am working on a passport-saml implementation in our NodeJS application.
The reason to do so is to give our customers the possibility to connect to their AD FS systems and take advantage of SingleSignOn(SSO).
As we also want to give logout functionality I was working on that logic. However, I can't seem to get this simple piece of functionality working. I have already googled a lot, tried a lot of variations and configurations but unfortunately, it does not work.
I would like to give our customers the possibility to SingleLogOut (SLO) that is both SP and IdP driven. This was my starting point. During the course of debugging and development I already took a step back and tried to kill the local session but even that is not possible it seems.
This is the relevant code from the routes I configured for SAML:
const isAuthenticated = (req, res, next) => {
if (req.isAuthenticated()) {
// User logged in, pass on to next middleware
console.info('User authenticated');
return next();
}
// User not logged in, redirect to login page
console.info('User not authenticated');
return res.redirect('/login');
};
// GET-routes
app.get('/',
isAuthenticated,
(req, res) => {
res.send('Authenticated');
});
app.get('/login',
passport.authenticate('saml', {
successRedirect: '/',
failureRedirect: '/login/fail',
}));
app.get('/logout',
(req, res) => {
passport._strategy('saml').logout(req, (err, url) => {
return res.redirect(url);
});
});
// POST-routes
app.post('/adfs/callback',
(req, res, next) => {
passport.authenticate('saml', (err, user) => {
// If error occurred redirect to failure URL
if (err) return res.redirect('/login/fail');
// If no user could be found, redirect to failure URL
if (!user) return res.redirect('/login/fail');
// User found, handle registration of user on request
req.logIn(user, (loginErr) => {
if (loginErr) return res.status(400).send(err);
// Request session set, put in store
store.set(req.sessionID, req.session, (storeErr) => {
if (storeErr) return res.status(400).send(storeErr);
return res.redirect('/');
});
});
})(req, res, next);
});
app.post('/logout/callback', (req, res) => {
// Destroy session and cookie
store.destroy(req.sessionID, async (err) => {
req.logout();
return res.redirect('/');
});
});
As can be seen I took control of the session store handling (setting and destroying sessions, but if this is unwise please advise).
The session store implemented is the MemoryStore (https://www.npmjs.com/package/memorystore).
What happens is that, when a user is logged in everything works fine.
Then a request is sent to route /logout, and some stuff happend and I can see the session changing, the session ID gets changed as well as relevant parameters for passport-saml (nameID, sessionIndex) and the user is then rerouted to '/'.
However then the user is seen as not authenticated and rerouted to '/login'. One would argue that it stops here, as the credentials have to be re-entered.
This is not the case, as the user is directly logged in again, without re-entering credentials and I do not know how to prevent this.
I do hope anybody knows what's going on :)
If there is need for additional information I would like to hear gladly.
So after much research and investigation I did found the solution for this problem.
The trick was in the definition of the passport-saml package, in particular the authncontext parameter.
So previously I had the SamlStrategy options defined as:
{
// URL that should be configured inside the AD FS as return URL for authentication requests
callbackUrl: `<URL>`,
// URL on which the AD FS should be reached
entryPoint: <URL>,
// Identifier for the CIR-COO application in the AD FS
issuer: <identifier>,
identifierFormat: null,
// CIR-COO private certificate
privateCert: <private_cert_path>,
// Identity Provider's public key
cert: <cert_path>,
authnContext: ["urn:federation:authentication:windows"],
// AD FS signature hash algorithm with which the response is encrypted
signatureAlgorithm: <algorithm>,
// Single Log Out URL AD FS
logoutUrl: <URL>,
// Single Log Out callback URL
logoutCallbackUrl: `<URL>`,
}
But after much research I realised this authentication:windows option was the culprit, so I changed it to:
{
// URL that should be configured inside the AD FS as return URL for authentication requests
callbackUrl: `<URL>`,
// URL on which the AD FS should be reached
entryPoint: <URL>,
// Identifier for the CIR-COO application in the AD FS
issuer: <identifier>,
identifierFormat: null,
// CIR-COO private certificate
privateCert: <private_cert_path>,
// Identity Provider's public key
cert: <cert_path>,
authnContext: ["urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport",
"urn:federation:authentication:windows"],
// AD FS signature hash algorithm with which the response is encrypted
signatureAlgorithm: <algorithm>,
// Single Log Out URL AD FS
logoutUrl: <URL>,
// Single Log Out callback URL
logoutCallbackUrl: `<URL>`,
},
Which basically means it won't retrieve the Windows credentials of the user that is logged onto the system by default, thus redirecting to the login screen of the ADFS server.
Using authnContext: ["urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"] in the session setup will show the username and password page again after logging out.
Logout achieved https://adfs-url/adfs/ls/?wa=wsignout1.0.
A new session-id is created after signing in again.

jwt token validation on main page

before i started working with reactJS i was using express sessions (with expressJS of course) to determine whether user was authenticated or not, my middleware was passed in /profile URL like this router.use('/profile', middleware, require('./profilePageFile')) and if user was not authenticated i was redirecting to login page with simple code
if(!req.session.user){
res.redirect('/login')
}
i tried to use redirecting with react too but since react has it's own routing system (react-router-dom) and express is only needed for creating APIs when i was logging in /profile url it was still showing me page content and redirecting me after xxx milliseconds later, and i think it would be better practice if i have my profile page and main page on default url ( 'domain.com/' ), as i see many websites are using this technique including Facebook, at this point i was trying to make something like this: if user has not token or token expired, don't display some "hello user" button, otherwise display it. my only problem is that i do not know how to do that.
if i have boolean in my react state called isAuthenticated or something like this which determines whether user is authenticated or not according to the header that i send from server-side, it would be bad practice for security, i think, and also when i tried that, it did not work anyway. at this point only thing that i can do is to pass req.userId to client if token exists. this works but it is not enough, if anyone got the point i will be glad if i get help
here is my middleware code
const guard = (req, res, next) => {
const token =
req.body.token ||
req.query.token ||
req.headers["x-access-token"] ||
req.cookies.token;
if (!token) {
res.status(401).send({ auth: false });
} else {
jwt.verify(token, process.env.SECRET, function(err, decoded) {
if (err) {
return res.status(500).send({
message: err.message
});
}
req.userId = decoded.id;
res.status(200).send({ auth: true });
next();
});
}
};
I have made two changes to your code.
const guard = (req, res, next) => {
const token = req.body.token ||
req.query.token ||
req.headers['x-access-token'] ||
req.cookies.token;
if (!token) {
// Authentication failed: Token missing
return res.status(401).send({ auth: false })
}
jwt.verify(token, process.env.SECRET, function (err, decoded) {
if (err) {
// Authentication failed: Token invalid
return res.status(401).send({
auth: false,
message: err.message
})
}
req.userId = decoded.id
next()
})
}
First, inside the if(err) condition I have changed the status code to 401 because if the token is invalid, it will raise the error here.
Secondly, I have removed the res.status(200).send({auth:true}) from the bottom of the function.
This is because the middleware should pass on to the route (which we are trying to protect with the JWT check) to respond. This was responding to the request before it got to the actual route.

Passport-jwt authenticate not working well with node-jwt-simple

I'm using passport-jwt to authenticate some routes and I'm creating my jwts with node-jwt-simple/jwt-simple but facing some difficulties cause it looks like my passport-jwt authenticate middleware is not being called at all.
Here is my
passport-jwt-strategy
const jwtOpts = {
jwtFromRequest: ExtractJwt.fromHeader('Authorization'),
secretOrKey: secret,
};
passport.use(new jwtStrategy(jwtOpts, (payload, done) => {
console.log('payload ', payload.sub);
User.findById(payload.sub, (err, user) => {
if(err) { return done(err); }
if(!user) { console.log('didnt find!'); return done(null, false); }
done(null, user);
});
}));
which i'm then integrating it over here.
routes file
router.get('/success',
passport.authenticate('jwt', {session: false}),
async (ctx, next) => ctx.body = await "success!");
Here is also the way I make my jwt.
function tokenForUser(user) {
const timeStamp = new Date().getTime;
return jwt.encode({sub: user._id, iat: timeStamp}, secret);
}
//- Later in signup process
userToSave.save(async(err, user) => {
if(err) { return next(err); }
const token = await tokenForUser(user);
next(token);
});
//- If this helps, here is how my secret file looks like.
const secret = "JKAha23ja1ddHdjjf31";
export default secret;
Problem comes, when I hit that route i only get Unauthorized and in the console nothing gets logged out not even the 'payload' key I specified first.
I should also say that I have the token at ctx.request.get('Authorization') (Koa based) i think it's something like req.header('Authorization') with express in all routes.
Also The exact express based problem can be found on the github issues of node-jwt-simple here incase there is any problem with my code samples.
Thank you.
After I wrapped my head right i knew that this has been my horrible understanding of how the whole authentification process works.
When I decoded the token from ctx.get('Authorization') I got a different _id than the one stored in the db Because I had hardcoded Authorization header in postman and thought "If I ctx.set('Authorization', token); It will replace the one I hardcoded on postman".
Less did I think that this jwt will be included in a header of requests when I make http calls on front end.
I naively thought jwts are passed directly from the server to the browser (Something like how render works) and Not from the server to an ajax process which later embeds it in request made which is the correct way.
The whole code is awesome, except now I have to just pass the token ctx.body = token; after I created it when I signed up.
Thank You.

Passport & JWT & Google Strategy - Disable session & res.send() after google callback

Using: passport-google-oauth2.
I want to use JWT with Google login - for that I need to disable session and somehow pass the user model back to client.
All the examples are using google callback that magically redirect to '/'.
How do I:
1. Disable session while using passport-google-oauth2.
2. res.send() user to client after google authentication.
Feel free to suggest alternatives if I'm not on the right direction.
Manage to overcome this with some insights:
1. disable session in express - just remove the middleware of the session
// app.use(session({secret: config.secret}))
2. when using Google authentication what actually happens is that there is a redirection to google login page and if login is successful it redirect you back with the url have you provided.
This actually mean that once google call your callback you cannot do res.send(token, user) - its simply does not work (anyone can elaborate why?). So you are force to do a redirect to the client by doing res.redirect("/").
But the whole purpose is to pass the token so you can also do res.redirect("/?token=" + token).
app.get( '/auth/google/callback',
passport.authenticate('google', {
//successRedirect: '/',
failureRedirect: '/'
, session: false
}),
function(req, res) {
var token = AuthService.encode(req.user);
res.redirect("/home?token=" + token);
});
But how the client will get the user entity?
So you can also pass the user in the same way but it didn't felt right for me (passing the whole user entity in the parameter list...).
So what I did is make the client use the token and retrieve the user.
function handleNewToken(token) {
if (!token)
return;
localStorageService.set('token', token);
// Fetch activeUser
$http.get("/api/authenticate/" + token)
.then(function (result) {
setActiveUser(result.data);
});
}
Which mean another http request - This make me think that maybe I didnt get right the token concept.
Feel free to enlighten me.
Initialize passport in index.js:
app.use(passport.initialize());
In your passport.js file:
passport.use(
new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL:
'http://localhost:3000/auth/google/redirect',
},
async (accessToken, refreshToken, profile,
callback) => {
// Extract email from profile
const email = profile.emails![0].value;
if (!email) {
throw new BadRequestError('Login failed');
}
// Check if user already exist in database
const existingUser = await User.findOne({ email
});
if (existingUser) {
// Generate JWT
const jwt = jwt.sign(
{ id: existingUser.id },
process.env.JWT_KEY,
{ expiresIn: '10m' }
);
// Update existing user
existingUser.token = jwt
await existingUser.save();
return callback(null, existingUser);
} else {
// Build a new User
const user = User.build({
email,
googleId: profile.id,
token?: undefined
});
// Generate JWT for new user
const jwt = jwt.sign(
{ id: user.id },
process.env.JWT_KEY,
{ expiresIn: '10m' }
);
// Update new user
user.token = jwt;
await auth.save();
return callback(null, auth);
}
}));
Receive this JWT in route via req.user
app.get('/google/redirect', passport.authenticate('google',
{failureRedirect: '/api/relogin', session: false}), (req, res) => {
// Fetch JWT from req.user
const jwt = req.user.token;
req.session = {jwt}
// Successful authentication, redirect home
res.status(200).redirect('/home');
}

Resources