I have a MERN stack app which uses Passport.js for Facebook/Twitter/Google authentication. In the development environment, everything works well and we are able to authenticate the user and log them into the application.
After confirming the functionality in the development environment, we deployed to Heroku, but the same functionality is not working in production, and Passport.js is failing with a “TokenError” stating that the Domain URL is not included in the app’s domains (even though we confirmed it is listed in the app’s domains).
The code is listed below along with screenshots confirming that the URL is in the app’s domains. You can also see the live behavior of the callback being successfully called from Facebook in this recording – https://thiag0.tinytake.com/tt/MzcxMjIyNV8xMTI5MTA3MA
Any help pointing me in the right direction is greatly appreciated!
Server.js
const express = require('express');
const connectDB = require('./config/db');
const configurePassport = require('./config/passport');
const path = require('path');
const fs = require('fs');
const https = require('https');
const app = express();
// Connect Database
connectDB();
// Init Middleware
app.use(express.json({ extended: false }));
// Passport Middleware
configurePassport(app);
// Define Routes
app.use('/api/users', require('./routes/api/users'));
app.use('/api/auth', require('./routes/api/auth'));
app.use('/api/profile', require('./routes/api/profile'));
app.use('/api/posts', require('./routes/api/posts'));
// Serve static assets in production
if (process.env.NODE_ENV === 'production') {
// Set static folder
app.use(express.static('client/build'));
app.get('*', (req, res) => {
res.sendFile(path.resolve(__dirname, 'client', 'build', 'index.html'));
});
}
const PORT = process.env.PORT || 5000;
if (process.env.NODE_ENV === 'production') {
app.listen(PORT, () => console.log(`Server started on port ${PORT}`));
} else {
https
.createServer(
{
key: fs.readFileSync('config/certificate/server.key'),
cert: fs.readFileSync('config/certificate/server.cert')
},
app
)
.listen(PORT, () => console.log(`Server started on port ${PORT}`));
}
Passport.js
const passport = require('passport');
const config = require('config');
const session = require('express-session');
const jwt = require('jsonwebtoken');
const gravatar = require('gravatar');
const bcrypt = require('bcryptjs');
const uuid = require('uuid');
const FacebookStrategy = require('passport-facebook').Strategy;
const TwitterStrategy = require('passport-twitter').Strategy;
const GoogleStrategy = require('passport-google-oauth2').Strategy;
const User = require('../models/User');
const webHost = config.get('webHost');
passport.serializeUser(function(user, done) {
done(null, user.id);
});
passport.deserializeUser(function(id, done) {
User.findById(id, function(err, user) {
done(err, user);
});
});
const configurePassport = app => {
app.use(
session({
secret: config.get('jwtSecret'),
resave: false,
saveUninitialized: true
})
);
app.use(passport.initialize());
app.use(passport.session());
passport.use(
new FacebookStrategy(
{
clientID: config.get('facebook_app_id'),
clientSecret: config.get('facebook_app_secret'),
callbackURL: config.get('facebook_callback_url'),
profileFields: ['email', 'name']
},
async (accessToken, refreshToken, profile, done) => {
const { email, last_name, first_name, id } = profile._json;
let user = await User.findOne({ facebook_id: id });
if (!user) {
// Create user if they don't exist
user = new User({
...
});
await user.save();
}
done(null, user);
}
)
);
passport.use(
new TwitterStrategy(
{
consumerKey: config.get('twitter_consumer_key'),
consumerSecret: config.get('twitter_consumer_secret'),
callbackURL: config.get('twitter_callback_url'),
userProfileURL:
'https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true'
},
async function(token, tokenSecret, profile, done) {
const { email, name, id } = profile._json;
let user = await User.findOne({ twitter_id: id });
if (!user) {
// Create user if they don't exist
user = new User({
...
});
await user.save();
}
done(null, user);
}
)
);
// Use the GoogleStrategy within Passport.
// Strategies in Passport require a `verify` function, which accept
// credentials (in this case, an accessToken, refreshToken, and Google
// profile), and invoke a callback with a user object.
passport.use(
new GoogleStrategy(
{
clientID: config.get('google_app_id'),
clientSecret: config.get('google_app_secret'),
callbackURL: config.get('google_callback_url')
},
async function(accessToken, refreshToken, profile, done) {
const { email, name, sub } = profile._json;
let user = await User.findOne({ google_id: sub });
if (!user) {
// Create user if they don't exist
user = new User({
...
});
await user.save();
}
done(null, user);
}
)
);
app.get(
'/auth/facebook/callback',
passport.authenticate('facebook', {
session: false,
failureRedirect: webHost + '/login'
}),
function(req, res) {
const payload = {
user: {
id: req.user._id
}
};
jwt.sign(
payload,
config.get('jwtSecret'),
{ expiresIn: 36000 },
(err, token) => {
if (err) throw err;
return res.redirect(webHost + '/social/facebook/' + token);
}
);
}
);
app.get(
'/auth/facebook',
passport.authenticate('facebook', {
session: false,
scope: ['email', 'user_posts']
})
);
// Redirect the user to Twitter for authentication. When complete, Twitter
// will redirect the user back to the application at /auth/twitter/callback
app.get(
'/auth/twitter',
passport.authenticate('twitter', {
session: false
})
);
// Twitter will redirect the user to this URL after approval. Finish the
// authentication process by attempting to obtain an access token. If
// access was granted, the user will be logged in. Otherwise, authentication has failed.
app.get(
'/auth/twitter/callback',
passport.authenticate('twitter', {
session: false,
failureRedirect: webHost + '/login'
}),
function(req, res) {
const payload = {
user: {
id: req.user._id
}
};
jwt.sign(
payload,
config.get('jwtSecret'),
{ expiresIn: 36000 },
(err, token) => {
if (err) throw err;
return res.redirect(webHost + '/social/twitter/' + token);
}
);
}
);
// GET /auth/google
app.get(
'/auth/google',
passport.authenticate('google', {
session: false,
scope: [
'https://www.googleapis.com/auth/plus.login',
'https://www.googleapis.com/auth/userinfo.email'
]
})
);
// GET /auth/google/callback
app.get(
'/auth/google/callback',
passport.authenticate('google', {
session: false,
failureRedirect: webHost + '/login'
}),
function(req, res) {
const payload = {
user: {
id: req.user._id
}
};
jwt.sign(
payload,
config.get('jwtSecret'),
{ expiresIn: 36000 },
(err, token) => {
if (err) throw err;
return res.redirect(webHost + '/social/google/' + token);
}
);
}
);
};
module.exports = configurePassport;
Heroku Logs
2019-08-20T20:04:52.264433+00:00 app[web.1]: FacebookTokenError: Can't Load URL: The domain of this URL isn't included in the app's domains. To be able to load this URL, add all domains and subdomains of your app to the App Domains field in your app settings.
2019-08-20T20:04:52.264453+00:00 app[web.1]: at Strategy.parseErrorResponse (/app/node_modules/passport-facebook/lib/strategy.js:198:12)
2019-08-20T20:04:52.264456+00:00 app[web.1]: at Strategy.OAuth2Strategy._createOAuthError (/app/node_modules/passport-oauth2/lib/strategy.js:405:16)
2019-08-20T20:04:52.264460+00:00 app[web.1]: at /app/node_modules/passport-oauth2/lib/strategy.js:175:45
2019-08-20T20:04:52.264462+00:00 app[web.1]: at /app/node_modules/oauth/lib/oauth2.js:191:18
2019-08-20T20:04:52.264465+00:00 app[web.1]: at passBackControl (/app/node_modules/oauth/lib/oauth2.js:132:9)
2019-08-20T20:04:52.264466+00:00 app[web.1]: at IncomingMessage.<anonymous> (/app/node_modules/oauth/lib/oauth2.js:157:7)
2019-08-20T20:04:52.264469+00:00 app[web.1]: at IncomingMessage.emit (events.js:203:15)
2019-08-20T20:04:52.264470+00:00 app[web.1]: at endReadableNT (_stream_readable.js:1145:12)
2019-08-20T20:04:52.264472+00:00 app[web.1]: at process._tickCallback (internal/process/next_tick.js:63:19)
2
Answers
use passport-twitter-token instead of passport-twitter library that contains a strategy for Twitter authentication, but this library is not suitable for RESTful API. It is better suited for Express.js applications which are used with some server rendering
The code for initialization of Passport with Twitter strategy looks like this:
I used the solution here https://www.soccermass.com you can read more on the solution here https://medium.com/@robince885/how-to-do-twitter-authentication-with-react-and-restful-api-e525f30c62bb
Try setting
sameSite:"none"
inapp.use(session())
. This is what worked for me.