auth0 / passport-linkedin-oauth2

Passport Strategy for LinkedIn OAuth 2.0
MIT License
119 stars 106 forks source link

NodeJS + Passport + Linkedin CORS issue #43

Open calbertts opened 8 years ago

calbertts commented 8 years ago

I've been trying to make this work from a local app, but I always got this error: XMLHttpRequest cannot load https://www.linkedin.com/uas/oauth2/authorization?response_type=code&redire…s%20r_basicprofile&state=GK1frP3WPaXU2b0JFjNFi1gz&client_id=78ufyyp89k3thk. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://127.0.0.1:3000' is therefore not allowed access.

Does anyone know why and how can I fix this issue? I mean, I know what CORS is about, but I don't understand why I'm seeing this here, I just have this node app:

let express = require('express');
let path = require('path');
let cors = require('cors');
let bodyParser = require('body-parser');
let cookieParser = require('cookie-parser');
let session = require('express-session');
let passport = require('passport');
let LinkedInStrategy = require('passport-linkedin-oauth2').Strategy;

let app = express();

app.use(cors());
app.use(bodyParser.json());
app.use(cookieParser());
app.use(bodyParser.urlencoded({
  extended: true
}));
app.use(session({
  secret: 'cyz4.1nc',
  resave: false,
  saveUninitialized: true,
}));
app.use(express.static(__dirname + '/../dist'));
app.use(passport.initialize());
app.use(passport.session());

passport.use(new LinkedInStrategy({
    clientID: 'CLIENT_ID,
    clientSecret: 'CLIENT_SECRET,
    callbackURL: "http://127.0.0.1:3000/auth/linkedin/callback",
    scope: ['r_emailaddress', 'r_basicprofile'],
    state: true
  },
  (token, tokenSecret, profile, done) => {
    console.log(token, tokenSecret, profile, done)
  }
));

app.get('/auth/linkedin', passport.authenticate('linkedin'), (req, res) => {
  console.log(err, 'not called')
})

app.get('/auth/linkedin/callback', 
  passport.authenticate('linkedin', { failureRedirect: '/login' }),
  (req, res) => {
    console.log('red')
    res.redirect('/home');
  });

app.get('/welcome', function (req, res) {
  res.sendFile(path.resolve(__dirname + '/../dist/index.html'));
});

app.listen(3000, () => {
  console.log('Example app listening on port 3000!');
});

Any idea? I'd really appreciate any kind of help.

Thanks!

pmespresso commented 7 years ago

Hmm, did you add your localhost url to the list of authorized redirect urls in the linkedin developer console?

Webdesignwill commented 7 years ago

Did this get resolved?

Albosonic commented 7 years ago

I have the same issue and my localhost is added to list of authorized redirect urls in the linkedin developer console.

pastorsj commented 7 years ago

I added this and it seemed to fix the issue

app.all('/*', function(req, res, next) {
    res.header("Access-Control-Allow-Origin", "*");
    next();
});
BastienScanu commented 7 years ago

Hello, I'm facing the same issue with my application. I tried to add the Access-Control-Allow-Origin header as @pastorsj did. Now I have a new error:

XMLHttpRequest cannot load http://localhost:3000/api/auth/linkedin. Redirect from 'http://localhost:3000/api/auth/linkedin' to 'https://www.linkedin.com/oauth/v2/authorization?response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Flinkedin%2Fcallback&scope=r_emailaddress%20r_basicprofile&state=8Q5q4G0eGSr5UUEFjeCKvHvt&client_id=77283cf7nmchg3' has been blocked by CORS policy: 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'. Origin 'http://localhost:4200' is therefore not allowed access. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.

So I tried to replace the wildcard '*' by 'http://localhost:4200' (the address on which my client app is served), and I'm back to the first error message:

XMLHttpRequest cannot load https://www.linkedin.com/oauth/v2/authorization?response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Flinkedin%2Fcallback&scope=r_emailaddress%20r_basicprofile&state=IEW35cSUiV08oUox8HTFgbXN&client_id=77283cf7nmchg3. Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'null' is therefore not allowed access. The response had HTTP status code 404.

Has anyone encountered the same issue ? Thanks

pastorsj commented 7 years ago

@BastienScanu My initial instinct is that you configured your callback url incorrectly. This most likely has nothing to do with the code that you added, however, I would keep the code since it may fix a problem down the line. The redirect uri is http://localhost:3000/api/auth/, which means that your application needs to be hosted on port 3000, not 4200. That callback url is configured after you create an application that requires access to the linkedin api. Note how it says Origin 'http://localhost:4200' is therefore not allowed access You could change the callback url to localhost:4200, which would probably fix the problem. See directions here: https://developer.linkedin.com/docs/oauth2#

BastienScanu commented 7 years ago

Thank you for your answer. My application is actually two applications. On the port 4200 is the client-side application, whose purpose is only to serve the front-end (written with angular2). This application is linked to a back-end API, which is a Node.js application, on port 3000. This is the back-end application that handles the authentication, that's why the callback is localhost:3000. So the front-end app (on port 4200) calls a function of the back-end app (on port 3000), and this function uses passport-linkedin-oauth2 to authenticate the user via linkedIn. This is a bit complex and this is probably the source of this error. Do you think I should use localhost:4200 for the redirect uri, and then call the back-end app from the front-end app ?

pastorsj commented 7 years ago

@BastienScanu To make this work, I had to run the Node.js application, written in Express, on the same port as my front end application. So I had to build my front end code and have the Node application serve that static html/js/css on its port. Here is a link to my project: https://github.com/pastorsj/intouch. It is written in React, but the idea is the same.

This is the line where the static html was served on my Node.js application.

ghost commented 6 years ago

@pastorsj I am facing the same problem. i have used the Middlewares in the same way and yes I cecked the github link as you did & the suggestion with app.all. Have you resolved it yet?

I am using the same strategy to use a auth middleware. i am getting the link which should ideally be the one that client should open up through Facebook but then with the link this error pops up in dev console

Failed to load https://www.facebook.com/dialog/oauth?response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fauth%2Ffacebook%2Fcallback&scope=email&client_id=1862357220760064: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:8080' is therefore not allowed access.

& my callback doesn't get called as well

Inside auth.js

const express = require('express');
const passport = require('passport');

const router = express.Router();

router.get('/facebook', passport.authenticate('facebook', { scope: 'email' }));
router.get('/facebook/callback', (req, res, next) => {
  console.log('entering here');
  passport.authenticate('facebook', (err, user) => {
    if (err) {
      console.log(err);
      return res.redirect('/');
    }
    if (!user) {
      console.log('user did not permit');
      return res.redirect('/');
    }

    req.login(user, (error) => {
      if (err) {
        console.log(err);
        return next(error);
      }
      console.log('all worked well');
      return res.redirect('/');
    });
    return null;
  })(req, res, next);
});

module.exports = router;

In my passport.js which I am importing right after initialising express with require('../authMiddleware/passport')(passport);

const FacebookStrategy = require('passport-facebook').Strategy;
const configAuth = require('./authConfig');

module.exports = (passport) => {
  passport.serializeUser((user, done) => {
    console.log(user);
    done(null, user.id);
  });

  passport.deserializeUser((id, done) => {
    console.log(id);
    done(null, id);
  });

  passport.use(new FacebookStrategy({
    clientID: configAuth.facebookAuth.clientID,
    clientSecret: configAuth.facebookAuth.clientSecret,
    callbackURL: configAuth.facebookAuth.callbackURL,
    passReqToCallback: true,
  }, (req, token, refreshToken, profile, done) => {
    process.nextTick(() => {
      if (!req.user) console.log(profile);
      return done(null, profile);
    });
  }));
};
pastorsj commented 6 years ago

@rahulbasu710 Can you post your app.js file? It should look something like this. I assume you added these lines to that file.

pastorsj commented 6 years ago

Also, take a look at the network requests being made on the chrome debugger. If you can find that request to the oauth route on facebook, you can probably determine whether you included the Access-Control-Allow-Origin header on that request.

ghost commented 6 years ago

Woosh, solved it. Instead of making a request with axios, the Facebook API need it as a a href because of some client and server distinction for request origin.

Thanks for your time @pastorsj

imtodor commented 6 years ago

experiencing the same issue as @rahulbasu710 and solve it like this...however, I don't like that approach so much :(

ghost commented 6 years ago

@imtodor yeah me too & the biggest problem being listening for response but I guess its more of an architecture & safety issue for auth from the auth providers

TimonSotiropoulos commented 6 years ago

Just wanted to elaborate a little bit on the above as it didn't make any sense to me at first to resolve the issue.

According to this stack overflow post the LinkedIn auth api doesn't respond with the cross origin request headers. This means that it is not possible to make an axios/fetch/superagent etc request from your front-end to your back-end to handle this call.

Instead you need to direct your browser to your back-end using an a tag or link which will then allow all the responses to correctly work (assuming you add the cross-origin stuff all mentioned above).

For example, I was running a separate React app using create-react-app running on http://localhost:3000 with a node backend on http://localhost:3001. I was attemping to make an axios call to my back end with a basic call like this fired from an onClick event on one of my front-end button components:

export const loginWithLinkedin = () => {
    const params = {
        method: "GET",
        headers: {
            "Content-Type": "application/json",
            "Access-Control-Allow-Origin": "http://localhost:3001",
            "Access-Control-Allow-Methods": "GET, POST, OPTIONS, PUT, PATCH, DELETE",
            "Access-Control-Allow-Headers": "X-Requested-With,content-type",
            "Access-Control-Allow-Credentials": true,
        },
        withCredentials: true,
        data: undefined
    }
    return axios('/api/account/linkedin', params)
    .then(respack.handleResponse)
    .catch(respack.handleError);
}

I had to change my Button component to not call this onClick function and instead wrapped it in an a tag with a href directly to my back-end

<a href="http://localhost:3001/api/account/linkedin">
    <Button
        type={'linkedin'}
        label={'Login with LinkedIn'}
        width={'100%'}
        height={50} />
</a>

My back end was then handling this with the basic way that was outlined in the OP of this post and everything seemed to work as expected again.

Hope this helps anyone else getting stuck in this situation!

amarjeet987 commented 6 years ago

This works perfectly @TimonSotiropoulos , but how do you get back the response in the front end. Response is sent to the browser in this case. It might be a stupid question though, I am just a beginner. Help me out.

chikeud commented 6 years ago

question

@amarjeet987 I'm currently having the same issue. Did you ever figure it out ?

im-andreeta66 commented 5 years ago

@chikeud I'm currently having the same issue. Did you ever figure it out ?

rambharlia007 commented 5 years ago

If your web app and API are running in different ports then for authentication using passport we can try this approach

victorvasujoseph commented 5 years ago

Easy fix - Change the button to

https://mherman.org/blog/social-authentication-with-passport-dot-js/

shankcode commented 5 years ago

Just wanted to elaborate a little bit on the above as it didn't make any sense to me at first to resolve the issue.

According to this stack overflow post the LinkedIn auth api doesn't respond with the cross origin request headers. This means that it is not possible to make an axios/fetch/superagent etc request from your front-end to your back-end to handle this call.

Instead you need to direct your browser to your back-end using an a tag or link which will then allow all the responses to correctly work (assuming you add the cross-origin stuff all mentioned above).

For example, I was running a separate React app using create-react-app running on http://localhost:3000 with a node backend on http://localhost:3001. I was attemping to make an axios call to my back end with a basic call like this fired from an onClick event on one of my front-end button components:

export const loginWithLinkedin = () => {
    const params = {
        method: "GET",
        headers: {
            "Content-Type": "application/json",
            "Access-Control-Allow-Origin": "http://localhost:3001",
            "Access-Control-Allow-Methods": "GET, POST, OPTIONS, PUT, PATCH, DELETE",
            "Access-Control-Allow-Headers": "X-Requested-With,content-type",
            "Access-Control-Allow-Credentials": true,
        },
        withCredentials: true,
        data: undefined
    }
    return axios('/api/account/linkedin', params)
    .then(respack.handleResponse)
    .catch(respack.handleError);
}

I had to change my Button component to not call this onClick function and instead wrapped it in an a tag with a href directly to my back-end

<a href="http://localhost:3001/api/account/linkedin">
    <Button
        type={'linkedin'}
        label={'Login with LinkedIn'}
        width={'100%'}
        height={50} />
</a>

My back end was then handling this with the basic way that was outlined in the OP of this post and everything seemed to work as expected again.

Hope this helps anyone else getting stuck in this situation!

@TimonSotiropoulos I wrapped my Login with google button within <a> tag as,

<a href="http://localhost:5000/api/auth/google">
     <button>Login With Google</button>
</a>

and its working fine in development (localhost).

How to make it work in case of production, because for production I did so

<a href="http://myappname.herokuapp.com/api/auth/google">
     <button>Login With Google</button>
</a>

but its not working, rather its hitting my Not Found page (Client).

TimonSotiropoulos commented 5 years ago

@shankcode Not sure exactly what your issue it, but if you are getting a not found page on your client it sounds like your server is not getting hit at the correct end point (it is just going to your front end).

cancerts commented 5 years ago

I added this and it seemed to fix the issue

app.all('/*', function(req, res, next) {
    res.header("Access-Control-Allow-Origin", "*");
    next();
});

app.all 在哪

maitrungduc1410 commented 5 years ago

[Edited] For those who is having this issue. Because some third party providers like linkedin, google, facebook, github... need us to redirect to their site when doing authentication. But when make request using axios in your js script it just makes an API call to third party instead of redirecting, so it causes error says 'CORS...' . The solution is using a tag or using window.location.href to your route for authentication. For example:

<a href="/auth/linkedin/login">Login</a>

// or

window.location.href('/auth/linkedin/login')
OctaneInteractive commented 5 years ago

For those who is having this issue. You need to call your API from a tag in href instead of using axios

I tried that, but Vue grabs it first via its own routing system.

maitrungduc1410 commented 5 years ago

For those who is having this issue. You need to call your API from a tag in href instead of using axios

I tried that, but Vue grabs it first via its own routing system.

I'm using vue and vue-router too. Vue doesn't grab routing system by default. Vue-router is out-of-the-box. But why do you need to call your api using <router-link>, router-link should only be used in changing between routes, not to call API. Just use normal a tag. Remember when call API by using a tag, it will be GET method

OctaneInteractive commented 5 years ago

Hi @maitrungduc1410, you've made a contradiction:

You need to call your API from a tag in href instead of using axios.

... but then:

But why do you need to call your API using <router-link>, [which] should only be used in changing between routes, not to call API.

maitrungduc1410 commented 5 years ago

Hi @maitrungduc1410, you've made a contradiction:

You need to call your API from a tag in href instead of using axios.

... but then:

But why do you need to call your API using <router-link>, [which] should only be used in changing between routes, not to call API.

Oop, sorry, when review my comments I see I have some problem when when explaining to you, lol. I just made a correction for my first comment. You can check it :)

MihaiWill commented 4 years ago

Is there a way that now we can fetch the route, instead using href?

lakshaygupta21 commented 3 years ago

Guys, I have a genuine fix for this. If not yet resolved go and visit this for the step by step implementation

https://www.wellhow.online/2021/04/setting-up-linkedin-oauth-and-fixing.html

Summary: Hit request to get the code(from frontend) now send this code to the backend In the backend, make another call to LinkedIn OAuth API and get the access token With this access, token make 3 separate calls to get the name, profile picture and email of the user(yes you heard that right you need to make 3 separate calls, and also the response JSON format is not very appealing)

major697 commented 3 years ago

@amarjeet987 @chikeud @TimonSotiropoulos Did you found resolve for this query?:

This works perfectly @TimonSotiropoulos , but how do you get back the response in the front end. Response is sent to the browser in this case. It might be a stupid question though, I am just a beginner. Help me out.

sanchit123k commented 3 years ago

@amarjeet987 @chikeud @TimonSotiropoulos Did you found resolve for this query?:

This works perfectly @TimonSotiropoulos , but how do you get back the response in the front end. Response is sent to the browser in this case. It might be a stupid question though, I am just a beginner. Help me out.

DId you find any solution for this issue of sending back the response to the front end? Please help if you found any solution

florianmartens commented 2 years ago

My 2ct.

With nest + passport, we're implementing the @UseGuards(GoogleOauthGuard) on your social auth handler. The Guard is configured to send a 302 (with a Location property for a new token) response if the request is not sent correctly.

Check if all your query params are set correctly and exist by the time that the request is made. To do this, find the request that returned the 302 response and check the query string.

rolandm2017 commented 1 year ago

For anyone else who has this problem, the solution in this thread works

<a href="http://localhost:3000/login/google">Google</a>

Where "localhost:3000/login/google" goes to app.get('/login/google', passport.authenticate('google', { scope: ['profile','email']}) );

This isn't about passport and linkedin but I believe it's a perfect analogue to the passport and google issue I'm having now

JamesIde commented 1 year ago

@plutownium that makes sense. But if the /login/google route returns a JWT in json, how can the frontend access the response from the call?

angela1393aa commented 1 year ago

@amarjeet987 @chikeud @TimonSotiropoulos Did you found resolve for this query?:

This works perfectly @TimonSotiropoulos , but how do you get back the response in the front end. Response is sent to the browser in this case. It might be a stupid question though, I am just a beginner. Help me out.

angela1393aa commented 1 year ago
//back
router.get('/google/callback', 

    . . .

    res.send(`
        <script>
        window.opener.postMessage(${JSON.stringify(returnData)}, '*');
        window.close();
        </script>
    `);
   }
  )
//front
  useEffect(() => {
    window.addEventListener('message', handleAuthentication);
    return () => {
      window.removeEventListener('message', handleAuthentication);
    };
  }, []);

  const handleAuthentication = (event) => {
    const authData = event.data;
    if (authData.status) {
      // console.log('google驗證資料',authData)
      dispatch(googleData({ token: authData.token, data: authData.data }))
      if(GoogleData.user.userInfo!==undefined){
        console.log('後端傳回來的',GoogleData.user.userInfo);
        message.success('登入成功');
        navigate('device/MonitorMachine');
      }
    }
  }
  const google = () => {
    window.open(`http://localhost:8000/auth/google`,"mywindow","location=1,status=1,scrollbars=1, width=800,height=800");
  }

<img className="login" onClick={google} src={googlelogin} alt="Login With Google"/>
irtizashabrez commented 12 months ago
//back
router.get('/google/callback', 

    . . .

    res.send(`
        <script>
        window.opener.postMessage(${JSON.stringify(returnData)}, '*');
        window.close();
        </script>
    `);
   }
  )
//front
  useEffect(() => {
    window.addEventListener('message', handleAuthentication);
    return () => {
      window.removeEventListener('message', handleAuthentication);
    };
  }, []);

  const handleAuthentication = (event) => {
    const authData = event.data;
    if (authData.status) {
      // console.log('google驗證資料',authData)
      dispatch(googleData({ token: authData.token, data: authData.data }))
      if(GoogleData.user.userInfo!==undefined){
        console.log('後端傳回來的',GoogleData.user.userInfo);
        message.success('登入成功');
        navigate('device/MonitorMachine');
      }
    }
  }
  const google = () => {
    window.open(`http://localhost:8000/auth/google`,"mywindow","location=1,status=1,scrollbars=1, width=800,height=800");
  }

<img className="login" onClick={google} src={googlelogin} alt="Login With Google"/>

@angela1393aa, were you able to get data on the frontend? I'm learning NestJS/React, and I tried your code above, but it did not work for me. If you can provide any help, that would be great.

TimonSotiropoulos commented 12 months ago

It has been a very long time since I worked on this project and I haven't used passport in a while, also how you are getting data back to your frontend after the request is extremely subjective depending on how you are managing state in your application, however I will at least attempt to explain roughly what I did, as this issue seems to be a problem for a lot of people. Please keep in mind this is a 4 year old project worth of code I am quoting, I have no idea if passport has changed the way it works since then but I will at least add what I have done.

So firstly, as was mentioned, I am can't use an axios request to setup the linkedin callback, I have to make a direct browser change using an tag. My actual final version of how I did this was as follows:

    _loginWithLinkedIn = () => {
        const { loginUserWithLinkedin } = this.props;
        loginUserWithLinkedin();
        window.location.assign(`${SYSTEM.SERVER_DOMAIN}/api/account/linkedin`)
    }

In this case, the loginUserWithLinkedin() function is an action that I fire off to cause the login button to enter a loading state so the UI on the front end shows a spinner, thats all it does. For the people asking how I was handling the different between development and production environments, as you can see I have abstracted out the SERVER_DOMAIN from my codebase, this is condensed down into:

const HTTP_PROTOCOL = (process.env.APP_ENV === 'development') ? 'http://' : 'https://';

let server;
let client;
let DOMAIN;
let PORT;
let SERVER_PORT;
let CLIENT_PORT;

switch(process.env.APP_ENV) {
    case 'development':
        DOMAIN = 'localhost';
        SERVER_PORT = 3001;
        CLIENT_PORT = 3000;
        PORT = SERVER_PORT;
        server = `${HTTP_PROTOCOL}${DOMAIN}:${SERVER_PORT}`;
        client = `${HTTP_PROTOCOL}${DOMAIN}:${CLIENT_PORT}`;
        break;
    case 'production':
        PORT = 5000;
        SERVER_PORT = PORT;
        CLIENT_PORT = PORT;
        DOMAIN = 'app.application.com';
        server = `${HTTP_PROTOCOL}${DOMAIN}`;
        client = `${HTTP_PROTOCOL}${DOMAIN}`;
        break;
    default:
        break;
}

const SERVER_DOMAIN = server;
const CLIENT_DOMAIN = client;

const SYSTEM = {
    SERVER_DOMAIN,
    CLIENT_DOMAIN,
    HTTP_PROTOCOL,
    SERVER_PORT,
    CLIENT_PORT,
    DOMAIN
}

export default SYSTEM;

From here we move into my backend code, where I have setup two end points. One that handles my frontend hitting the backend, and then one to handle the callback from the LinkedIn signin process:

router.get('api/account/linkedin', passport.authenticate('linkedin'), (req, res) => {});

This call sends the request off too LinkedIn to do its authentication magic, when it then returns back to my callbackUrl. I have an endpoint to handle that which is outlined as below

router.get('api/account/linkedin/callback', handleLinkedInAuth);

// And the function defined here
const handleLinkedInAuth = (req, res, next) => {
    passport.authenticate('linkedin', (err, user, info) => {
        const DOMAIN = (process.env.APP_ENV === 'development') ? SYSTEM.CLIENT_DOMAIN : SYSTEM.SERVER_DOMAIN;
        if (err) {
            return res.redirect(`${DOMAIN}/login`);
        }
        if (!user) {
            return res.redirect(`${DOMAIN}/login`);
        }
        req.logIn(user, function(err) {
            if (err) { return next(err); }
            DBController.account.updateLastLoggedIn(user._id);
            const redirectURL = (info.isNewUser) ? `${DOMAIN}/onboarding` : `${DOMAIN}/dashboard`;
            return res.redirect(redirectURL);
        });
    })(req, res, next)
}

Firstly there are two important things happening here, the first is that in the passport strategy I am reading the response from LinkedIn and setting the user up in my backend database. For this project I am using MongoDB with mongoose and the passport-local-mongoose library. How you implement this part of the project depends entirely on you, I will attach my linkedin passport strategy to this post as well, but you will need to update that to handle things how you want them.

The next important thing is once I have processed the linkedin strategy, I call the req.logIn function which when using passport will add a req.user if they are a valid user to the request structure that the backend sends to the front end. This is important as now when we redirect to the front end, we will have a user attached to all subsequent requests.

See the attached Passport strategy below, more information after this.

// The passport strategy for 'linkedin'

module.exports = (app) => {
    const strategy = new LinkedInStrategy({
            clientID: SECRETS.LINKEDIN_CLIENT_ID,
            clientSecret: SECRETS.LINKEDIN_CLIENT_SECRET,
            callbackURL: `${SYSTEM.SERVER_DOMAIN}/api/account/linkedin/callback`,
            scope: ['r_emailaddress', 'r_basicprofile'],
            state: true,
            passReqToCallback: true
        },
        function(req, token, tokenSecret, profile, done) {
            // asynchronous verification, for effect...
            process.nextTick(function () {
                const username = profile._json.emailAddress;
                // Basically from this point you will need to figure out how to store that user and create then an account in your system
                Models.account.findOne({ username: username }, (err, user) => {
                    // If the user exists, log them in and continue
                    if (user) {
                        console.log("User Exists in Databse, logging user in...");
                        return done(null, user, {isNewUser: false});
                    }
                    // Otherwise Create a new account for the user and then log them in!
                    else {
                        // Generate a password for the account
                        crypto.randomBytes(32, function(err, buf) {
                            if(err) { return reject(err); }
                            const password = buf.toString('hex');
                            const newUser = {
                                username: username,
                                accountType: USER_TYPE.FREE,
                                creationDate: Date.now(),
                                lastActive: Date.now()
                            }

                            Models.account.register(new Models.account(newUser), password, function(err, account) {
                                if (err) {
                                    console.log("Error Registering New account?");
                                    console.log(err);
                                    return done(err, null);
                                }
                                const userID = account._id;
                                const email = account.username;

                                const fName = (profile && profile._json && profile._json.firstName) ? profile._json.firstName : "";
                                const lName = (profile && profile._json && profile._json.lastName) ? profile._json.lastName : "";
                                const fullName = (profile && profile._json && profile._json.formattedName) ? profile._json.formattedName : "";
                                const company = (profile && profile._json && profile._json.positions && profile._json.positions._total > 0 && profile._json.positions.values[0].company && profile._json.positions.values[0].company.name) ? profile._json.positions.values[0].company.name : "";
                                const role = (profile && profile._json && profile._json.positions && profile._json.positions._total > 0 && profile._json.positions.values[0].title) ? profile._json.positions.values[0].title : "";
                                const location = (profile && profile._json && profile._json.location && profile._json.location.name) ? profile._json.location.name : "";
                                const profileImage = (profile && profile._json && profile._json.pictureUrl) ? profile._json.pictureUrl : "";
                                const industry = (profile && profile._json && profile._json.industry) ? profile._json.industry : "";

                                const profileData = {
                                    fName: fName,
                                    lName: lName,
                                    fullName: fullName,
                                    company: company,
                                    role: role,
                                    location: location,
                                    profileImage: profileImage,
                                    industry: industry
                                }

                                Promise.all([
                                    DBController.profile.createProfile(userID, profileData)
                                ])
                                .then((values) => {
                                    return done(null, account, {isNewUser: true});
                                })
                                .catch((err) => {
                                    console.log("error Creating Account Details");
                                    return done(err, null);
                                });
                            });
                        });
                    }
                });
            });
        });
    return strategy;
}

When we are authenticating a React application like this, we are breaking the normal "Single Page Application" protocol, and we are forcing the front end to actually reload the entire application. If you have server side rendering of some kind, you would be able to send the user information along with the initial load of your application. For my implementation, I DON'T have server side rendering, so whenever my React Application loads for the first time, I have to make a request to my backend to initialise the application. This does a number of things including checking to see if there is an existing session for the user (through cookies) and if it does attempts to log the user in again. So because the backend attaches the req.user to the request (through passports attached .logIn function), when my frontend makes its initial request to "initialise the application" I already have a req.user attached, which means my backend can handle validating that user and then providing the required information it needs to start the application.

What makes this super easy, is that now that the user is attached to our request, I can simply use passports attached function if the user is authenticated, as shown below:

router.post('api/account/auth', initialiseApplication);

const initialiseApplication = async (req, res) => {

    const isAuthenticated = req.isAuthenticated();
    // If the user is authenticated then we want to
    // go to the database and get all the different
    // user information
    if (isAuthenticated) {
        const userID = req.user._id;
                 const email = req.user.username;
        const admin = req.user.admin;
        const userType = req.user.accountType;
        const receiveEmails = req.user.receiveEmails;
        DBController.account.updateLastLoggedIn(userID);
        getAllUserInformation(userID, userType)
        .then((userInfo) => {
            return res.json(respack.sendOkay({
                isAuthenticated,
                                 userID: userID,
                                 email: email,
                admin: admin,
                userType: userType,
                ...userInfo,
            }));
        })
        .catch(() => {
            return res.json(respack.sendError({
                err: 'Failed to Start Application'
            }));
        })
    // If the user is not authenticated then we
    // can simply return the not authenticated
    // response
    } else {
        return res.json(respack.sendOkay({
            isAuthenticated,
        }));
    }
}

So for people to understand how to get the data back into their front end application, the process in short is a follows:

Step 1) From front end, make call to your backend that essentially forces a page refresh (either tag or other described methods) Step 2) Make the call to linkedIn and setup your callback from your backend Step 3) When the callback is hit by LinkedIn, save the user into your user management Step 4) Ensure in this callback call you use the req.logIn() function, this will add the user to all subsequent requests Step 5) Redirect to a new page from your callback (this will cause your React page to refresh)

Are you using Server Side Rendering? If No, like me Step 6) On initial load of your React App, make a request to your backend before it loads to check your server if the user is authenticated, if they are get their user data and send it back to your front end Step 7) Profit? If Yes, unlike me Step 6) Send your user information back with your initial load of your application

NOTES: i believe req.login and req.logIn are aliases for eachother, that may have changed in more recent apis

This is about as much information as I can provide on this topic i'm afraid, its been a while since I have used passport and this style of authentication.

Good Luck!

rathclayton1 commented 8 months ago

It has been a very long time since I worked on this project and I haven't used passport in a while, also how you are getting data back to your frontend after the request is extremely subjective depending on how you are managing state in your application, however I will at least attempt to explain roughly what I did, as this issue seems to be a problem for a lot of people. Please keep in mind this is a 4 year old project worth of code I am quoting, I have no idea if passport has changed the way it works since then but I will at least add what I have done.

So firstly, as was mentioned, I am can't use an axios request to setup the linkedin callback, I have to make a direct browser change using an tag. My actual final version of how I did this was as follows:

    _loginWithLinkedIn = () => {
        const { loginUserWithLinkedin } = this.props;
        loginUserWithLinkedin();
        window.location.assign(`${SYSTEM.SERVER_DOMAIN}/api/account/linkedin`)
    }

In this case, the loginUserWithLinkedin() function is an action that I fire off to cause the login button to enter a loading state so the UI on the front end shows a spinner, thats all it does. For the people asking how I was handling the different between development and production environments, as you can see I have abstracted out the SERVER_DOMAIN from my codebase, this is condensed down into:

const HTTP_PROTOCOL = (process.env.APP_ENV === 'development') ? 'http://' : 'https://';

let server;
let client;
let DOMAIN;
let PORT;
let SERVER_PORT;
let CLIENT_PORT;

switch(process.env.APP_ENV) {
    case 'development':
        DOMAIN = 'localhost';
        SERVER_PORT = 3001;
        CLIENT_PORT = 3000;
        PORT = SERVER_PORT;
        server = `${HTTP_PROTOCOL}${DOMAIN}:${SERVER_PORT}`;
        client = `${HTTP_PROTOCOL}${DOMAIN}:${CLIENT_PORT}`;
        break;
    case 'production':
        PORT = 5000;
        SERVER_PORT = PORT;
        CLIENT_PORT = PORT;
        DOMAIN = 'app.application.com';
        server = `${HTTP_PROTOCOL}${DOMAIN}`;
        client = `${HTTP_PROTOCOL}${DOMAIN}`;
        break;
    default:
        break;
}

const SERVER_DOMAIN = server;
const CLIENT_DOMAIN = client;

const SYSTEM = {
    SERVER_DOMAIN,
    CLIENT_DOMAIN,
    HTTP_PROTOCOL,
    SERVER_PORT,
    CLIENT_PORT,
    DOMAIN
}

export default SYSTEM;

From here we move into my backend code, where I have setup two end points. One that handles my frontend hitting the backend, and then one to handle the callback from the LinkedIn signin process:

router.get('api/account/linkedin', passport.authenticate('linkedin'), (req, res) => {});

This call sends the request off too LinkedIn to do its authentication magic, when it then returns back to my callbackUrl. I have an endpoint to handle that which is outlined as below

router.get('api/account/linkedin/callback', handleLinkedInAuth);

// And the function defined here
const handleLinkedInAuth = (req, res, next) => {
  passport.authenticate('linkedin', (err, user, info) => {
      const DOMAIN = (process.env.APP_ENV === 'development') ? SYSTEM.CLIENT_DOMAIN : SYSTEM.SERVER_DOMAIN;
      if (err) {
          return res.redirect(`${DOMAIN}/login`);
      }
      if (!user) {
          return res.redirect(`${DOMAIN}/login`);
      }
      req.logIn(user, function(err) {
          if (err) { return next(err); }
          DBController.account.updateLastLoggedIn(user._id);
          const redirectURL = (info.isNewUser) ? `${DOMAIN}/onboarding` : `${DOMAIN}/dashboard`;
          return res.redirect(redirectURL);
      });
  })(req, res, next)
}

Firstly there are two important things happening here, the first is that in the passport strategy I am reading the response from LinkedIn and setting the user up in my backend database. For this project I am using MongoDB with mongoose and the passport-local-mongoose library. How you implement this part of the project depends entirely on you, I will attach my linkedin passport strategy to this post as well, but you will need to update that to handle things how you want them.

The next important thing is once I have processed the linkedin strategy, I call the req.logIn function which when using passport will add a req.user if they are a valid user to the request structure that the backend sends to the front end. This is important as now when we redirect to the front end, we will have a user attached to all subsequent requests.

See the attached Passport strategy below, more information after this.

// The passport strategy for 'linkedin'

module.exports = (app) => {
    const strategy = new LinkedInStrategy({
            clientID: SECRETS.LINKEDIN_CLIENT_ID,
            clientSecret: SECRETS.LINKEDIN_CLIENT_SECRET,
            callbackURL: `${SYSTEM.SERVER_DOMAIN}/api/account/linkedin/callback`,
            scope: ['r_emailaddress', 'r_basicprofile'],
            state: true,
            passReqToCallback: true
        },
        function(req, token, tokenSecret, profile, done) {
            // asynchronous verification, for effect...
            process.nextTick(function () {
                const username = profile._json.emailAddress;
                // Basically from this point you will need to figure out how to store that user and create then an account in your system
                Models.account.findOne({ username: username }, (err, user) => {
                    // If the user exists, log them in and continue
                    if (user) {
                        console.log("User Exists in Databse, logging user in...");
                        return done(null, user, {isNewUser: false});
                    }
                    // Otherwise Create a new account for the user and then log them in!
                    else {
                        // Generate a password for the account
                        crypto.randomBytes(32, function(err, buf) {
                            if(err) { return reject(err); }
                            const password = buf.toString('hex');
                            const newUser = {
                                username: username,
                                accountType: USER_TYPE.FREE,
                                creationDate: Date.now(),
                                lastActive: Date.now()
                            }

                            Models.account.register(new Models.account(newUser), password, function(err, account) {
                                if (err) {
                                    console.log("Error Registering New account?");
                                    console.log(err);
                                  return done(err, null);
                              }
                                const userID = account._id;
                                const email = account.username;

                                const fName = (profile && profile._json && profile._json.firstName) ? profile._json.firstName : "";
                                const lName = (profile && profile._json && profile._json.lastName) ? profile._json.lastName : "";
                                const fullName = (profile && profile._json && profile._json.formattedName) ? profile._json.formattedName : "";
                                const company = (profile && profile._json && profile._json.positions && profile._json.positions._total > 0 && profile._json.positions.values[0].company && profile._json.positions.values[0].company.name) ? profile._json.positions.values[0].company.name : "";
                                const role = (profile && profile._json && profile._json.positions && profile._json.positions._total > 0 && profile._json.positions.values[0].title) ? profile._json.positions.values[0].title : "";
                                const location = (profile && profile._json && profile._json.location && profile._json.location.name) ? profile._json.location.name : "";
                                const profileImage = (profile && profile._json && profile._json.pictureUrl) ? profile._json.pictureUrl : "";
                                const industry = (profile && profile._json && profile._json.industry) ? profile._json.industry : "";

                                const profileData = {
                                    fName: fName,
                                    lName: lName,
                                    fullName: fullName,
                                    company: company,
                                    role: role,
                                    location: location,
                                    profileImage: profileImage,
                                    industry: industry
                                }

                                Promise.all([
                                    DBController.profile.createProfile(userID, profileData)
                                ])
                                .then((values) => {
                                    return done(null, account, {isNewUser: true});
                                })
                                .catch((err) => {
                                    console.log("error Creating Account Details");
                                    return done(err, null);
                                });
                            });
                        });
                  }
                });
            });
        });
    return strategy;
}

When we are authenticating a React application like this, we are breaking the normal "Single Page Application" protocol, and we are forcing the front end to actually reload the entire application. If you have server side rendering of some kind, you would be able to send the user information along with the initial load of your application. For my implementation, I DON'T have server side rendering, so whenever my React Application loads for the first time, I have to make a request to my backend to initialise the application. This does a number of things including checking to see if there is an existing session for the user (through cookies) and if it does attempts to log the user in again. So because the backend attaches the req.user to the request (through passports attached .logIn function), when my frontend makes its initial request to "initialise the application" I already have a req.user attached, which means my backend can handle validating that user and then providing the required information it needs to start the application.

What makes this super easy, is that now that the user is attached to our request, I can simply use passports attached function if the user is authenticated, as shown below:

router.post('api/account/auth', initialiseApplication);

const initialiseApplication = async (req, res) => {

  const isAuthenticated = req.isAuthenticated();
    // If the user is authenticated then we want to
    // go to the database and get all the different
    // user information
  if (isAuthenticated) {
      const userID = req.user._id;
                 const email = req.user.username;
      const admin = req.user.admin;
      const userType = req.user.accountType;
      const receiveEmails = req.user.receiveEmails;
      DBController.account.updateLastLoggedIn(userID);
      getAllUserInformation(userID, userType)
      .then((userInfo) => {
          return res.json(respack.sendOkay({
              isAuthenticated,
                                 userID: userID,
                                 email: email,
              admin: admin,
              userType: userType,
              ...userInfo,
          }));
      })
      .catch(() => {
          return res.json(respack.sendError({
              err: 'Failed to Start Application'
          }));
      })
    // If the user is not authenticated then we
    // can simply return the not authenticated
    // response
  } else {
      return res.json(respack.sendOkay({
          isAuthenticated,
      }));
  }
}

So for people to understand how to get the data back into their front end application, the process in short is a follows:

Step 1) From front end, make call to your backend that essentially forces a page refresh (either tag or other described methods) Step 2) Make the call to linkedIn and setup your callback from your backend Step 3) When the callback is hit by LinkedIn, save the user into your user management Step 4) Ensure in this callback call you use the req.logIn() function, this will add the user to all subsequent requests Step 5) Redirect to a new page from your callback (this will cause your React page to refresh)

Are you using Server Side Rendering? If No, like me Step 6) On initial load of your React App, make a request to your backend before it loads to check your server if the user is authenticated, if they are get their user data and send it back to your front end Step 7) Profit? If Yes, unlike me Step 6) Send your user information back with your initial load of your application

NOTES: i believe req.login and req.logIn are aliases for eachother, that may have changed in more recent apis

This is about as much information as I can provide on this topic i'm afraid, its been a while since I have used passport and this style of authentication.

Good Luck!

Been searching for days, and this is the best explanation I have found. Thank you