20
votes

This question is for anyone who is familiar with

  • Node.js
  • Express
  • Passport
  • JWT Authentication with passport (JSON Web Tokens)
  • Facebook OAuth2.0 OR Google OAuth2.0

I have been doing some online courses and understand how to do the two following things:

  1. Authentication using Passport Local Strategy + JWT Tokens
  2. Authentication using Passport Google/Facebook Strategy + Cookie/sessions.

I am trying to combine the content from these two courses basically. I want to use Google Strategy + JWT Authentication. I want to use JWT instead of cookies because my app is going to be a web/mobile/tablet app, and I need to be accessing the api from different domains.

There are two issues I am having with this: To kick off the Google/facebook OAuth pipelines, you need to call either '/auth/facebook' or '/auth/google'. Both Oauth flows work basically the same so when I say '/auth/google' from now on, I am referring to either. Now the issue I'm having is: On the client, do I call the '/auth/google' route with a href button link or an axios/ajax call? If I use the href or axios/ajax approach I am still getting problems with both solutions.

The href approach problem: When I assign an <a> tag with a href to '/auth/google' the authentication works perfectly fine. The user gets pushed through the Google Auth flow, they log in and the '/auth/google/callback' route gets called. The problem I have now is how do I correctly send the JWT token back to the client from '/auth/google/callback'?

After a lot of googling I have seen that people have simply passed the the JWT back to the client from the oauth callback in the redirect query param. For example:

res.redirect(301, `/dashboard?token=${tokenForUser(req.user)}`);

The issue I have with this is that now the the ability to authenticate is saved in my browser history! I could log out (destroying the token saved in localStorage), and then simply look at my browser url history, go back to the url that contains the token in the query param, and I would automatically log in again without having to go through the Google Strategy! This is a huge security flaw and is obviously the incorrect way to approach it.

The axios/ajax approach problem: Now before I explain the problem with this issue, I know for sure that If I get this working, it will solve all issues I was having with the previous href problem. If I manage to call '/google/auth' from an axios.get() call and receive the JWT in the response body, I will not be sending the token as url param, and it will not get saved in the browser history! Perfect right? well there is still some problems with this approach :(

When try to call axios.get('/auth/google') I get the following error:

enter image description here

How I've tried to solve the problem:

  • I installed cors to my npm server, and added app.use(cors()); to my index.js.
  • I took a stab and added "http://localhost:3000" to the "Authorised JavaScript origins" in Google developer console.

Neither of these solutions solved the issue, so now I really feel stuck. I want to use the axios/ajax approach, but I'm not sure how to get past this cors error.

Sorry for such a long message, but I really felt I had to give you all the information in order for you to properly help me.

Thanks again, looking forward to hear from you!

3

3 Answers

22
votes

I solved this in this way:

  1. On Front-End (can be mobile app) I made login request to Google (or Facebook) and after the user selected his account and logged in I got back response that contained google auth token and basic user info.
  2. Then I sent that google auth token to backend where my API sent one more request to the Google API to confirm that token. (See step 5)
  3. After successful request comes you get basic user info and e-mail. At this point, you can assume that user login via Google is good since google check returned that it's okay.
  4. Then you just signup or login user with that email and create that JWT token.
  5. Return token to your client and just use it for future requests.

I hope it helps. I implemented this multiple times and it showed like a good solution.

17
votes

Though there is good answer, I wanted to add more information with example.

  • Passport's google/facebook strategy is session based, it stores user info in cookie which is not advisable. So we need to disable it first

To disable session we need modify our redirect router. For example if we have redirect path /google/redirect like following, we need to pass { session: false } object as parameter.

router.get('/google/redirect', passport.authenticate('google', { session: false }), (req, res)=> {
    console.log(":::::::::: user in the redirect", req.user);
    //GENERATE JWT TOKEN USING USER
    res.send(TOKEN);
})

So where does this user come from? This user comes from passport's callback function. In the previous snippet we have added passport.authenticate(....) This middlewire initiates passport's google-strategy's callback which deals with the user. For example

passport.use(
    new GoogleStrategy({
        callbackURL: '/google/redirect',
        clientID: YOUR_GOOGLE_CLIENT_ID
        clientSecret: YOUR_GOOGLE_SECRET_KEY
    }, 
    (accessToken, refreshToken, profile, done)=>{
        console.log('passport callback function fired');

        // FETCH USER FROM DB, IF DOESN'T EXIST CREATE ONE

        done(null, user);

    })
)

That's it. We have successfully combined JWT and Google/Facebook Strategy.

-1
votes

The solution I found was to do the OAuth flow in a pop-up (window.open), that makes use of a pre-defined callback to pass the token to the front-end upon successful authentication.

Below are the relevant code samples, taken from this tutorial: https://www.sitepoint.com/spa-social-login-google-facebook/

Here is the pre-defined callback and initial open method, called from your front-end:

window.authenticateCallback = function(token) {
  accessToken = token;
};

window.open('/api/authentication/' + provider + '/start');

And here is what your OAuth Callback URL should return, upon successful authentication (which is the last step/page inside your pop-up):

<!-- src/public/authenticated.html -->
<!DOCTYPE html>
<html>
  <head>
    <title>Authenticated</title>
  </head>
  <body>
    Authenticated successfully.

    <script type="text/javascript">
      window.opener.authenticateCallback('{{token}}');
      window.close();
    </script>
  </body>
</html>

Your token would now be available to your front-end's pre-defined callback function, where you could easily save it in localStorage.

I suppose though, you could do the OAuth flow in the same window then (sans pop-up) and return an HTML page (similar to the above) that just saves the token and redirects the user to a dashboard immediately.

If your front-end domain was different from your api/auth server, however, you would probably need to redirect from your api/auth server to your front-end with a single-use, time-sensitive token (generated by your api/auth server), that your front-end could then use to call and receive (with axios) your actual token. This way you wouldn't have that browser history security problem.