Working with express session and angularjs - angularjs

I have a problem trying to create an APIRest with express.
Currently, I have register and login working correctly using MongoDB and passport, the problem is that when I login, I need the API to understand that the user is still logged in, so I'm using:
//Session
app.use(session({
secret: 'keyboard cat',
resave: false,
saveUninitialized: true,
cookie: { httpOnly: false, maxAge: null, secure: false },
store: new MongoStore({
url: configDB.url,
collection: 'sessions'
})
}));
To check if the user is authenticated, i'm using:
//Confirm Login status
app.get('/api/loggedin', function (req, res) {
return res.send(req.isAuthenticated() ? req.user : 'Not Logged!');
});
With the function:
function IsAuthenticated(req, res, next) {
if (req.isAuthenticated()) {
next();
} else {
next(res.send('Sorry!'));
}
}
Using Postman, it works just fine, I can see te cookie "connect.sid". But when I login from angularjs using this endpoint, the cookie is not beeing set, and basically, it does not work, returns "Not Logged!".
PS: I'm using ionic as my framework. My node API server is under Azure webapp.
Any question lt me know guys, thanks so far!

Related

Cross-Domain Session Cookie (Express API on Heroku + React App on Netlify)

I have a React App making calls to an API in node.js/Express.
Frontend is deployed in Netlify (https), Backend deployed on Heroku (https).
My problem:
Everything working in dev environment (localhost)
In production (Netlify/Heroku), the api calls to register and login seem to work but the session cookie is not stored in the browser. Because of that, any other calls to protected routes in the API fail (because I don't receive the user credentials).
             
Talking is cheap, show me the code....
Backend (Express API):
I'm using passport (local strategy), express-session, cors.
App.js
require('./configs/passport');
// ...
const app = express();
// trust proxy (https://stackoverflow.com/questions/64958647/express-not-sending-cross-domain-cookies)
app.set("trust proxy", 1);
app.use(
session({
secret: process.env.SESSION_SECRET,
cookie: {
sameSite: process.env.NODE_ENV === "production" ? 'none' : 'lax',
maxAge: 60000000,
secure: process.env.NODE_ENV === "production",
},
resave: true,
saveUninitialized: false,
ttl: 60 * 60 * 24 * 30
})
);
app.use(passport.initialize());
app.use(passport.session());
// ...
app.use(
cors({
credentials: true,
origin: [process.env.FRONTEND_APP_URL]
})
);
//...
app.use('/api', require('./routes/auth-routes'));
app.use('/api', require('./routes/item-routes'));
CRUD endpoint (ex. item-routes.js):
// Create new item
router.post("/items", (req, res, next) => {
Item.create({
title: req.body.title,
description: req.body.description,
owner: req.user._id // <-- AT THIS POINT, req.user is UNDEFINED
})
.then(
// ...
);
});
Frontend (React App):
Using Axios with the option "withCredentials" set to true...
User registration and login:
class AuthService {
constructor() {
let service = axios.create({
baseURL: process.env.REACT_APP_API_URL,
withCredentials: true
});
this.service = service;
}
signup = (username, password) => {
return this.service.post('/signup', {username, password})
.then(response => response.data)
}
login = (username, password) => {
return this.service.post('/login', {username, password})
.then(response => response.data)
}
//...
}
Creating a new item...:
axios.post(`${process.env.REACT_APP_API_URL}/items`, {
title: this.state.title,
description: this.state.description,
}, {withCredentials:true})
.then( (res) => {
// ...
});
Short answer:
It wasn't working as expected because I was testing on Chrome Incognito and, by default, Chrome blocks third party cookies in Incognito mode (more details).
Below is a list with some things to check if you're having a similar issue ;)
Checklist
In case it helps, here's a checklist with different things that you main need ;)
(Backend) Add "trust proxy" option
If you're deploying on Heroku, add the following line (you can add it before your session settings).
app.set("trust proxy", 1);
(Backend) Check your session settings
In particular, check the option sameSite and secure (more details here).
The code below will set sameSite: 'none' and secure: true in production:
app.use(
session({
secret: process.env.SESSION_SECRET || 'Super Secret (change it)',
resave: true,
saveUninitialized: false,
cookie: {
sameSite: process.env.NODE_ENV === "production" ? 'none' : 'lax', // must be 'none' to enable cross-site delivery
secure: process.env.NODE_ENV === "production", // must be true if sameSite='none'
}
})
);
(Backend) CORS config
app.use(
cors({
credentials: true,
origin: [process.env.FRONTEND_APP_URL]
})
);
(Backend) Environment Variables
Setup the environment variables in Heroku. For example:
FRONTEND_APP_URL = https://my-project.netlify.app
IMPORTANT: For the CORS URL, avoid a trailing slash at the end. The following may not work:
FRONTEND_APP_URL = https://my-project.netlify.app/ --> avoid this trailing slash!
(Frontend) Send credentials
Make sure you're sending credentials in your API calls (you need to do that for all calls you make to the API, including the call for user login).
If you're using axios, you can do use withCredentials option. For example:
axios.post(`${process.env.REACT_APP_BACKEND_API_URL}/items`, {
title: this.state.title,
description: this.state.description,
}, {withCredentials:true})
.then( (res) => {
// ...
});
(Browser) Check the configuration for third-party cookies
For testing, you probably want to make sure you're using the default configuration provided by each browser.
For example, as of 2021, Chrome blocks third-party cookies in Incognito mode (but not in "normal" mode), so you probably want to have something like this:
...and deal with browser restrictions...:
Finally, keep in mind that each browser has a different policy for third party cookies and, in general, those restrictions are expected to increase in the coming years.
For example, Chrome is expected to block third-party cookies at some point in 2023 (source).
If your App needs to bypass those restrictions, here are some options:
Implement Backend & Frontend under the same domain
Implement Backend & Frontend under subdomains of the same domain (example, example.com & api.example.com)
Have your Backend API under a proxy (if you're using Netlify, you can easily setup a proxy using a _redirects file)
The issue comes down to third party cookies.
If you're sending data from server.herokuapp.com to site.herokuapp.com you're going to have this issue.
The solution is to use a custom domain for your Heroku applications.
Please see this post for more details: Cookies Only set in Chrome - not set in Safari, Mobile Chrome, or Mobile Safari

How to pass passport facebook data to angular?

I am using passport facebook for user authentication in my web app. My Node backed is running on localhost:8080 and angular frontend is running on localhost:4200. How can I save the data received from Facebook, Save it to a database and then pass that database data to my angular frontend? I tried so many guides and tutorials online, all of those are running on the same domain, but mine is different domains(8080 & 4200).
Below is my social auth code, if it can be of any help.
module.exports = function(app, db) {
var express = require('express'),
ObjectID = require("mongodb").ObjectID,
passport = require('passport'),
FacebookStrategy = require('passport-facebook').Strategy,
GoogleStrategy = require( 'passport-google-oauth2' ).Strategy,
LinkedInStrategy = require('passport-linkedin');
var authConfig = require('../config/socialConfig');
var session = require('express-session');
app.use(passport.initialize());
app.use(passport.session());
app.use(session({
secret: 'keyboard cat',
resave: false,
saveUninitialized: true,
cookie: { secure: false }
}))
passport.serializeUser(function(user, done) {
done(null, user.id);
});
passport.deserializeUser(function(id, done) {
User.findById(id, function(err, user) {
done(err, user);
});
});
passport.use(new FacebookStrategy({
clientID: authConfig.facebookAuth.clientID,
clientSecret:authConfig.facebookAuth.clientSecret ,
callbackURL: authConfig.facebookAuth.callbackURL,
profileFields: ['id', 'displayName', 'photos', 'email']
},
function(token, refreshToken, profile, done) {
console.log("Hello" + profile);
// User.findOrCreate(..., function(err, user) {
// if (err) { return done(err); }
// done(null, user);
// });
done(null, profile);
}
));
app.get('/auth/facebook/callback', passport.authenticate('facebook', { failureRedirect: '/login' }));
app.get('/auth/facebook', passport.authenticate('facebook', { scope: 'email' }));
}
And below is my frontend link to facebook auth
Facebook Login
Any help will be highly appreciated. Looking forward to some help, thanks in advance.
Since you Node.js app is on the other port, you need to specify the full URL to the /auth/facebook API (http://localhost:4020/auth/facebook).
Also, quoting the another post:
For two documents to be considered to have the same origin, the protocol >(http/https), the domain and the port (the default 80 or :xx) have to be >indentical
So you need to enable CORS on your node.js server. An easy way to do it is to use Express-cors npm
const cors = require('cors')
const app = express()
app.use(cors())

How to use the session from passportjs

I am able to login using passport-local. I want to test if the session created by passport is valid. I am logging in from Angular. When the user logs in, i dont create any manual cookie but see a connect.sid cookie is created. Now from Angular I'm sending another req:
$scope.test = function(){
$http.get('\test').then(function(response){
if(response){
console.log(response);
} else {
console.log("Nothing Returned!");
}
});
}
And in node :
app.use(session({
secret: 'mysecret',
resave: true,
saveUninitialized: true
}));
//Passport Init
app.use(passport.initialize());
app.use(passport.session());
passport.serializeUser(function(user, done) {
done(null, user.id);
});
passport.deserializeUser(function(id, done) {
User.findById(id, function(err, user) {
done(err, user);
});
});
app.get('\test', function(req, res){
//Tried the following (one at a time) :
var user = req.user;
var user = req[user];
var user = req["user"];
var user = req.session;
console.log(user);
res.send(user);
});
None of the above works. I get res code 200 but nothing in response.data in Angular and nothing in undefined in node console.
Im doing this cause I think :
After user logs in, Passportjs creates session is persisted until its destroyed by logout.
After user logs in, there is no need to create a cookie and send it to Angular. Passport does this automatically.
When Angular sends any request, node can access the session of req and verify with it's own session.
Am I correct with all these 3 points?
Many thanks!
EDIT
My mongoose schema:
var UserSchema = mongoose.Schema({
id : {type:String, default:"abc123"},
username: {type:String, index: true},
password: String,
email: {type:String, unique:true}
});
module.exports = mongoose.model('User', UserSchema);
EDIT 2
Adding output that i get when placed the express session before passport session
EDIT 3
My strategy :
passport.use(new LocalStrategy(
function(username, password, done){
User.findOne({username: username}, function(err, doc){
if(err) {
// console.log(err);
return done(err);
}
return done(null, doc);
});
}
));
You need to change the order, put 'express session' before 'passport session'. It should work this way:
app.use(session({
secret: 'mysecret',
resave: true,
saveUninitialized: true
}));
app.use(passport.initialize());
app.use(passport.session());
http://passportjs.org/docs/configure
Are you calling passport.initialize() and passport.session() ?
passport.session is the call that reads the cookie and stores user in req object.
Check this answer for more details: What does passport.session() middleware do?

Passport session disappears after oauth callback

Making a hybrid app with Ionic, Angular, nodejs, etc.
User logs in with email and password and then wants to add 3rd party authentication to their account.
They are serialized into session.
We check, using passport, if they are authorized with 3rd party and if not send them to do so.
When the user comes back to the callback url we don't know who they are anymore because req.session is undefined.
Edit: I've been trying to simplify the code to get to the route of the problem.
// require everything and app.use them
// this is what I'm using for the session config
app.use(session({
secret: 'thisIsASecret',
resave: false,
saveUninitialized: false,
cookie: {secure: true, maxAge: (4*60*60*1000)}
}));
var user = { // simple user model for testing
id: 1,
username: 'username',
password: 'password',
oauthId: null
};
passport.serializeUser(function(user, done) {
done(null, user.id);
});
passport.deserializeUser(function(id, done) {
done(err, user);
});
// Local Passport
passport.use(new LocalStrategy(function(username, password, done) {
return done(null, user);
}));
app.post('/login', passport.authenticate('local'), function(req, res) {
console.log(req.session); // Prints out session object with passport.user = 1
res.end();
});
// oauth Passport
passport.use(new oauthStrategy({
clientID: ****,
clientSecret: ****,
callbackURL: 'http://localhost:3000/auth/oauth/callback',
passReqToCallback: true
}, function(req, accessToken, refreshToken, profile, done) {
console.log(req.session); // no passport object in session anymore
return done(null, profile);
}));
app.get('/auth/oauth', passport.authorize('oauth'));
app.get('/auth/oauth/callback', passport.authorize('oauth'), function(req, res) {
console.log(req.session); // no passport object in session here either
res.end();
});
On the client side after logging in I use this because the regular http request method doesn't work.
window.location.href = 'http://localhost:3000/auth/oauth';
Edit 2: Ionic doesn't allow sessions apparently. So I found that you can use the state parameter to send a token with the oauth request which comes back to the callback and use that to link the oauth details to the user's account.
app.get('auth/oauth/:token', function(req, res) {
passport.authorize('oauth', {state: req.params.token});
});
The only problem is now it won't redirect to the 3rd party to authorize with them. Just times out...
The solution was to use the route like this, where token is used to identify the user.
app.get('auth/oauth/:token', function(req, res, next) {
passport.authorize('oauth', {state: req.params.token})(req, res, next);
});
Then the token was available in the callback (req.query.state) and we can add the new details to our existing user details.

passportjs and backbone: authenticating users

Currently I been using a chrome app called Postman to test my services from nodejs/express/passportjs.
Currently I'm having trouble wrapping my head around how I should grab the user info and authenticate it with backbone.
I would try to authenticate the user like so:
$.post("http://localhost:3000/login", { username: "joe", password: "pass" })
.done(function(data) {
console.log(data)
//try to pull a service that's protected by passport
})
.fail(function(data) {
console.log(data)
})
Which is not working at all when it's successful. Its giving the 500 error I set for when someone isn't logged in.
Any particular direction I should be going in to manage authentication with passportjs in backbone?
The 500 error means some part of the code in the server is not working properly.
You can send the logged in user from express using passport. You can follow the following example.
var app = express();
var login = require('./routes/login');
app.post('/login',
passport.authenticate('local', { successRedirect: '/',
failureRedirect: '/login',
failureFlash: true }),
login.login);
where your login.js file may look like this
exports.login = function (req, res) {
res.json(req.user);
}
the authenticate process of passport populates user variable in request (req) with the logged in user.
Please note, you have to use cookie parser and session of express to make the passport session working. e.g.,
app.use(express.cookieParser());
app.use(express.session({ secret: 'keyboard cat' }));
your local authentication may look like the following (say you have a function that finds user by username (findByUsername)).
passport.use(new LocalStrategy({
usernameField: 'username',
passwordField: 'password'
},
function(username, password, done) {
// asynchronous verification, for effect...
process.nextTick(function () {
// Find the user by username. If there is no user with the given
// username, or the password is not correct, set the user to `false` to
// indicate failure and set a flash message. Otherwise, return the
// authenticated `user`.
findByUsername(username, function(err, user) {
if (err) { return done(err); }
if (!user) { return done(null, false, { message: 'Unknown user ' + username }); }
if (user.password != password) { return done(null, false, { message: 'Invalid password' }); }
return done(null, user);
})
});
}
));

Resources