3

I'm trying to set up a React/Redux - NodeJs Express stack with Google OAuth authentication. My issue is a CORs error kicking back in the console. I've found some Stack Overflow questions that I feel were exactly my issue, but the solutions aren't producing any results. Specifically these two: CORS with google oauth and CORS/CORB issue with React/Node/Express and google OAuth.

So I've tried a variety of fixes that all seem to lead me back to the same error. Here's the most straight forward of them:

const corsOptions = {
    origin: 'http://localhost:3000',
    optionsSuccessStatus: 200,
    credentials: true
}
app.use(cors(corsOptions));

This is in the root of my API.js file. The console error I receive state:

Access to XMLHttpRequest at 'https://accounts.google.com/o/oauth2/v2/auth?response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2Fapi%2Foauth%2Fgoogle%2Freturn&scope=profile&client_id=PRIVATE_CLIENT_ID.apps.googleusercontent.com' (redirected from 'http://localhost:5000/api/oauth/google') from origin 'null' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

So if I look at my network log in the dev tools, I look at my request to the API path and see what I expect to see:

Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: content-type
Access-Control-Allow-Methods: GET,HEAD,PUT,PATCH,POST,DELETE
Access-Control-Allow-Origin: http://localhost:3000

So it seems to me that my issue isn't within my front to back communication. Which leads me to believe it's maybe an issue with the Passport token validation. Here are my simplified routes:

router.post('/oauth/google', passport.authenticate('googleVerification', {
    scope: ['profile']
}), (req, res) => {
    console.log('Passport has verified the oauth token...');
    res.status(200)
});

And the callback route:

router.get('/oauth/google/return', (req, res) => {
    console.log('google oauth return has been reached...')
    res.status(200)
});

And lastly, the simplified strategy:

passport.use('googleVerification', new GoogleStrategy({
    clientID: process.env.OAUTH_CLIENT_ID,
    clientSecret: process.env.OAUTH_SECRET,
    callbackURL: 'http://localhost:5000/api/oauth/google/return'
}, (accessToken, refreshToken, profile, cb) => {
    console.log('Passport OAuth Strategy reached');
    cb(null, profile)
}));

I know all these won't lead to anything functional, but I've just ripped out as much fluff as I can trying to get a handle on where the block in my authentication flow is. Just in case it may be helpful in narrowing this down, here is the action creator in Redux that logs the last step in the process before the errors start coming ('redux accepting token and passing to API:', token):

export const signIn = (token) => {
    console.log('redux accepting token and passing to API:', token)
    return async dispatch => {
        const res = await Axios({
            method: 'post',
            url: `${API_ROOT}/api/oauth/google`,
            withCredentials: true,
            data: {
                access_token: token
            }
        })

        console.log('API has returned a response to redux:', res)

        dispatch({
            type: SIGN_IN,
            payload: res
        })
    }
};

This never actually reaches the return and does not log the second console.log for the record.

Yilmaz
  • 35,338
  • 10
  • 157
  • 202
Dale Spiteri
  • 767
  • 1
  • 9
  • 21
  • The error message cited in the question shows that your frontend JavaScript code is making a request to the `https://accounts.google.com/o/oauth2/v2/auth` endpoint, and getting a response back directly from that endpoint. But because that endpoint intentionally doesn’t include the Access-Control-Allow-Origin response header, your browser blocks your frontend code from accessing the response. That endpoint isn’t meant to called from code. Instead, users are meant to be navigated to there and then redirected back to your app. See https://stackoverflow.com/a/43276710 etc. – sideshowbarker Nov 26 '19 at 01:48
  • Based on the code snippets in the question, I guess maybe you’re trying to proxy the request through your backend. But if so, that’s not what’s happening. Instead, it seems like the request from your frontend code is just getting redirected (not proxied) to the `https://accounts.google.com/o/oauth2/v2/auth` URL. Regardless, the only thing that should ever by happening with that URL is that users are navigated to it, login manually, and then it redirects them back to your app. – sideshowbarker Nov 26 '19 at 01:51
  • @sideshowbarker Thank you so much for the response. This is what I suspected was the issue, but despite days of searching, I cannot find a clear explanation of how the flow is supposed to work. It seems to me that you would have to proxy through a server if you want to use oauth for verification and protected routes. I use react-google-login package in React. It sends the user to login, and returns a user object (including the token). I'm sending that token to my server to verify it against my client secret using Passport. – Dale Spiteri Nov 27 '19 at 00:44
  • @sideshowbarker If I remove Passport from protecting the `/oauth/google` route, that route receives a request with the a body containing the user's access token with no errors. So I'm forced to assume Passport is making the request to `https://accounts.google.com/o/oauth2/v2/auth` that is causing the error. For the life of me, I cannot find a straight forward working example of this flow. This baffles me since React, Node, Express, Passport all seem to be industry standards. – Dale Spiteri Nov 27 '19 at 00:50

1 Answers1

8

That CORS is not related to making request to google because when you registered your app in console.developers.google.com it is already handled by google.

The issue is between CRA developer server and express api server. You are making request from localhost:3000 to localhost:5000. To fix this use proxy.

In the client side directory:

npm i http-proxy-middleware --save

Create setupProxy.js file in client/src. No need to import this anywhere. create-react-app will look for this directory

Add your proxies to this file:

module.exports = function(app) {
    app.use(proxy("/auth/google", { target: "http://localhost:5000" }));
    app.use(proxy("/api/**", { target: "http://localhost:5000" }));
};

We are saying that make a proxy and if anyone tries to visit the route /api or /auth/google on our react server, automatically forward the request on to localhost:5000.

Here is a link for more details:

https://create-react-app.dev/docs/proxying-api-requests-in-development/

by default password.js does not allow proxied requests.

passport.use('googleVerification', new GoogleStrategy({
    clientID: process.env.OAUTH_CLIENT_ID,
    clientSecret: process.env.OAUTH_SECRET,
    callbackURL: 'http://localhost:5000/api/oauth/google/return',
    proxy:true
}

One important thing here is, you should understand why proxy is used. As far as I understood from your code, from browser, you make request to express, and express will handle the authentication with password.js. After password.js runs through all authentication steps, it will create a cookie, stuffed it with the id, give it to express and express will send it to the browser. this is your app structure:

 BROWSER ==> EXPRESS ==> GOOGLE-SERVER

Browsers automatically attaches the cookie to the evey request to server which issued the cookie. So browser knows which cookie belongs to which server, so when they make a new request to that server they attach it. But in your app structure, browser is not talking to GOOGLE-SERVER. If you did not use proxy, you would get the cookie from GOOGLE-SERVER through express, but since you are not making request to GOOGLE-SERVER, cookie would not be used, it wont be automatically attached. that is the point of using cookies, browsers automatically attaches the cookie. BY setting up proxy, now browser is not aware of GOOGLE-SERVER. as far as it knows, it is making request to express server. so every time browser make request to express with the same port, it attaches the cookie. i hope this part is clear.

Now react is communicating only with express-server.

  BROWSER ==> EXPRESS

since react and exress are not on the same port, you would get cors error.

there are 2 solutions. 1 is using the cors package.

its setup is very easy

var express = require('express')
var cors = require('cors')
var app = express()
 
app.use(cors()) // use this before route handlers

second solution is manually setting up a middleware before route handlers

app.use((req, res, next) => {
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader(
    "Access-Control-Allow-Methods",
    "OPTIONS, GET, POST, PUT, PATCH, DELETE"
  );
  res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
  next(); // dont forget this
});
Yilmaz
  • 35,338
  • 10
  • 157
  • 202
  • What if my backend app is running completely seperately from my frontend app? Say my frontend is running on Firebase, and my backend on Heroku, will the proxy still work? Thanks in advance! – George Sep 08 '20 at 14:42
  • @Yilmaz I've tried using this solution without any success. I have made a post on SO about it here ( https://stackoverflow.com/questions/65033944/google-oauth-redirection-blocked-by-cors-in-cra) however the proxy doesn't seem to do the trick in react – asus Dec 02 '20 at 18:37
  • @asus I updated the question in detail. i hope this solves the issue – Yilmaz Dec 02 '20 at 19:51
  • for your second solution, where would this go? in the main entry point for an express app or in a controller? – asus Dec 02 '20 at 20:10
  • @asus in the main app. second solution is a regular middleware – Yilmaz Dec 02 '20 at 20:12
  • @Yilmaz I've tried both solutions but I still the CORS error in the browser. The way I am doing it now is sending a GET using fetch to `localhost:3001/google/login` using a client called `ky` (similar to fetch) with no body. I've set `'Access-Control-Allow-Credentials` to true in the header for ky. The **only** way I have found around this is to open the google oAuth window using `window.open('localhost:3001/google/login'` but this seems wrong.. or is it just me? – asus Dec 02 '20 at 21:53
  • @asus what error are u getting. are u sure you are fetching from a right endpoint – Yilmaz Dec 02 '20 at 22:25
  • @Yilmaz I get two errors. `Access to fetch at 'http://localhost:3001/google/login' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. `and `failed to fetch` as the second error. Opening `http://localhost:3001/google/login` works and shows the google accounts I can sign into – asus Dec 02 '20 at 22:29
  • @Yilmaz I am actually trying a combination of your solution 1 and 2 from the above and putting `http://localhost:3000` in place of `*` for the header but that still renders the same error – asus Dec 02 '20 at 23:02
  • @asus make sure you use the middlewares above the handlers. use it right after you defined "const app=express()". this is a simple cors error. for the second error, dont use wildcard then. "app.use(proxy("/api/NoWildCard, { target: "http://localhost:5000" }));" – Yilmaz Dec 02 '20 at 23:15
  • @Yilmaz https://stackoverflow.com/a/63736799/8518207 a similar solution to this worked for me using `window` in the client side. couldn't get it to work without it but I appreciative your help – asus Dec 03 '20 at 07:38