简体   繁体   中英

CORs Error: Google Oauth from React to Express (PassportJs validation)

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.

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
});

I am having the same issue raised here, but am unable to get the solution offered by @Yazmin to work.

I am attempting to create a React, Express/Nodejs, MongoDB stack with Google authentication and authorization. I am currently developing the stack on Windows 10, using Vs Code (React on 'localhost:3000, Nodejs on localhost:5000 and MongoDB on localhost:27017.

The app's purpose is to display Urban Sketches(images) on a map using google maps, google photos api and google Gmail api. I may in the future also require similar access to Facebook Groups to access Urban Sketches. But for now I have only included the profile and Email scopes for authorization.

I want to keep all requests for third party resources in the backend, as architecturally I understand this is best practice.

The google authorisation issued from origin http://localhost:5000 works just fine and returns the expected results. However, when I attempt to do the same from the client - origin Http://localhost:3000 the following error is returned in the developers tools console following the first attempt to access the google auth2 api.

Access to fetch at 'https://accounts.google.com/o/oauth2/v2/auth?response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2Fauth%2Fgoogle%2Fcallback&scope=profile%20email%20https%3A%2F%2Fmail.google.com%2F&client_id=' (redirected from 'http://localhost:3000/auth/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.

No matter what I try the error message is the same.

I think that google is sending the reply to the client (localhost:3000) rather than to the server.

I understand the error is raised by the browser when it receives a response where the Scheme, Domain and port do not match with what it is expecting, and that it is not a problem in production as the client code is loaded from the app server.

Among other solutions, I attempted to implement Yilmaz's solution by Quote: “Create setupProxy.js file in client/src. No need to import this anywhere. create-react-app will look for this directory” I had already created my client by running create-react-app previously. So I added setupProxy.js inside my src folder.

Question: I assume I am correct that the new setupProxy.ts file containing my settings will be included by webpack after I restart the client.

It seems to me that the flow I am getting is not BROWSER ==> EXPRESS ==> GOOGLE-SERVER but BROWSER ==> EXPRESS ==> GOOGLE-SERVER ==>BROWSER where it stops with the cors error as shown above.

To test this theory, I put some console log messages in the client\\node_modules\\http-proxy-middleware\\lib\\index.js functions "shouldProxy" and "middleware", but could not detect any activity from the auth/google end point from the google authorization server response ( https://accounts.google.com/o/oauth2/v2/auth ).

So my theory is wrong and I don't know how I will get this working.

Console log messages displayed on VsCode terminal following request to /auth/google endpoint from the React client are as follows...

http-proxy-middleware - 92 HttpProxyMiddleware - shouldProxy
  context [Function: context]
  req.url /auth/google
  req.originalUrl /auth/google
  Trace
      at shouldProxy (C:\Users\User\github\GiveMeHopev2\client\node_modules\http-proxy-middleware\lib\index.js:96:13)
      at middleware (C:\Users\User\github\GiveMeHopev2\client\node_modules\http-proxy-middleware\lib\index.js:49:9)
      at handle (C:\Users\User\github\GiveMeHopev2\client\node_modules\webpack-dev-server\lib\Server.js:322:18)
      at Layer.handle [as handle_request] (C:\Users\User\github\GiveMeHopev2\client\node_modules\express\lib\router\layer.js:95:5)
      at trim_prefix (C:\Users\User\github\GiveMeHopev2\client\node_modules\express\lib\router\index.js:317:13)
      at C:\Users\User\github\GiveMeHopev2\client\node_modules\express\lib\router\index.js:284:7
      at Function.process_params (C:\Users\User\github\GiveMeHopev2\client\node_modules\express\lib\router\index.js:335:12)
      at next (C:\Users\User\github\GiveMeHopev2\client\node_modules\express\lib\router\index.js:275:10)
      at goNext (C:\Users\User\github\GiveMeHopev2\client\node_modules\webpack-dev-middleware\lib\middleware.js:28:16)
      at processRequest (C:\Users\User\github\GiveMeHopev2\client\node_modules\webpack-dev-middleware\lib\middleware.js:92:26)
http-proxy-middleware - 15 HttpProxyMiddleware - prepareProxyRequest
req localhost

This is a listing of my nodejs server code. I have left in commented out code for some of the different attempts I have made.

import express from 'express'
import mongoose from 'mongoose'
//import cors from 'cors'
import dotenv from 'dotenv'
import cors, { CorsOptions } from 'cors'
import { exit } from 'process';
//imports index 02
import passport from 'passport'

import session from 'express-session' 
import MongoStore from 'connect-mongo'
import morgan from 'morgan' 
//Routes
import GiveMeHopeRouter from './routes/giveMeHope'
//import AuthRouter from './routes/auth'
import process from 'process';

//index 02
 
dotenv.config();
// express
const app = express();
// passport config
require ('./config/passport')(passport)

/*
// CORs
// !todo Access-Control-Allow-Credentials will not accept a boolean true in typescript
// ! hoping a string containing something is in fact true - just a guess. 
app.use('*', (req, res, next) => {
    res.header("Access-Control-Allow-Origin", "http://localhose:3000")
    res.header("Access-Control-Allow-Headers", "X-Requested-with")
    res.header("Access-Control-Allow-Headers", "Content-Type")
    res.header("Access-Control-Allow-Credentials", 'true' )
    next()
})
app.options('*' as any,cors() as any) ;

*/

// logging
if( process.env.NODE_ENV! !== 'production') {
    app.use(morgan('dev'))
}

const conn = process.env.MONGODB_LOCAL_URL!
/**
 * dbConnection and http port initialisation
 */

const dbConnnect = async (conn: string, port: number) => {

    try {
        let connected = false;
        await mongoose.connect(conn, { useNewUrlParser: true, useUnifiedTopology: true })
        app.listen(port, () => console.log(`listening on port ${port}`))
        return connected;
    } catch (error) {
        console.log(error)
        exit(1)
    }
}

const port = process.env.SERVERPORT as unknown as number
dbConnnect(conn, port)
//index 02
// Pre Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }))


const mongoStoreOptions = {
    mongoUrl: conn,
    collectionName: 'sessions'
}

app.use(
    session({
        secret: process.env.SESSIONKEY as string,
        resave: false,
        saveUninitialized: false,
        store: MongoStore.create(mongoStoreOptions),
    })
)

app.use(passport.initialize())
app.use(passport.session())

app.get('/', (req, res) => {
    res.send('Hello World');
})
// Authentication and Authorisation

const emailScope: string = process.env.GOOGLE_EMAIL_SCOPE as string
//GOOGLE_EMAIL_SCOPE=https://www.googleapis.com/auth/gmail/gmail.compose

const scopes = [
    'profile',
    'email',
    emailScope

].join(" ")




// Authenticate user
// !Start of CORs Testing options
/**
 * Cors Test 1 default
 
app.use(cors())
*/
/**
 * CORs Test 2 - Restrict to Origin
 */ 
const googleoptions2 = {
    origin: ['http://localhost:3000', 'https://accounts.google.com'],
    credentials: true,
}
app.use(cors(googleoptions2))

app.get('/auth/google', passport.authenticate('google', {
    scope: [
        'https://www.googleapis.com/auth/userinfo.profile',
        'https://www.googleapis.com/auth/userinfo.email'
    ]
}));



/**
 * CORs Test 3 - Restrict to Origin for multiple origins
 * failed - origin undefined
 

var whitelist = ['http://localhost:3000', 'https://accounts.google.com']
var googleoptions3 = {
  origin: function (origin: string, callback: (arg0: Error | null, arg1: boolean | undefined) => CorsOptions) {
      console.log('origin',origin)
    if (whitelist.indexOf(origin) !== -1) {
      callback(null, true)
    } else {
      callback(new Error('Not allowed by CORS'),undefined)!
    }
  }
}
*/



/**
 * Cors Test 4 with pre-request
 * Note: Tested with GoolgleStrategy Proxy set to true and false
 */
/*
const googleoptions4 = {
    origin: false,
    methods: ["GET"],
    credentials: true,
    maxAge: 3600
  };
app.options('/auth/google',cors(googleoptions4) as any)
*/

/**
 * Cors Test 5 set headers
 * Note: Tested with GoolgleStrategy Proxy set to true and false
 * 
 * see solution  https://stackoverflow.com/questions/59036377/cors-error-google-oauth-from-react-to-express-passportjs-validation
 * by https://stackoverflow.com/users/10262805/yilmaz
 * 
 * Create setupProxy.js file in client/src. No need to import this anywhere. create-react-app will look for this directory
 * ! Todo How to set up setupProxy.js in client folder src and then run create-react-app. create-react-app fails when trying to recreate client
 */
 

// Authentication callback
app.get('/auth/google/callback', passport.authenticate('google', { failureRedirect: '/'}),
    (req, res) => {
        res.redirect('/auth/google/log')
    }
)
// Refresh RefeshToken
app.get('/auth/google/refreshtoken', (req, res) => { 
    console.log(req.user);
    res.send('refreshToken')
})

/**
* Logout
* !!! With passport, req will have a logout method 
*/

app.get('/auth/login', (req, res) => {
  
    
   res.redirect('auth/google')
  
})

/**
 * Logout
 * !!! With passport, req will have a logout method 
 */

app.get('/logout', (req, res) => {
    req.logout()
    res.redirect('/')
})

app.get('/auth/google/log', (req,res) => {
    console.log('/google/log', req)
    res.send('Google Login Successful ')
})

app.use('/givemehope', GiveMeHopeRouter)


My passport configuration:

import { PassportStatic} from 'passport';
import {format, addDays} from 'date-fns'
import { IUserDB, IUserWithRefreshToken, ProfileWithJson} from '../interfaces/clientServer'

 
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const User = require('../models/User')



module.exports   = function (passport:PassportStatic) {

    const clientID: string = process.env.GOOGLE_CLIENTID as string
    const clientSecret: string = process.env.GOOGLE_SECRET as string
    const callbackURL: string = process.env.GOOGLE_AUTH_CALLBACK as string

    const strategy = new GoogleStrategy(
        {
            clientID: clientID,
            clientSecret: clientSecret,
            callbackURL: callbackURL,
            proxy: true
            
        },
        async (_accesstoken: string, _refreshtoken: string,
            profile: ProfileWithJson,
            done: (arg0: null, arg1: any) => void) => {
            console.log('accesstoken' , _accesstoken)
            console.log(',_refreshtoken', _refreshtoken)
              
            const newUser: IUserWithRefreshToken = {
                googleId: profile.id,
                displayName: profile.displayName,
                firstName: profile._json.given_name,
                lastName: profile._json.family_name,
                image: profile._json.picture ,
                email: profile._json.email,
                refreshToken: {
                    value: _refreshtoken || process.env.GOOGLE_REFRESH_TOKEN_INIT!,
                    refreshed: format(new Date(), 'yyyy-MM-dd'),
                    expires: format(addDays(new Date(), 5), 'yyyy-MM-dd') ,
                }
            }
            try {
                let user: IUserDB = await User.findOne({ googleId: profile.id })
                 
                if (user) {
                    // Update refresh token if it is has changed.
                    if (_refreshtoken) {
                        user = await User.update({refreshToken: newUser.refreshToken})
                        done(null, user)
                    }
                    done(null, user)
                } else {
                    user = await User.create(newUser)
                    done(null, user)
                }
                
            } catch (err) {
                console.log('******45 Passport.ts - error thrown ', err)
                done(null,err)
            }
            
        }
    )

    passport.use(strategy)
            
    passport.serializeUser(async (user: any, done) => done(null, user._id))
    passport.deserializeUser(async (_id, done) => done(null, await User.findOne({_id})))
     
}

SetupProxy.ts file in the client src folder:

import {proxy} from 'http-proxy-middleware'
module.exports = function(app: { use: (arg0: any) => void }) {
     
    
    app.use(proxy("auth/google",{target: "http://localhost:5000"} ))
    app.use(proxy("givemehope/**",{target: "http://localhost:5000"} ))
    app.use(proxy("/o/oauth2/v2/",{target: "http://localhost:5000"} ))
    
}

And finally my fetch reqyest:

 async function http(request: RequestInfo): Promise<any> {
    try {
      const response = await fetch(request,{credentials: 'include'}) 
      const body = await response.json();
      return body
    } catch (err) { console.log(`Err SignInGoogle`) }
  };



Here's my solution... Code is slightly different but you will get the concept.

// step 1:
// onClick handler function of the button should use window.open instead 
// of axios or fetch
const loginHandler = () => window.open("http://[server:port]/auth/google", "_self")

//step 2: 
// on the server's redirect route add this successRedirect object with correct url. 
// Remember! it's your clients root url!!! 
router.get(
    '/google/redirect', 
    passport.authenticate('google',{
        successRedirect: "[your CLIENT root url/ example: http://localhost:3000]"
    })
)

// step 3:
// create a new server route that will send back the user info when called after the authentication 
// is completed. you can use a custom authenticate middleware to make sure that user has indeed 
// been authenticated
router.get('/getUser',authenticated, (req, res)=> res.send(req.user))

// here is an example of a custom authenticate express middleware 
const authenticated = (req,res,next)=>{
    const customError = new Error('you are not logged in');
    customError.statusCode = 401;
    (!req.user) ? next(customError) : next()
}
// step 4: 
// on your client's app.js component make the axios or fetch call to get the user from the 
// route that you have just created. This bit could be done many different ways... your call.
const [user, setUser] = useState()
useEffect(() => {
    axios.get('http://[server:port]/getUser',{withCredentials : true})
    .then(response => response.data && setUser(response.data) )
},[])


will load your servers auth url on your browser and make the auth request.将在您的浏览器上加载您的服务器身份验证 URL 并发出身份验证请求。
then reload the client url on the browser when the authentication is complete.然后在身份验证完成后在浏览器上重新加载客户端 url。
makes an api endpoint available to collect user info to update the react state使 api 端点可用于收集用户信息以更新反应状态
makes a call to the endpoint, fetches data and updates the users state.用端点,获取数据并更新用户状态。

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM