Intro
What follows is a quick piece of documentation on how I managed to set up session handling with React, Redux, express-session
, and Apollo.
A few necessary disclaimers:
- It is not meant to be used as a full, start-to-finish tutorial and it generally assumes a base-level understanding of all the above technologies.
- However, it can definitely be used as a taking off point or reference if you are going down a similar path, especially in combination with your own research and the accompanying codebase.
- I am not an expert in any of these technologies. This is simply how I solved the problems I was facing. If anyone has suggestions / better ways of handling them, please let me know!
Context
I've been building a Spaced Repetition Learning application, which you can check out the github repo for here. I decided to build the application in three separate waves. The first wave, I simply built out a local CLI version, which is fully functional and buildable / installable from here. The second wave, I built out a (what has ended up being [though at the time I thought it was complete, of course]]) rough-draft of the backend API. It was during this wave that I was naturally confronted with the crucial issues of authentication and authorization.
This is my first full stack app I have built completely on my own. When doing tutorials in the past, they tended to utilize JSON Web Tokens (JWT) for authentication and session handling. However, with a little bit of research, it seems that the use of JWT for this purpose is rather contentious from a security standpoint. Sure, I am essentially making a flashcard app, not a banking app, but my security researcher past wouldn't let me live with myself if I built out something as essential as AuthN on a shaky foundation. Plus, what a user studies can provide quite a bit of insight into who they are, so there is indeed a privacy issue at play there.
Thus, I decided to go with the tried and proven express-session
for session handling, with connect-mongodb-session
as the session store. But this would prove to be a little tricky when tying it in with Apollo on the client-side.
Server-Side
On the backend, implementing session handling was relatively straight-forward. First, we import the relative packages in our server.js
(note, I use transpiling on the import
statements. also, i am of course leaving out unrelated code. For the full code, see the github repo):
import express from "express";
import { ApolloServer } from "apollo-server-express";
import session from "express-session";
var MongoDBStore = require("connect-mongodb-session")(session);
Then, we set up Apollo and the session handling:
[...]
var server = new ApolloServer({
typeDefs: [rootSchema, ...schemaTypes],
resolvers: merge({}, user, deck, card),
context(req) {
return { ...req.req };
}
});
var app = express();
[...]
var store = new MongoDBStore({
uri: config.DB_URI,
collection: "sessions"
});
store.on("error", function(error) {
console.log(error);
});
app.use(
session({
name: config.SESS_NAME,
secret: config.SESS_SECRET,
resave: true,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV == "production",
maxAge: config.SESS_LIFETIME
},
store: store
})
);
[...]
var corsOptions = {
origin: ["http://localhost:3000", serverUrl],
credentials: true
};
app.use(cors(corsOptions));
server.applyMiddleware({ app, cors: false });
[...]
Note that we must set credentials: true
in corsOptions
for the Apollo server in order for cookie data to be sent along with the graphql requests. Also, since we set this manually in corsOptions
we must also manually disable the cors
option in the call to server.applyMiddleware
; else, our values will be overridden. Thanks to Ryan Doyle for figuring that piece of the puzzle out.
Also note that we build a context
object out of req.req
. This is important, as we will store the user object there and essentially utilize it for all authN and authZ checks.
For example, our login
and logout
resolvers (types/user/user.resolver.js
) may look like this:
async function login(_, args, ctx) {
if (isAuthenticated(ctx.session)) {
throw new ForbiddenError("User already authenticated");
}
try {
return await loginUser(
args.input.username,
args.input.password,
ctx.session
);
} catch (err) {
throw err;
}
}
async function logout(_, args, ctx) {
if (!isAuthenticated(ctx.session)) {
throw new AuthenticationError("User not authenticated");
}
return await logoutUser(ctx);
}
...with isAuthenticated
, loginUser
, and logoutUser
(utils/auth.js
) being defined as:
function isAuthenticated(session) {
return session.user != undefined;
}
async function loginUser(username, password, session) {
if (isValidUsername && isValidPassword) {
var user = await User.findOne({ username });
if (user != null && (await user.checkPassword(password))) {
session.user = {
_id: user._id,
username: user.username
};
return session.user;
}
}
throw new UserInputError("Invalid username or password.");
}
async function logoutUser(ctx) {
var loggedOutUser = ctx.session.user;
await ctx.session.destroy();
ctx.res.clearCookie(SESS_NAME);
return loggedOutUser;
}
Notice how we're simply examining if the user
object exists on the context (ctx
) for the authN check? As long as we make sure we correctly add and remove the user object (with the help of express-session
builtins like session.destroy()
), we can sleep soundly knowing the simple authN check is sufficient.
Client-Side
Okay, so we can login and logout all day via Insomnia or Postman on the backend, but how do we tie this into our React frontend? While it seems like everybody now thinks the most straight-forward way to do this is with React's useContext
API, the most viable way that I found (ie, that I actually understood AND was able to get working without a major headache) was with good ol' Redux.
I am using Formik for the sign in page on the app; so, the onSubmit
looks something like this (client/src/components/auth/SignIn.js
):
[...]
<Fragment>
<Formik
initialValues={initialState}
validationSchema={validationSchema}
onSubmit={async (values, actions) => {
const variables = {
input: {
username: values.username,
password: values.password
}
};
try {
await signIn(variables);
actions.setSubmitting(false);
history.push("/dashboard");
} catch (err) {
console.log(err);
actions.setSubmitting(false);
actions.setStatus({ msg: "Invalid username or password." });
}
}}
>
[...]
Note how we're calling the signIn
function, which in our case is a Redux action (client/src/actions/session.js
):
import * as apiUtil from '../util/session';
export const RECEIVE_CURRENT_USER = 'RECEIVE_CURRENT_USER';
export const LOGOUT_CURRENT_USER = 'LOGOUT_CURRENT_USER';
const receiveCurrentUser = user => ({
type: RECEIVE_CURRENT_USER,
user
})
const logoutCurrentUser = () => ({
type: LOGOUT_CURRENT_USER
})
export const signIn = variables => async dispatch => {
try {
var data = await apiUtil.signIn(variables);
return dispatch(receiveCurrentUser(data));
} catch(err) {
throw err;
}
}
export const signOut = () => async dispatch => {
try {
await apiUtil.signOut();
return dispatch(logoutCurrentUser());
} catch(err) {
throw err;
}
}
And of course, the relevant reducers look something like (client/src/reducers/session.js
):
import { RECEIVE_CURRENT_USER, LOGOUT_CURRENT_USER } from "../actions/session";
const _nullSession = {
username: null,
userId: null
};
export default (state = _nullSession, { type, user }) => {
Object.freeze(state);
switch (type) {
case RECEIVE_CURRENT_USER:
return user;
case LOGOUT_CURRENT_USER:
return _nullSession;
default:
return state;
}
};
So we have our reducers and actions defined but how do we make the Apollo client call to actually interact with our graphql server-side resolvers? You'll notice in our actions we reference util/session
, let's take a look at that:
import { gql } from "apollo-boost";
// this is placed in its own module in the actual codebase
const client = new ApolloClient({
uri: "http://localhost:4000/graphql",
credentials: "include"
});
const signInMutation = gql`
mutation signin($input: LoginUserInput!) {
login(input: $input) {
username
_id
}
}
`;
const signOutMutation = gql`
mutation logout {
logout {
username
_id
}
}
`;
async function signIn(variables) {
var data = await client.mutate({ mutation: signInMutation, variables });
return {
username: data.data.login.username,
userId: data.data.login._id
}
}
async function signOut() {
return await client.mutate({ mutation: signOutMutation })
}
Here we manually create our Apollo client and write out the relevant graphql mutations. Finally, we use them with calls to client.mutate
. This was the most straight-forward way I found to perform such operations and ended up using a similar pattern for pretty much all my Apollo client / server interactions. I am especially keen on finding out from Apollo experts if there are more optimal ways of handling this.
The last mini piece is simply making sure your desired protected React routes are actually protected! This can be achieved with something like this (client/src/components/common/ProtectedRoute
):
import React from "react";
import { Route, Redirect } from "react-router-dom";
import { connect } from "react-redux";
const mapStateToProps = ({ session }) => ({
loggedIn: Boolean(session.userId)
});
const ProtectedRoute = ({ loggedIn, component: Component, ...rest }) => (
<Route
{...rest}
render={props =>
loggedIn ? <Component {...props} /> : <Redirect to="/signin" />
}
/>
);
export default connect(mapStateToProps)(ProtectedRoute);
...and finally in App.js
:
[...]
function App() {
return (
<BrowserRouter>
<div>
<Route exact path="/" component={Landing}
[...]
<ProtectedRoute exact path="/dashboard" component={Dashboard} />
</div>
</BrowserRouter>
);
}
export default App;
And that's it! Now we have authentication and session-handling implemented across the entire stack, with all pieces working in harmony.
Conclusion
With the increased popularity in JWT Use for session-handling there was a clear lack of documentation for using something like express-session
along with React and Apollo. Also, while many blogs now promote the use of useContext
for such app-wide state tracking, in my case, it actually seemed more appropriate and simpler to go with Redux. For interacting with the Apollo server, I opted to abstract out the relevant authN logic to its own module and make manual client mutation queries.
It was quite a puzzle to piece together but in the end it seems to function rather well. I encourage you to play around with it by cloning the app's repo and building / running it on your own!
And of course any suggestions are welcome!
Top comments (0)