21

I want to have a login button in my website so when a user clicks on it, the user can use their Google credentials. I'd like to ideally perform the authentication server side using Express.js and Passport.js.

I implemented authentication server-side but the problem is that I can't make an AJAX request from the website to the server to start authentication because Google or Oauth don't support CORS. So I need to use a href element in my website which would call the server authentication endpoint. However, I can't catch server response in this way.

If I perform the authentication client-side (I'm using React) I could store login state in Redux and allow the user to access the website's resources. However, when the user logs out I need to make sure that server endpoints stop serving the same user which feels like implementing authentication twice: client-side and server-side.

In addition when authenticating client-side, Google opens a popup for the user to authenticate which I think is worse user experience then just a redirect when authenticating server-side.

I'm wondering what the best practice in terms of authenticating using Oauth2/Google. For example, stackoverflow.com also has Google button but just makes a redirect, without any popup, so I guess they figured out a way to perform server-side authentication and to bypass CORS issue.

hitchhiker
  • 1,099
  • 5
  • 19
  • 44

3 Answers3

15

I faced the same issue. This article is Gold link

1.In auth route File I had following code

 const CLIENT_HOME_PAGE_URL = "http://localhost:3000";

  // GET  /auth/google
  // called to authenticate using Google-oauth2.0
  router.get('/google', passport.authenticate('google',{scope : ['email','profile']}));


  // GET  /auth/google/callback
  // Callback route (same as from google console)
   router.get(
    '/google/callback',  
    passport.authenticate("google", {
    successRedirect: CLIENT_HOME_PAGE_URL,
    failureRedirect: "/auth/login/failed"
   })); 

   // GET  /auth/google/callback
  // Rest Point for React to call for user object From google APi
   router.get('/login/success', (req,res)=>{
     if (req.user) {
         res.json({
          message : "User Authenticated",
         user : req.user
        })
     }
     else res.status(400).json({
       message : "User Not Authenticated",
      user : null
    })
  });

2.On React Side After when user click on button which call the above /auth/google api

  loginWithGoogle = (ev) => {
    ev.preventDefault();
    window.open("http://localhost:5000/auth/google", "_self");
  }

3.This will redirect to Google authentication screen and redirect to /auth/google/callback which again redirect to react app home page CLIENT_HOME_PAGE_URL

4.On home page call rest end point for user object

(async () => {
  const request = await fetch("http://localhost:5000/auth/login/success", {
   method: "GET",
   credentials: "include",
   headers: {
    Accept: "application/json",
     "Content-Type": "application/json",
    "Access-Control-Allow-Credentials": true,
  },
});

const res = await request.json();
   //In my case I stored user object in redux store
   if(request.status == 200){
     //Set User in Store
    store.dispatch({
      type: LOGIN_USER,
      payload : {
        user : res.user
     }
    });
  }

})();

5.last thing add cors package and following code in server.js/index.js in node module

// Cors
app.use(
  cors({
    origin: "http://localhost:3000", // allow to server to accept request from different origin
    methods: "GET,HEAD,PUT,PATCH,POST,DELETE",
    credentials: true // allow session cookie from browser to pass through
   })
);
It's EniGma
  • 426
  • 4
  • 6
11

Your authentication should be done server side. Here is how it works.

  1. You make a fetch or axios call to your authentication route.
  2. Your authentication route sends a request to Google's Authentication servers. This is important to have on the backend because you will need to provide your clientSecret. If you were to store this on the frontend, it would make it really easy for someone to find that value and compromise your website.
  3. Google authenticates the user and then sends you a set of tokens to your callback url to use for that user (refresh, auth, etc...). Then you would use the auth token for any additional authorization until it expires.
  4. Once that expires, you would use the refresh token to get a new authorization token for that client. That is a whole other process though.

Here is an example of what that looks like with Passport.js: https://github.com/jaredhanson/passport-google-oauth2

EDIT #1:

Here is an example with comments of the process in use with Facebook, which is the same OAuth codebase: https://github.com/passport/express-4.x-facebook-example/blob/master/server.js

Borduhh
  • 1,975
  • 2
  • 19
  • 33
  • 1
    As I mentioned in the OP you can't make an AJAX request (using `fetch` for example) because Oauth doesn't allow this as per this asnwer: https://stackoverflow.com/questions/46818363/how-to-handle-cors-in-a-service-account-call-to-google-oauth/46818486 – hitchhiker Mar 09 '19 at 16:30
  • 1
    The problem in that example is that they are trying to access the Google service directly from the frontend. You are instead accessing your own server from the frontend and then your server is handling the authentication for you. On your server, you would use Node's https module to make a call to Google servers, not `fetch`. – Borduhh Mar 09 '19 at 16:43
  • This is what I tried initially, making a request from the client to one of my server's routes which would authenticate with Google. But I had CORS issues which led me to believe that it's not possible to start the process from the client (I got this message from Chrome: Access to fetch at 'https://accounts.google.com/o/oauth2/v2/auth?response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fauth%2Fgoogle%2Fcallback&scope=email%20profile&client_id=XXXX' (redirected from 'http://localhost:3000/auth/login') – hitchhiker Mar 09 '19 at 16:51
  • from origin 'null' 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). When I removed the `fetch` call and added the server route to `a href` element CORS issue disappeared and authentication worked. Of course I made sure to add the route in Google dashboard and in my Node.js server CORS middleware. – hitchhiker Mar 09 '19 at 16:52
  • Can you share that code in your question? I will add some dummy code to my answer for explanation. – Borduhh Mar 09 '19 at 22:35
  • 1
    thanks for the example, but my problem isn't the server side code but the client-side code. In the link you gave: https://github.com/passport/express-4.x-facebook-example/blob/master/server.js the only code for client side is `Log In with Facebook` but my problem is that I use `fetch` to call server API endpoint where my CORS issue occurs – hitchhiker Mar 10 '19 at 05:20
  • I posted a slightly different but question here with code sample: https://stackoverflow.com/questions/55081056/why-fetching-a-server-api-which-triggers-oauth2-google-authentication-results-in – hitchhiker Mar 10 '19 at 05:20
  • As far as I see in the example links you provided it is assumed that the client and server run on the same domain and port, which is not the case in my case – hitchhiker Mar 10 '19 at 05:30
  • Check out this article. This is what you are looking for but with Facebook instead of Google. http://andrejgajdos.com/authenticating-users-in-single-page-applications-using-node-passport-react-and-redux/ – Borduhh Mar 11 '19 at 04:35
-5

Redux can really help with achieving this and this follows the same logic as Nick B already explained...

  1. You set up oauth on the server side and provide an endpoint that makes that call
  2. You set up the button on you react frontend and wire that through an action to the endpoint you already setup
  3. The endpoint supplies a token back which you can dispatch via a reducer to the central redux store.
  4. That token can now be used to set a user to authenticated

There you have it.

Jephtah
  • 1
  • 2