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

后端 未结 2 1258
無奈伤痛
無奈伤痛 2021-02-02 03:29

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.

相关标签:
2条回答
  • 2021-02-02 03:36

    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
    0 讨论(0)
  • 2021-02-02 03:59

    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

    0 讨论(0)
提交回复
热议问题