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 :
- Hydration / rehydration in Redux
- Hydrate / rehydrate idea
- Example of React / Passport authentication
- Example of cookie / Passport authentication