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

13,999

Solution 1

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

Solution 2

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
Share:
13,999

Related videos on Youtube

rg_
Author by

rg_

Updated on June 04, 2022

Comments

  • rg_
    rg_ almost 2 years

    I have a React/Redux/React Router front end, Node/Express back end. I’m using Passport (various strategies including Facebook, Google and Github) for authentication.

    What I want to happen:

    1. Unauthenticated user attempts to access protected client route (something like /posts/:postid, and is redirected to /login. (React Router is handling this part)

    2. User clicks the ‘Log in with Facebook’ button (or other Social auth service)

    3. After authentication, user is automatically redirected back to the route they were attempting to access in step 1.

    What is happening instead:

    The only way I’ve found to successfully handle Passport social authentication with a React front end is to wrap the ‘Log in with Facebook’ button in an <a> tag:

    <a href="http://localhost:8080/auth/facebook">Facebook Login</a>

    If I try to do it as an API call instead of a link I always get an error message (this issue is explained in a lot more detail here: Authentication with Passport + Facebook + Express + create-react-app + React-Router + proxy)

    So the user clicks the link, which hits the Express API, successfully authenticates with Passport, and then Passport redirects to the callback route (http://localhost:8080/auth/facebook/callback).

    In the callback function I need to (1) return the user object and token to the client, and (2) redirect to a client route — either the protected route they were trying to access before they got redirected to /login, or some default route like / or /dashboard.

    But since there isn’t a way to do both of these things in Express (I can’t res.send AND res.redirect, I have to choose one), I’ve been handling it in what feels like kind of a clunky way: res.redirect(`${CLIENT_URL}/user/${userId}`)

    This loads the /user route on the client, and then I’m pulling the userId out of the route params, saving it to Redux, then making ANOTHER call to the server to return the token to save token to localStorage.

    This is all working, although it feels clunky, but I can’t figure out how to redirect to the protected route the user was trying to access before being prompted to log in.

    I first tried saving the attempted route to Redux when the user tries to access it, thinking I could use that to redirect once they land on the profile page after authentication. But since the Passport auth flow takes the user off-site for 3d-party authentication and then reloads the SPA on res.redirect, the store is destroyed and the redirect path is lost.

    What I ended up settling on is saving the attempted route to localStorage, checking to see if there is a redirectUrl key in localStorage when the /user component mounts on the front end, redirecting with this.props.history.push(redirectUrl) and then clearing the redirectUrl key from localStorage. This seems like a really dirty workaround and there has got to be a better way to do this. Has anybody else figuree out how to make this work?

  • rg_
    rg_ about 6 years
    I am already storing JWT on server & using passport-jwt strategy to check authentication status. I'm trying to figure out how to deal with unauthenticated users who try to access a protected route, are sent to /login, click social auth which sends them to server URL, then Express res.redirects back to client on callback. the example you linked uses passport-local. with local strategy i can use a standard API call, which is working fine. with social auth (links directly to http://localhost:8080/auth/facebook) i'm trying to figure out how to handle client redirect after callback.
  • drinchev
    drinchev about 6 years
    You must have your app to intercept the callback from Facebook ( as per passport guideline ) and then redirect to your client app /some-page. Then when your client app loads you CHECK if the request is authenticated via /credentials/check or any other end-point that you can have. And if it is authenticated you load the specific route.