skip to Main Content

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)

enter image description here
enter image description here
enter image description here
enter image description here

2

Answers


  1. 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:

    'use strict';
    
    var passport = require('passport'),
      TwitterTokenStrategy = require('passport-twitter-token'),
      User = require('mongoose').model('User');
    
    module.exports = function () {
    
      passport.use(new TwitterTokenStrategy({
          consumerKey: 'KEY',
          consumerSecret: 'SECRET',
          includeEmail: true
        },
        function (token, tokenSecret, profile, done) {
          User.upsertTwitterUser(token, tokenSecret, profile, 
    function(err, user) {
            return done(err, user);
          });
        }));
    
    };
    

    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

    Login or Signup to reply.
  2. Try setting sameSite:"none" in app.use(session()). This is what worked for me.

    app.use(
      session({
        secret: process.env.SESSION_SECRET,
        resave: false,
        saveUninitialized: false,
        cookie: {
          // this is age for the cookie in milliseconds
          maxAge: 30 * 24 * 60 * 60 * 1000,
    
          // this is the key for the cookie
          keys: [process.env.COOKIE_KEY],
    
          // An HttpOnly Cookie is a tag added to a browser cookie that prevents client-side scripts from accessing data.
          httpOnly: false,
    
          // Note: Standards related to the Cookie SameSite attribute recently changed such that:
          // The cookie-sending behavior if SameSite is not specified is SameSite=Lax. Previously the default was that cookies were sent for all requests.
          // Cookies with SameSite=None must now also specify the Secure attribute (they require a secure context/HTTPS).
          // Cookies from the same domain are no longer considered to be from the same site if sent using a different scheme (http: or https:).
          sameSite: "none",
        },
        expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
        store: sessionStore,
      })
    );
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search