Search code examples
node.jsreactjsexpresspassport.jspassport-google-oauth

MERN OAuth Implementation does not redirect to the google login page


I'm currently running a webserver using the MERN stack, and I'm trying to get OAuth login working properly. However, when I click the "login with google" button, react loads the homepage (but the URL changes). Fetching the URL directly gets a 302 response from the server, but my front-end doesn't change.

Server.js

const express = require('express');
const path = require('path');
const mongoose = require('mongoose');
const bodyParser = require('body-parser');
const logger = require('morgan');
const cors = require('cors');
const secure = require('express-force-https');
const passport = require('passport');
const cookieSession = require('cookie-session');

require('dotenv').config();

const app = express();
const port = process.env.PORT || 5000;
const dbRoute = process.env.MONGODB_URI || 'NO DB ROUTE PROVIDED';

// db setup
mongoose.connect(
  dbRoute,
  {
    useNewUrlParser: true,
    useUnifiedTopology: true,
    dbName: process.env.DATABASE_NAME,
  }
);

let db = mongoose.connection;
db.once('open', () => console.log("Connected to the database"));
db.on('error', console.error.bind(console, "MongoDB connection error: "));

// middleware
app.use(cors());
app.use(bodyParser.urlencoded({ extended: false })); // body parsing
app.use(bodyParser.json());
app.use(logger("dev"));
app.use(express.static(path.join(__dirname, "client", "build"))); // for serving up the clientside code
app.use(secure); // ensure that the connection is using https
app.use(cookieSession({ // cookies!
  maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
  keys:['vcxzkjvasddkvaosd'] // yeah i'm sure that's secure enough
}));

// models
require('./models/rule');
require('./models/affix');
require('./models/user');

// passport security
require('./config/passport');
app.use(passport.initialize());
app.use(passport.session());

// routes
app.use(require('./routes'));

// The "catchall" handler: for any request that doesn't
// match one above, send back React's index.html file.
app.get('*', (req, res) => {
  res.sendFile(path.join(__dirname+'/client/build/index.html'));
});

app.listen(port);

console.log(`Server listening on ${port}`);

Route (There are a few index files in different folders, so the full path for this route it /api/user/google)

const mongoose = require('mongoose');
const passport = require('passport');
const router = require('express').Router();
const auth = require('../auth');
const User = mongoose.model('User');

router.get('/google', 
  passport.authenticate('google', {
    scope: ['profile', 'email']
  })
);

router.get('/google/callback', 
  passport.authenticate('google', { failureRedirect: '/affixes'}),
  (req, res) => {
    res.redirect('/?token=' + req.user.token);
  }
);

Passport.js

const mongoose = require('mongoose');
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
require('dotenv').config();

const User = mongoose.model('User');

passport.serializeUser(function(user, done) {
  done(null, user.id);
});

passport.deserializeUser(function(id, done) {
  User.findById(id).then((user) => {
    done(null, user);
  })
});

passport.use(new GoogleStrategy({
    clientID: process.env.OAUTH_CLIENT_ID,
    clientSecret: process.env.OAUTH_CLIENT_SECRET,
    callbackURL: '/api/user/google/callback',
    proxy: true
  },

  (accessToken, refreshToken, profile, done) => {
    User.findOne({ googleId: profile.id })
      .then((existingUser) => {
        if (existingUser) {
          done(null, existingUser);
        } else {
          new User({ googleId: profile.id }).save()
            .then((user) => done(null, user));
        }
      });
  }
));

Frontend login page (has a fetch button and a link button. As described above, different behavior)

import React from 'react';
import {
  ComingSoon
} from '../Common';
import {
  Button
} from '@material-ui/core';

const handleClick = () => {
  fetch('/api/user/google')
}

export default function Login() {
  return (
    <>
      <Button onClick={handleClick}>
        Login with Google
      </Button>

      <a href="/api/user/google"><button>Log in with Google</button></a>
    </>
  );
}

Update: Looks like some kind of CORS issue, although I still don't know how to fix it. Browser spits out

Access to fetch at '...' (redirected from 'http://localhost:3000/api/user/google') from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

Adding the requested header gives me

Access to fetch at '...' (redirected from 'http://localhost:3000/api/user/google') from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: Redirect is not allowed for a preflight request.

Solution

  • It turns out I was quite wrong about the nature of this issue! The problem was that my fetch requests to my OAuth endpoint were calling my frontend, not my backend because the request included text/html in its Accept header. Using the react advanced proxy setup to route to the proper URI fixed the issue.

    See: https://github.com/facebook/create-react-app/issues/5103 and https://github.com/facebook/create-react-app/issues/8550