How to redirect to correct client route after social auth with Passport (react, react-router, express, passport)

In case anybody else is struggling with this, this is what I ended up going with:

1. When user tries to access protected route, redirect to /login with React-Router.

First define a <PrivateRoute> component:

// App.jsx

const PrivateRoute = ({ component: Component, loggedIn, ...rest }) => {
  return (
    <Route
      {...rest}
      render={props =>
        loggedIn === true ? (
          <Component {...rest} {...props} />
        ) : (
          <Redirect
            to={{ pathname: "/login", state: { from: props.location } }}
          />
        )
      }
    />
  );
};

Then pass the loggedIn property to the route:

// App.jsx

<PrivateRoute
  loggedIn={this.props.appState.loggedIn}
  path="/poll/:id"
  component={ViewPoll}
/>

2. In /login component, save previous route to localStorage so I can later redirect back there after authentication:

// Login.jsx

  componentDidMount() {
   const { from } = this.props.location.state || { from: { pathname: "/" } };
   const pathname = from.pathname;
   window.localStorage.setItem("redirectUrl", pathname);
}

3. In SocialAuth callback, redirect to profile page on client, adding userId and token as route params

// auth.ctrl.js

exports.socialAuthCallback = (req, res) => {
  if (req.user.err) {
    res.status(401).json({
        success: false,
        message: `social auth failed: ${req.user.err}`,
        error: req.user.err
    })
  } else {
    if (req.user) {
      const user = req.user._doc;
      const userInfo = helpers.setUserInfo(user);
      const token = helpers.generateToken(userInfo);
      return res.redirect(`${CLIENT_URL}/user/${userObj._doc._id}/${token}`);
    } else {
      return res.redirect('/login');
    }
  }
};

4. In the Profile component on the client, pull the userId and token out of the route params, immediately remove them using window.location.replaceState, and save them to localStorage. Then check for a redirectUrl in localStorage. If it exists, redirect and then clear the value

// Profile.jsx

  componentWillMount() {
    let userId, token, authCallback;
    if (this.props.match.params.id) {
      userId = this.props.match.params.id;
      token = this.props.match.params.token;
      authCallback = true;

      // if logged in for first time through social auth,
      // need to save userId & token to local storage
      window.localStorage.setItem("userId", JSON.stringify(userId));
      window.localStorage.setItem("authToken", JSON.stringify(token));
      this.props.actions.setLoggedIn();
      this.props.actions.setSpinner("hide");

      // remove id & token from route params after saving to local storage
      window.history.replaceState(null, null, `${window.location.origin}/user`);
    } else {
      console.log("user id not in route params");

      // if userId is not in route params
      // look in redux store or local storage
      userId =
        this.props.profile.user._id ||
        JSON.parse(window.localStorage.getItem("userId"));
      if (window.localStorage.getItem("authToken")) {
        token = window.localStorage.getItem("authToken");
      } else {
        token = this.props.appState.authToken;
      }
    }

    // retrieve user profile & save to app state
    this.props.api.getProfile(token, userId).then(result => {
      if (result.type === "GET_PROFILE_SUCCESS") {
        this.props.actions.setLoggedIn();
        if (authCallback) {
          // if landing on profile page after social auth callback,
          // check for redirect url in local storage
          const redirect = window.localStorage.getItem("redirectUrl");
          if (redirect) {
            // redirect to originally requested page and then clear value
            // from local storage
            this.props.history.push(redirect);
            window.localStorage.setItem("redirectUrl", null);
          }
        }
      }
    });
  }

This blog post was helpful in figuring things out. The #4 (recommended) solution in the linked post is much simpler and would probably work fine in production, but I couldn't get it to work in development where the server and client have different base URLs, because a value set to localStorage by a page rendered at the server URL will not exist in local Storage for the client URL


Depending on your application architecture, I can give you a couple of ideas, but they are all based on the fundamental :

Once you have backend handling authentication, you need to store the state of the user in your backend as well ( via session cookie / JWT )

You can create a cookie-session store for your express app which cookie, you need to configure properly to use both the domains ( the backend domain and the front-end domain ) or use JWT for this.

Let's go with more details

Use React to check the authentication state

You can implement an end-point in express called /api/credentials/check which will return 403 if the user is not authenticated and 200 if is.

In your react app you will have to call this end-point and check if the user is authenticated or not. In case of not authenticated you can redirect to /login in your React front-end.

I use something similar :

class AuthRoute extends React.Component {
    render() {

        const isAuthenticated = this.props.user;
        const props = assign( {}, this.props );

        if ( isAuthenticated ) {
             return <Route {...props} />;
        } else {
             return <Redirect to="/login"/>;
        }

    }
}

And then in your router

<AuthRoute exact path="/users" component={Users} />
<Route exact path="/login" component={Login} />

In my root component I add

componentDidMount() {
    store.dispatch( CredentialsActions.check() );
}

Where CredentialsActions.check is just a call that populates props.user in case we return 200 from /credentials/check.

Use express to render your React app and dehydrate the user state inside the react app

This one is a bit tricky. And it has the presumption that your react app is served from your express app and not as static .html file.

In this case you can add a special <script>const state = { authenticated: true }</script> which will be served by express if the user was authenticated.

By doing this you can do:

const isAuthenticated = window.authenticated;

This is not the best practice, but it's the idea of hydrate and rehydration of your state.

References :

  1. Hydration / rehydration in Redux
  2. Hydrate / rehydrate idea
  3. Example of React / Passport authentication
  4. Example of cookie / Passport authentication