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:
Unauthenticated user attempts to access protected client route
(something like /posts/:postid
, and is redirected to /login
.
(React Router is handling this part)
User clicks the ‘Log in with Facebook’ button (or other Social auth service)
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?
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
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
.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With