Building Chatty — Part 7: GraphQL Authentication

A WhatsApp clone with React Native and Apollo

Simon Tucker
38 min readMay 8, 2017

--

This is the seventh blog in a multipart series where we will be building Chatty, a WhatsApp clone, using React Native and Apollo. You can view the code for this part of the series here.

In this tutorial, we’ll be adding authentication (auth) to Chatty, solidifying Chatty as a full-fledged MVP messaging app!

Here’s what we will accomplish in this tutorial:

  1. Introduce JSON Web Tokens (JWT)
  2. Build server-side infrastructure for JWT auth with Queries and Mutations
  3. Refactor Schemas and Resolvers with auth
  4. Build server-side infrastructure for JWT auth with Subscriptions
  5. Design login/signup layout in our React Native client
  6. Build client-side infrastructure for JWT auth with Queries and Mutations
  7. Build client-side infrastructure for JWT auth with Subscriptions
  8. Refactor Components, Queries, Mutations, and Subscriptions with auth
  9. Reflect on all we’ve accomplished!

Yeah, this one’s gonna be BIG….

JSON Web Tokens (JWT)

JSON Web Token (JWT) is an open standard (RFC 7519) for securely sending digitally signed JSONs between parties. JWTs are incredibly cool for authentication because they let us implement reliable Single Sign-On (SSO) and persisted auth with low overhead on any platform (native, web, VR, whatever…) and across domains. JWTs are a strong alternative to pure cookie or session based auth with simple tokens or SAML, which can fail miserably in native app implementations. We can even use cookies with JWTs if we really want.

Without getting into technical details, a JWT is basically just a JSON message that gets all kinds of encoded, hashed, and signed to keep it super secure. Feel free to dig into the details here.

For our purposes, we just need to know how to use JWTs within our authentication workflow. When a user logs into our app, the server will check their email and password against the database. If the user exists, we’ll take their {email: <your-email>, password: <your-pw>}combination, turn it into a lovely JWT, and send it back to the client. The client can store the JWT forever or until we set it to expire.

Whenever the client wants to ask the server for data, it’ll pass the JWT in the request’s Authorization Header (Authorization: Bearer <token>). The server will decode the Authorization Header before executing every request, and the decoded JWT should contain {email: <your-email>, password: <your-pw>}. With that data, the server can retrieve the user again via the database or a cache to determine whether the user is allowed to execute the request.

Let’s make it happen!

JWT Authentication for Queries and Mutations

We can use the excellent express-jwt and jsonwebtoken packages for all our JWT encoding/decoding needs. We’re also going to use bcrypt for hashing passwords and dotenv to set our JWT secret key as an environment variable:

npm i express-jwt jsonwebtoken bcrypt dotenv

In a new .env file on the root directory, let’s add a JWT_SECRET environment variable:

Step 7.1: Add environment variables for JWT_SECRET

Added .env

...# .env
# use your own secret!!!
JWT_SECRET=your_secret

We’ll process the JWT_SECRET inside a new file server/config.js:

Step 7.1: Add environment variables for JWT_SECRET

Added server/config.js

...import dotenv from 'dotenv';dotenv.config({ silent: true });export const {
JWT_SECRET,
} = process.env;
const defaults = {
JWT_SECRET: 'your_secret',
};
Object.keys(defaults).forEach((key) => {
if (!process.env[key] || process.env[key] === defaults[key]) {
throw new Error(`Please enter a custom ${key} in .env on the root directory`);
}
});
export default JWT_SECRET;

Now, let’s update our express server in server/index.js to use express-jwt middleware. Even though our app isn't a pure express app, we can still use express-style middleware on requests passing through our ApolloServer:

Step 7.2: Add jwt middleware to express

Changed server/index.js

...import { ApolloServer } from 'apollo-server';
import jwt from 'express-jwt';
import { typeDefs } from './data/schema';
import { mocks } from './data/mocks';
import { resolvers } from './data/resolvers';
import { JWT_SECRET } from './config';
import { User } from './data/connectors';
const PORT = 8080;... resolvers,
typeDefs,
// mocks,
context: ({ req, res, connection }) => {
// web socket subscriptions will return a connection
if (connection) {
// check connection for metadata
return {};
}
const user = new Promise((resolve, reject) => {
jwt({
secret: JWT_SECRET,
credentialsRequired: false,
})(req, res, (e) => {
if (req.user) {
resolve(User.findOne({ where: { id: req.user.id } }));
} else {
resolve(null);
}
});
});
return {
user,
};
},
});
server.listen({ port: PORT }).then(({ url }) => console.log(`🚀 Server ready at ${url}`));

The express-jwt middleware checks our Authorization Header for a Bearer token, decodes the token using the JWT_SECRET into a JSON object, and then attaches that Object to the request as req.user. We can use req.user to find the associated User in our database — we pretty much only need to use the id parameter to retrieve the User because we can be confident the JWT is secure (more on this later). Lastly, we return the found User in this context function. By doing this, every one of our Resolvers will get passed a context parameter with the User, which we will use to validate credentials before touching any data.

Note that by setting credentialsRequired: false, we allow non-authenticated requests to pass through the middleware. This is required so we can allow signup and login requests (and others) through the endpoint.

Refactoring Schemas

Time to focus on our Schema. We need to perform 3 changes to server/data/schema.js:

  1. Add new GraphQL Mutations for logging in and signing up
  2. Add the JWT to the User type
  3. Since the User will get passed into all the Resolvers automatically via context, we no longer need to pass a userId to any queries or mutations, so let’s simplify their inputs!

Step 7.3: Update Schema with auth

Changed server/data/schema.js

...    messages: [Message] # messages sent by user
groups: [Group] # groups the user belongs to
friends: [User] # user's friends/contacts
jwt: String # json web token for access
}
# a message sent from a user to a group...
type Mutation {
# send a message to a group
createMessage(text: String!, groupId: Int!): Message
createGroup(name: String!, userIds: [Int]): Group
deleteGroup(id: Int!): Group
leaveGroup(id: Int!): Group # let user leave group
updateGroup(id: Int!, name: String): Group
login(email: String!, password: String!): User
signup(email: String!, password: String!, username: String): User
}
type Subscription {
# Subscription fires on every message added
# for any of the groups with one of these groupIds
messageAdded(groupIds: [Int]): Message
groupAdded(userId: Int): Group
}

Because our server is stateless, we don’t need to create a logout mutation! The server will test for authorization on every request and login state will solely be kept on the client.

Refactoring Resolvers

We need to update our Resolvers to handle our new login and signup Mutations. We can update server/data/resolvers.js as follows:

Step 7.4: Update Resolvers with login and signup mutations

Changed server/data/resolvers.js

...import GraphQLDate from 'graphql-date';
import { withFilter } from 'apollo-server';
import { map } from 'lodash';
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import { Group, Message, User } from './connectors';
import { pubsub } from '../subscriptions';
import { JWT_SECRET } from '../config';
const MESSAGE_ADDED_TOPIC = 'messageAdded';
const GROUP_ADDED_TOPIC = 'groupAdded';
... return Group.findOne({ where: { id } })
.then(group => group.update({ name }));
},
login(_, { email, password }, ctx) {
// find user by email
return User.findOne({ where: { email } }).then((user) => {
if (user) {
// validate password
return bcrypt.compare(password, user.password).then((res) => {
if (res) {
// create jwt
const token = jwt.sign({
id: user.id,
email: user.email,
}, JWT_SECRET);
user.jwt = token;
ctx.user = Promise.resolve(user);
return user;
}
return Promise.reject('password incorrect');
});
}
return Promise.reject('email not found');
});
},
signup(_, { email, password, username }, ctx) {
// find user by email
return User.findOne({ where: { email } }).then((existing) => {
if (!existing) {
// hash password and create user
return bcrypt.hash(password, 10).then(hash => User.create({
email,
password: hash,
username: username || email,
})).then((user) => {
const { id } = user;
const token = jwt.sign({ id, email }, JWT_SECRET);
user.jwt = token;
ctx.user = Promise.resolve(user);
return user;
});
}
return Promise.reject('email already exists'); // email already exists
});
},
},
Subscription: {
messageAdded: {

Let’s break this code down a bit. First let’s look at login:

  1. We search our database for the User with the supplied email
  2. If the User exists, we use bcrypt to compare the User’s password (we store a hashed version of the password in the database for security) with the supplied password
  3. If the passwords match, we create a JWT with the User’s id and email
  4. We return the User with the JWT attached and also attach a User Promise to context to pass down to other resolvers.

The code for signup is very similar:

  1. We search our database for the User with the supplied email
  2. If no User with that email exists yet, we hash the supplied password and create a new User with the email, hashed password, and username (which defaults to email if no username is supplied)
  3. We return the new User with the JWT attached and also attach a User Promise to context to pass down to other resolvers.

We need to also change our fake data generator in server/data/connectors.js to hash passwords before they’re stored in the database:

Step 7.5: Update fake data with hashed passwords

Changed server/data/connectors.js

...import { _ } from 'lodash';
import faker from 'faker';
import Sequelize from 'sequelize';
import bcrypt from 'bcrypt';
// initialize our database
const db = new Sequelize('chatty', null, null, {
... name: faker.lorem.words(3),
}).then(group => _.times(USERS_PER_GROUP, () => {
const password = faker.internet.password();
return bcrypt.hash(password, 10).then(hash => group.createUser({
email: faker.internet.email(),
username: faker.internet.userName(),
password: hash,
}).then((user) => {
console.log(
'{email, username, password}',
... text: faker.lorem.sentences(3),
}));
return user;
}));
})).then((userPromises) => {
// make users friends with all users in the group
Promise.all(userPromises).then((users) => {

Sweet! Now let’s refactor our Type, Query, and Mutation resolvers to use authentication to protect our data. Our earlier changes to ApolloServer will attach a context parameter with the authenticated User to every request on our GraphQL endpoint. We consume context (ctx) in the Resolvers to build security around our data. For example, we might change createMessage to look something like this:

// this isn't good enough!!!
createMessage(_, { groupId, text }, ctx) {
if (!ctx.user) {
throw new ForbiddenError('Unauthorized');
}
return ctx.user.then((user)=> {
if(!user) {
throw new ForbiddenError('Unauthorized');
}
return Message.create({
userId: user.id,
text,
groupId,
}).then((message) => {
// Publish subscription notification with the whole message
pubsub.publish('messageAdded', message);
return message;
});
});
},

This is a start, but it doesn’t give us the security we really need. Users would be able to create messages for any group, not just their own groups. We could build this logic into the resolver, but we’re likely going to need to reuse logic for other Queries and Mutations. Our best move is to create a business logic layer in between our Connectors and Resolvers that will perform authorization checks. By putting this business logic layer in between our Connectors and Resolvers, we can incrementally add business logic to our application one Type/Query/Mutation at a time without breaking others.

In the Apollo docs, this layer is occasionally referred to as the models layer, but that name can be confusing, so let’s just call it logic.

Let’s create a new file server/data/logic.js where we’ll start compiling our business logic:

Step 7.6: Create logic.js

Added server/data/logic.js

...import { ApolloError, AuthenticationError, ForbiddenError } from 'apollo-server';
import { Message } from './connectors';
// reusable function to check for a user with context
function getAuthenticatedUser(ctx) {
return ctx.user.then((user) => {
if (!user) {
throw new AuthenticationError('Unauthenticated');
}
return user;
});
}
export const messageLogic = {
createMessage(_, { text, groupId }, ctx) {
return getAuthenticatedUser(ctx)
.then(user => user.getGroups({ where: { id: groupId }, attributes: ['id'] })
.then((group) => {
if (group.length) {
return Message.create({
userId: user.id,
text,
groupId,
});
}
throw new ForbiddenError('Unauthorized');
}));
},
};

We’ve separated out the function getAuthenticatedUser to check whether a User is making a request. We’ll be able to reuse this function across our logic for other requests.

Now we can start injecting this logic into our Resolvers:

Step 7.7: Apply messageLogic to createMessage resolver

Changed server/data/resolvers.js

...import { Group, Message, User } from './connectors';
import { pubsub } from '../subscriptions';
import { JWT_SECRET } from '../config';
import { messageLogic } from './logic';
const MESSAGE_ADDED_TOPIC = 'messageAdded';
const GROUP_ADDED_TOPIC = 'groupAdded';
... },
},
Mutation: {
createMessage(_, args, ctx) {
return messageLogic.createMessage(_, args, ctx)
.then((message) => {
// Publish subscription notification with message
pubsub.publish(MESSAGE_ADDED_TOPIC, { [MESSAGE_ADDED_TOPIC]: message });
return message;
});
},
createGroup(_, { name, userIds, userId }) {
return User.findOne({ where: { id: userId } })

createMessage will return the result of the logic in messageLogic, which returns a Promise that either successfully resolves to the new Message or rejects due to failed authorization.

Let’s fill out our logic in server/data/logic.js to cover all GraphQL Types, Queries and Mutations:

Step 7.8: Create logic for all Resolvers

Changed server/data/logic.js

...import { ApolloError, AuthenticationError, ForbiddenError } from 'apollo-server';
import { Group, Message, User } from './connectors';
// reusable function to check for a user with context
function getAuthenticatedUser(ctx) {
...}export const messageLogic = {
from(message) {
return message.getUser({ attributes: ['id', 'username'] });
},
to(message) {
return message.getGroup({ attributes: ['id', 'name'] });
},
createMessage(_, { text, groupId }, ctx) {
return getAuthenticatedUser(ctx)
.then(user => user.getGroups({ where: { id: groupId }, attributes: ['id'] })
... }));
},
};
export const groupLogic = {
users(group) {
return group.getUsers({ attributes: ['id', 'username'] });
},
messages(group, { first, last, before, after }) {
// base query -- get messages from the right group
const where = { groupId: group.id };
// because we return messages from newest -> oldest
// before actually means newer (date > cursor)
// after actually means older (date < cursor)
if (before) {
// convert base-64 to utf8 iso date and use in Date constructor
where.id = { $gt: Buffer.from(before, 'base64').toString() };
}
if (after) {
where.id = { $lt: Buffer.from(after, 'base64').toString() };
}
return Message.findAll({
where,
order: [['id', 'DESC']],
limit: first || last,
}).then((messages) => {
const edges = messages.map(message => ({
cursor: Buffer.from(message.id.toString()).toString('base64'), // convert createdAt to cursor
node: message, // the node is the message itself
}));
return {
edges,
pageInfo: {
hasNextPage() {
if (messages.length < (last || first)) {
return Promise.resolve(false);
}
return Message.findOne({
where: {
groupId: group.id,
id: {
[before ? '$gt' : '$lt']: messages[messages.length - 1].id,
},
},
order: [['id', 'DESC']],
}).then(message => !!message);
},
hasPreviousPage() {
return Message.findOne({
where: {
groupId: group.id,
id: where.id,
},
order: [['id']],
}).then(message => !!message);
},
},
};
});
},
query(_, { id }, ctx) {
return getAuthenticatedUser(ctx).then(user => Group.findOne({
where: { id },
include: [{
model: User,
where: { id: user.id },
}],
}));
},
createGroup(_, { name, userIds }, ctx) {
return getAuthenticatedUser(ctx)
.then(user => user.getFriends({ where: { id: { $in: userIds } } })
.then((friends) => { // eslint-disable-line arrow-body-style
return Group.create({
name,
}).then((group) => { // eslint-disable-line arrow-body-style
return group.addUsers([user, ...friends]).then(() => {
group.users = [user, ...friends];
return group;
});
});
}));
},
deleteGroup(_, { id }, ctx) {
return getAuthenticatedUser(ctx).then((user) => { // eslint-disable-line arrow-body-style
return Group.findOne({
where: { id },
include: [{
model: User,
where: { id: user.id },
}],
}).then(group => group.getUsers()
.then(users => group.removeUsers(users))
.then(() => Message.destroy({ where: { groupId: group.id } }))
.then(() => group.destroy()));
});
},
leaveGroup(_, { id }, ctx) {
return getAuthenticatedUser(ctx).then((user) => {
return Group.findOne({
where: { id },
include: [{
model: User,
where: { id: user.id },
}],
}).then((group) => {
if (!group) {
throw new ApolloError('No group found', 404);
}
return group.removeUser(user.id)
.then(() => group.getUsers())
.then((users) => {
// if the last user is leaving, remove the group
if (!users.length) {
group.destroy();
}
return { id };
});
});
});
},
updateGroup(_, { id, name }, ctx) {
return getAuthenticatedUser(ctx).then((user) => { // eslint-disable-line arrow-body-style
return Group.findOne({
where: { id },
include: [{
model: User,
where: { id: user.id },
}],
}).then(group => group.update({ name }));
});
},
};
export const userLogic = {
email(user, args, ctx) {
return getAuthenticatedUser(ctx).then((currentUser) => {
if (currentUser.id === user.id) {
return currentUser.email;
}
throw new ForbiddenError('Unauthorized');
});
},
friends(user, args, ctx) {
return getAuthenticatedUser(ctx).then((currentUser) => {
if (currentUser.id !== user.id) {
throw new ForbiddenError('Unauthorized');
}
return user.getFriends({ attributes: ['id', 'username'] });
});
},
groups(user, args, ctx) {
return getAuthenticatedUser(ctx).then((currentUser) => {
if (currentUser.id !== user.id) {
throw new ForbiddenError('Unauthorized');
}
return user.getGroups();
});
},
jwt(user) {
return Promise.resolve(user.jwt);
},
messages(user, args, ctx) {
return getAuthenticatedUser(ctx).then((currentUser) => {
if (currentUser.id !== user.id) {
throw new ForbiddenError('Unauthorized');
}
return Message.findAll({
where: { userId: user.id },
order: [['createdAt', 'DESC']],
});
});
},
query(_, args, ctx) {
return getAuthenticatedUser(ctx).then((user) => {
if (user.id === args.id || user.email === args.email) {
return user;
}
throw new ForbiddenError('Unauthorized');
});
},
};

And now let’s apply that logic to the Resolvers in server/data/resolvers.js:

Step 7.9: Apply logic to all Resolvers

Changed server/data/resolvers.js

...import { Group, Message, User } from './connectors';
import { pubsub } from '../subscriptions';
import { JWT_SECRET } from '../config';
import { groupLogic, messageLogic, userLogic } from './logic';
const MESSAGE_ADDED_TOPIC = 'messageAdded';
const GROUP_ADDED_TOPIC = 'groupAdded';
... },
},
Query: {
group(_, args, ctx) {
return groupLogic.query(_, args, ctx);
},
user(_, args, ctx) {
return userLogic.query(_, args, ctx);
},
},
Mutation: {
... return message;
});
},
createGroup(_, args, ctx) {
return groupLogic.createGroup(_, args, ctx).then((group) => {
pubsub.publish(GROUP_ADDED_TOPIC, { [GROUP_ADDED_TOPIC]: group });
return group;
});
},
deleteGroup(_, args, ctx) {
return groupLogic.deleteGroup(_, args, ctx);
},
leaveGroup(_, args, ctx) {
return groupLogic.leaveGroup(_, args, ctx);
},
updateGroup(_, args, ctx) {
return groupLogic.updateGroup(_, args, ctx);
},
login(_, { email, password }, ctx) {
// find user by email
... },
},
Group: {
users(group, args, ctx) {
return groupLogic.users(group, args, ctx);
},
messages(group, args, ctx) {
return groupLogic.messages(group, args, ctx);
},
},
Message: {
to(message, args, ctx) {
return messageLogic.to(message, args, ctx);
},
from(message, args, ctx) {
return messageLogic.from(message, args, ctx);
},
},
User: {
email(user, args, ctx) {
return userLogic.email(user, args, ctx);
},
friends(user, args, ctx) {
return userLogic.friends(user, args, ctx);
},
groups(user, args, ctx) {
return userLogic.groups(user, args, ctx);
},
jwt(user, args, ctx) {
return userLogic.jwt(user, args, ctx);
},
messages(user, args, ctx) {
return userLogic.messages(user, args, ctx);
},
},
};

We also need to update our subscription filters with the user context. Fortunately, withFilter can return a Boolean or Promise<Boolean>.

Step 7.10: Apply user context to subscription filters

Changed server/data/resolvers.js

...    messageAdded: {
subscribe: withFilter(
() => pubsub.asyncIterator(MESSAGE_ADDED_TOPIC),
(payload, args, ctx) => {
return ctx.user.then((user) => {
return Boolean(
args.groupIds &&
~args.groupIds.indexOf(payload.messageAdded.groupId) &&
user.id !== payload.messageAdded.userId, // don't send to user creating message
);
});
},
),
},
groupAdded: {
subscribe: withFilter(
() => pubsub.asyncIterator(GROUP_ADDED_TOPIC),
(payload, args, ctx) => {
return ctx.user.then((user) => {
return Boolean(
args.userId &&
~map(payload.groupAdded.users, 'id').indexOf(args.userId) &&
user.id !== payload.groupAdded.users[0].id, // don't send to user creating group
);
});
},
),
},

So much cleaner and WAY more secure!

The Expired Password Problem

We still have one last thing that needs modifying in our authorization setup. When a user changes their password, we issue a new JWT, but the old JWT will still pass verification! This can become a serious problem if a hacker gets ahold of a user’s password. To close the loop on this issue, we can make a clever little adjustment to our UserModel database model to include a version parameter, which will be a counter that increments with each new password for the user. We’ll incorporate version into our JWT so only the newest JWT will pass our security. Let’s update ApolloServer and our Connectors and Resolvers accordingly:

Step 7.11: Apply versioning to JWT auth

Changed server/data/connectors.js

...  email: { type: Sequelize.STRING },
username: { type: Sequelize.STRING },
password: { type: Sequelize.STRING },
version: { type: Sequelize.INTEGER }, // version the password
});
// users belong to multiple groups... email: faker.internet.email(),
username: faker.internet.userName(),
password: hash,
version: 1,
}).then((user) => {
console.log(
'{email, username, password}',

Changed server/data/resolvers.js

...              const token = jwt.sign({
id: user.id,
email: user.email,
version: user.version,
}, JWT_SECRET);
user.jwt = token;
ctx.user = Promise.resolve(user);
... email,
password: hash,
username: username || email,
version: 1,
})).then((user) => {
const { id } = user;
const token = jwt.sign({ id, email, version: 1 }, JWT_SECRET);
user.jwt = token;
ctx.user = Promise.resolve(user);
return user;

Changed server/index.js

...        credentialsRequired: false,
})(req, res, (e) => {
if (req.user) {
resolve(User.findOne({ where: { id: req.user.id, version: req.user.version } }));
} else {
resolve(null);
}

Testing

It can’t be understated just how vital testing is to securing our code. Yet, like with most tutorials, testing is noticeably absent from this one. We’re not going to cover proper testing here because it really belongs in its own post and would make this already egregiously long post even longer.

For now, we’ll just use GraphQL Playground to make sure our code is performing as expected.

Here are the steps to test our protected GraphQL endpoint in GraphQL Playground:

  1. Use the signup or login mutation to receive a JWT
GraphQL Playground, how I missed you!

2. Apply the JWT to the Authorization Header for future requests and make whatever authorized query or mutation requests we want

We’re logged in as User 20!
We’re not logged in as User 19 — Unauthorized!
We shouldn’t have access to friends’ emails — we return nulls for types with unauthorized fields and returns errors!

JWT Authentication for Subscriptions

Our Queries and Mutations are secure, but our Subscriptions are wide open. Right now, any user could subscribe to new messages for all groups, or track when any group is created. The security we’ve already implemented limits the Message and Group fields a hacker could view, but that’s not good enough! Secure all the things!

In this workflow, we will only allow WebSocket connections once the user is authenticated. Whenever the user is logged off, we terminate the connection, and then reinitiate a new connection the next time they log in. This workflow is suitable for applications that don’t require subscriptions while the user isn’t logged in and makes it easier to defend against DOS attacks.

Just like with Queries and Mutations, we can pass a context parameter to our Subscriptions every time a user connects over WebSockets! When constructing ApolloServer, we can pass an onConnect parameter, which is a function that runs before every WebSocket connection. The onConnect function offers 2 parameters — connectionParams and webSocket — and should return a Promise that resolves the context.

connectionParams is where we will receive the JWT from the client. Inside onConnect, we will extract the User Promise from the JWT and replace return the User Promise as the context.

Let’s first update ApolloServer in server/index.js to use onConnect to validate the JWT and return a context with the User for subscriptions:

Step 7.12: Add onConnect to ApolloServer config

Changed server/index.js

...import { ApolloServer, AuthenticationError } from 'apollo-server';
import jwt from 'express-jwt';
import jsonwebtoken from 'jsonwebtoken';
import { typeDefs } from './data/schema';
import { mocks } from './data/mocks';
... // web socket subscriptions will return a connection
if (connection) {
// check connection for metadata
return connection.context;
}
const user = new Promise((resolve, reject) => {... user,
};
},
subscriptions: {
onConnect(connectionParams, websocket, wsContext) {
const userPromise = new Promise((res, rej) => {
if (connectionParams.jwt) {
jsonwebtoken.verify(
connectionParams.jwt, JWT_SECRET,
(err, decoded) => {
if (err) {
rej(new AuthenticationError('No token'));
}
res(User.findOne({ where: { id: decoded.id, version: decoded.version } }));
},
);
} else {
rej(new AuthenticationError('No token'));
}
});
return userPromise.then((user) => {
if (user) {
return { user: Promise.resolve(user) };
}
return Promise.reject(new AuthenticationError('No user'));
});
},
},
});
server.listen({ port: PORT }).then(({ url }) => console.log(`🚀 Server ready at ${url}`));

First, onConnect will use jsonwebtoken to verify and decode connectionParams.jwt to extract a User from the database. It will do this work within a new Promise called user.

Second, we need to write our subscriptionLogic to validate whether this User is allowed to subscribe to this particular subscription:

Step 7.13: Create subscriptionLogic

Changed server/data/logic.js

...    });
},
};
export const subscriptionLogic = {
groupAdded(params, args, ctx) {
return getAuthenticatedUser(ctx)
.then((user) => {
if (user.id !== args.userId) {
throw new ForbiddenError('Unauthorized');
}
return Promise.resolve();
});
},
messageAdded(params, args, ctx) {
return getAuthenticatedUser(ctx)
.then(user => user.getGroups({ where: { id: { $in: args.groupIds } }, attributes: ['id'] })
.then((groups) => {
// user attempted to subscribe to some groups without access
if (args.groupIds.length > groups.length) {
throw new ForbiddenError('Unauthorized');
}
return Promise.resolve();
}));
},
};

Finally, we need a way to run this logic when the subscription will attempt to be initiated. This happens inside our resolvers when we run pubsub.asyncIterator, returning the AsyncIterator that will listen for events and trigger our server to send WebSocket emittions. We'll need to update this AsyncIterator generator to first validate through our subscriptionLogic and throw an error if the request is unauthorized. We can create a pubsub.asyncAuthIterator function that looks like pubsub.asyncIterator, but takes an extra authPromise argument that will need to resolve before any data gets passed from the AsyncIterator this function creates.

Step 7.13: Create subscriptionLogic

Changed server/subscriptions.js

...import { $$asyncIterator } from 'iterall';
import { PubSub } from 'apollo-server';
export const pubsub = new PubSub();pubsub.asyncAuthIterator = (messages, authPromise) => {
const asyncIterator = pubsub.asyncIterator(messages);
return {
next() {
return authPromise.then(() => asyncIterator.next());
},
return() {
return authPromise.then(() => asyncIterator.return());
},
throw(error) {
return asyncIterator.throw(error);
},
[$$asyncIterator]() {
return asyncIterator;
},
};
};
export default pubsub;

We can stick this pubsub.asyncAuthIterator in our resolvers like so:

Step 7.13: Create subscriptionLogic

Changed server/data/resolvers.js

...import { Group, Message, User } from './connectors';
import { pubsub } from '../subscriptions';
import { JWT_SECRET } from '../config';
import { groupLogic, messageLogic, userLogic, subscriptionLogic } from './logic';
const MESSAGE_ADDED_TOPIC = 'messageAdded';
const GROUP_ADDED_TOPIC = 'groupAdded';
... Subscription: {
messageAdded: {
subscribe: withFilter(
(payload, args, ctx) => pubsub.asyncAuthIterator(
MESSAGE_ADDED_TOPIC,
subscriptionLogic.messageAdded(payload, args, ctx),
),
(payload, args, ctx) => {
return ctx.user.then((user) => {
return Boolean(
... },
groupAdded: {
subscribe: withFilter(
(payload, args, ctx) => pubsub.asyncAuthIterator(
GROUP_ADDED_TOPIC,
subscriptionLogic.groupAdded(payload, args, ctx),
),
(payload, args, ctx) => {
return ctx.user.then((user) => {
return Boolean(

Unfortunately, there’s no easy way to currently test subscription context with GraphQL Playground, so let’s just hope the code does what it’s supposed to do and move on for now ¯_(ツ)_/¯

Now would be a good time to take a break!

GraphQL Authentication in React Native

Our server is now only serving authenticated GraphQL, and our React Native client needs to catch up!

Designing the Layout

First, let’s design the basic authentication UI/UX for our users.

If a user isn’t authenticated, we want to push a modal Screen asking them to login or sign up and then pop the Screen when they sign in.

Let’s start by creating a Signin screen (client/src/screens/signin.screen.js) to display our login/signup modal:

Step 7.14: Create Signup Screen

Added client/src/screens/signin.screen.js

...import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {
ActivityIndicator,
KeyboardAvoidingView,
Button,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
backgroundColor: '#eeeeee',
paddingHorizontal: 50,
},
inputContainer: {
marginBottom: 20,
},
input: {
height: 40,
borderRadius: 4,
marginVertical: 6,
padding: 6,
backgroundColor: 'rgba(0,0,0,0.2)',
},
loadingContainer: {
left: 0,
right: 0,
top: 0,
bottom: 0,
position: 'absolute',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
},
switchContainer: {
flexDirection: 'row',
justifyContent: 'center',
marginTop: 12,
},
switchAction: {
paddingHorizontal: 4,
color: 'blue',
},
submit: {
marginVertical: 6,
},
});
class Signin extends Component {
static navigationOptions = {
title: 'Chatty',
headerLeft: null,
};
constructor(props) {
super(props);
this.state = {
view: 'login',
};
this.login = this.login.bind(this);
this.signup = this.signup.bind(this);
this.switchView = this.switchView.bind(this);
}
// fake for now
login() {
console.log('logging in');
this.setState({ loading: true });
setTimeout(() => {
console.log('signing up');
this.props.navigation.goBack();
}, 1000);
}
// fake for now
signup() {
console.log('signing up');
this.setState({ loading: true });
setTimeout(() => {
this.props.navigation.goBack();
}, 1000);
}
switchView() {
this.setState({
view: this.state.view === 'signup' ? 'login' : 'signup',
});
}
render() {
const { view } = this.state;
return (
<KeyboardAvoidingView
behavior={'padding'}
style={styles.container}
>
{this.state.loading ?
<View style={styles.loadingContainer}>
<ActivityIndicator />
</View> : undefined}
<View style={styles.inputContainer}>
<TextInput
onChangeText={email => this.setState({ email })}
placeholder={'Email'}
style={styles.input}
/>
<TextInput
onChangeText={password => this.setState({ password })}
placeholder={'Password'}
secureTextEntry
style={styles.input}
/>
</View>
<Button
onPress={this[view]}
style={styles.submit}
title={view === 'signup' ? 'Sign up' : 'Login'}
disabled={this.state.loading}
/>
<View style={styles.switchContainer}>
<Text>
{ view === 'signup' ?
'Already have an account?' : 'New to Chatty?' }
</Text>
<TouchableOpacity
onPress={this.switchView}
>
<Text style={styles.switchAction}>
{view === 'login' ? 'Sign up' : 'Login'}
</Text>
</TouchableOpacity>
</View>
</KeyboardAvoidingView>
);
}
}
Signin.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.func,
}),
};
export default Signin;

Next, we’ll add Signin to our Navigation. We'll also make sure the USER_QUERY attached to AppWithNavigationState gets skipped and doesn't query for anything for now. We don’t want to run any queries until a user officially signs in. Right now, we’re just testing the layout, so we don’t want queries to run at all no matter what. graphql let’s us pass a skip function as an optional parameter to our queries to skip their execution. We can update the code in client/src/navigation.js as follows:

Step 7.15: Add Signin to navigation and skip queries

Changed client/src/navigation.js

...import FinalizeGroup from './screens/finalize-group.screen';
import GroupDetails from './screens/group-details.screen';
import NewGroup from './screens/new-group.screen';
import Signin from './screens/signin.screen';
import { USER_QUERY } from './graphql/user.query';
import MESSAGE_ADDED_SUBSCRIPTION from './graphql/message-added.subscription';
...
const AppNavigator = StackNavigator({
Main: { screen: MainScreenNavigator },
Signin: { screen: Signin },
Messages: { screen: Messages },
GroupDetails: { screen: GroupDetails },
NewGroup: { screen: NewGroup },
...});const userQuery = graphql(USER_QUERY, {
skip: ownProps => true, // fake it -- we'll use ownProps with auth
options: () => ({ variables: { id: 1 } }), // fake the user for now
props: ({ data: { loading, user, refetch, subscribeToMore } }) => ({
loading,

Lastly, we need to modify the Groups screen to push the Signin modal and skip querying for anything:

Step 7.15: Add Signin to navigation and skip queries

Changed client/src/screens/groups.screen.js

...  onPress: PropTypes.func.isRequired,
};
// we'll fake signin for now
let IS_SIGNED_IN = false;
class Group extends Component {
constructor(props) {
super(props);
... this.onRefresh = this.onRefresh.bind(this);
}
componentDidMount() {
if (!IS_SIGNED_IN) {
IS_SIGNED_IN = true;
const { navigate } = this.props.navigation; navigate('Signin');
}
}
onRefresh() {
this.props.refetch();
// faking unauthorized status
}
keyExtractor = item => item.id.toString();...};const userQuery = graphql(USER_QUERY, {
skip: ownProps => true, // fake it -- we'll use ownProps with auth
options: () => ({ variables: { id: 1 } }), // fake the user for now
props: ({ data: { loading, networkStatus, refetch, user } }) => ({
loading, networkStatus, refetch, user,

Let’s test out our layout:

Personally, I’m more a fan of limpbiscuit4eva

Persisted authentication with React Native and Redux

Time to add authentication infrastructure to our React Native client! When a user signs up or logs in, the server is going to return a JWT. Whenever the client makes a GraphQL HTTP request to the server, it needs to pass the JWT in the Authorization Header to verify the request is being sent by the user.

Once we have a JWT, we can use it forever or until we set it to expire. Therefore, we want to store the JWT in our app’s storage so users don’t have to log in every time they restart the app . We’re also going to want quick access to the JWT for any GraphQL request while the user is active. We can use a combination of redux, redux-persist, and AsyncStorage to efficiently meet all our demands!

# make sure you add this package to the client!!!
cd client
npm i redux redux-persist redux-thunk seamless-immutable

redux is the BOMB. If you don’t know Redux, learn Redux!

redux-persist is an incredible package which let’s us store Redux state in a bunch of different storage engines and rehydrate our Redux store when we restart our app.

redux-thunk will let us return functions and use Promises to dispatch Redux actions.

seamless-immutable will help us use Immutable JS data structures within Redux that are backwards-compatible with normal Arrays and Objects.

First, let’s create a reducer for our auth data. We’ll create a new folder client/src/reducers for our reducer files to live and create a new file client/src/reducers/auth.reducer.js for the auth reducer:

Step 7.16: Create auth reducer

Added client/src/reducers/auth.reducer.js

...import Immutable from 'seamless-immutable';const initialState = Immutable({
loading: true,
});
const auth = (state = initialState, action) => {
switch (action.type) {
default:
return state;
}
};
export default auth;

The initial state for store.auth will be { loading: true }. We can combine the auth reducer into our store in client/src/app.js:

Step 7.17: Combine auth reducer with reducers

Changed client/src/app.js

...  navigationReducer,
navigationMiddleware,
} from './navigation';
import auth from './reducers/auth.reducer';
const URL = 'localhost:8080'; // set your comp's url here... combineReducers({
apollo: apolloReducer,
nav: navigationReducer,
auth,
}),
{}, // initial state
composeWithDevTools(

Now let’s add thunk middleware and persistence with redux-persist and AsyncStorage to our store in client/src/app.js:

Step 7.18: Add persistent storage

Changed client/src/app.js

...import React, { Component } from 'react';
import {
AsyncStorage,
} from 'react-native';
import { ApolloClient } from 'apollo-client';
import { ApolloLink } from 'apollo-link';
...import { WebSocketLink } from 'apollo-link-ws';
import { getMainDefinition } from 'apollo-utilities';
import { SubscriptionClient } from 'subscriptions-transport-ws';
import { PersistGate } from 'redux-persist/lib/integration/react';
import { persistStore, persistCombineReducers } from 'redux-persist';
import thunk from 'redux-thunk';
import AppWithNavigationState, {
navigationReducer,
...
const URL = 'localhost:8080'; // set your comp's url hereconst config = {
key: 'root',
storage: AsyncStorage,
blacklist: ['nav', 'apollo'], // don't persist nav for now
};
const reducer = persistCombineReducers(config, {
apollo: apolloReducer,
nav: navigationReducer,
auth,
});
const store = createStore(
reducer,
{}, // initial state
composeWithDevTools(
applyMiddleware(thunk, navigationMiddleware),
),
);
// persistent storage
const persistor = persistStore(store);
const cache = new ReduxCache({ store });const reduxLink = new ReduxLink(store);... return (
<ApolloProvider client={client}>
<Provider store={store}>
<PersistGate persistor={persistor}>
<AppWithNavigationState />
</PersistGate>
</Provider>
</ApolloProvider>
);

We have set our store data (excluding apollo) to persist via React Native’s AsyncStorage and to automatically rehydrate the store when the client restarts the app. When the app restarts, a REHYDRATE action will execute asyncronously with all the data persisted from the last session. We need to handle that action and properly update our store in our auth reducer:

Step 7.19: Handle rehydration in auth reducer

Changed client/src/reducers/auth.reducer.js

...import { REHYDRATE } from 'redux-persist';
import Immutable from 'seamless-immutable';
const initialState = Immutable({...
const auth = (state = initialState, action) => {
switch (action.type) {
case REHYDRATE:
// convert persisted data to Immutable and confirm rehydration
return Immutable(action.payload.auth || state)
.set('loading', false);
default:
return state;
}

The auth state will be { loading: true } until we rehydrate our persisted state.

When the user successfully signs up or logs in, we need to store the user’s id and their JWT within auth. We also need to clear this information when they log out. Let’s create a constants folder client/src/constants and file client/src/constants/constants.jswhere we can start declaring Redux action types and write two for setting the current user and logging out:

Step 7.20: Create constants

Added client/src/constants/constants.js

...// auth constants
export const LOGOUT = 'LOGOUT';
export const SET_CURRENT_USER = 'SET_CURRENT_USER';

We can add these constants to our auth reducer now:

Step 7.21: Handle login/logout in auth reducer

Changed client/src/reducers/auth.reducer.js

...import { REHYDRATE } from 'redux-persist';
import Immutable from 'seamless-immutable';
import { LOGOUT, SET_CURRENT_USER } from '../constants/constants';const initialState = Immutable({
loading: true,
});
... switch (action.type) {
case REHYDRATE:
// convert persisted data to Immutable and confirm rehydration
const { payload = {} } = action;
return Immutable(payload.auth || state)
.set('loading', false);
case SET_CURRENT_USER:
return state.merge(action.user);
case LOGOUT:
return Immutable({ loading: false });
default:
return state;
}

The SET_CURRENT_USER and LOGOUT action types will need to get triggered by ActionCreators. Let’s put those in a new folder client/src/actions and a new file client/src/actions/auth.actions.js:

Step 7.22: Create auth actions

Added client/src/actions/auth.actions.js

...import { client } from '../app';
import { SET_CURRENT_USER, LOGOUT } from '../constants/constants';
export const setCurrentUser = user => ({
type: SET_CURRENT_USER,
user,
});
export const logout = () => {
client.resetStore();
return { type: LOGOUT };
};

When logout is called, we’ll clear all auth data by dispatching LOGOUT and also all data in the apollo store by calling client.resetStore.

Let’s tie everything together. We’ll update the Signin screen to use our login and signup mutations, and dispatch setCurrentUser with the mutation results (the JWT and user’s id).

First we’ll create files for our login and signup mutations:

Step 7.23: Create login and signup mutations

Added client/src/graphql/login.mutation.js

...import gql from 'graphql-tag';const LOGIN_MUTATION = gql`
mutation login($email: String!, $password: String!) {
login(email: $email, password: $password) {
id
jwt
username
}
}
`;
export default LOGIN_MUTATION;

Added client/src/graphql/signup.mutation.js

...import gql from 'graphql-tag';const SIGNUP_MUTATION = gql`
mutation signup($email: String!, $password: String!) {
signup(email: $email, password: $password) {
id
jwt
username
}
}
`;
export default SIGNUP_MUTATION;

We connect these mutations and our Redux store to the Signin component with compose and connect:

Step 7.24: Add login and signup mutations to Signin screen

Changed client/src/screens/signin.screen.js

...import PropTypes from 'prop-types';
import {
ActivityIndicator,
Alert,
KeyboardAvoidingView,
Button,
StyleSheet,
... TouchableOpacity,
View,
} from 'react-native';
import { graphql, compose } from 'react-apollo';
import { connect } from 'react-redux';
import {
setCurrentUser,
} from '../actions/auth.actions';
import LOGIN_MUTATION from '../graphql/login.mutation';
import SIGNUP_MUTATION from '../graphql/signup.mutation';
const styles = StyleSheet.create({
container: {
... },
});
function capitalizeFirstLetter(string) {
return string[0].toUpperCase() + string.slice(1);
}
class Signin extends Component {
static navigationOptions = {
title: 'Chatty',
...
constructor(props) {
super(props);
if (props.auth && props.auth.jwt) {
props.navigation.goBack();
}
this.state = {
view: 'login',
};
... this.switchView = this.switchView.bind(this);
}
componentWillReceiveProps(nextProps) {
if (nextProps.auth.jwt) {
nextProps.navigation.goBack();
}
}
login() {
const { email, password } = this.state;
this.setState({
loading: true,
});
this.props.login({ email, password })
.then(({ data: { login: user } }) => {
this.props.dispatch(setCurrentUser(user));
this.setState({
loading: false,
});
}).catch((error) => {
this.setState({
loading: false,
});
Alert.alert(
`${capitalizeFirstLetter(this.state.view)} error`,
error.message,
[
{ text: 'OK', onPress: () => console.log('OK pressed') }, // eslint-disable-line no-console
{ text: 'Forgot password', onPress: () => console.log('Forgot Pressed'), style: 'cancel' }, // eslint-disable-line no-console
],
);
});
}
signup() {
this.setState({
loading: true,
});
const { email, password } = this.state;
this.props.signup({ email, password })
.then(({ data: { signup: user } }) => {
this.props.dispatch(setCurrentUser(user));
this.setState({
loading: false,
});
}).catch((error) => {
this.setState({
loading: false,
});
Alert.alert(
`${capitalizeFirstLetter(this.state.view)} error`,
error.message,
[{ text: 'OK', onPress: () => console.log('OK pressed') }], // eslint-disable-line no-console
);
});
}
switchView() {... onPress={this[view]}
style={styles.submit}
title={view === 'signup' ? 'Sign up' : 'Login'}
disabled={this.state.loading || !!this.props.auth.jwt}
/>
<View style={styles.switchContainer}>
<Text>
... navigation: PropTypes.shape({
goBack: PropTypes.func,
}),
auth: PropTypes.shape({
loading: PropTypes.bool,
jwt: PropTypes.string,
}),
dispatch: PropTypes.func.isRequired,
login: PropTypes.func.isRequired,
signup: PropTypes.func.isRequired,
};
const login = graphql(LOGIN_MUTATION, {
props: ({ mutate }) => ({
login: ({ email, password }) =>
mutate({
variables: { email, password },
}),
}),
});
const signup = graphql(SIGNUP_MUTATION, {
props: ({ mutate }) => ({
signup: ({ email, password }) =>
mutate({
variables: { email, password },
}),
}),
});
const mapStateToProps = ({ auth }) => ({
auth,
});
export default compose(
login,
signup,
connect(mapStateToProps),
)(Signin);

We attached auth from our Redux store to Signin via connect(mapStateToProps). When we sign up or log in, we call the associated mutation (signup or login), receive the JWT and id, and dispatch the data with setCurrentUser. In componentWillReceiveProps, once auth.jwt exists, we are logged in and pop the Screen. We’ve also included some simple error messages if things go wrong.

Let’s check it out!

woot woot!

Apollo-Client Authentication Middleware

We need to add Authorization Headers to our GraphQL requests from React Native before we can resume retrieving data from our auth protected server. We accomplish this by using middleware that will attach the headers to every request before they are sent out. Middleware works very elegantly within the apollo-link ecosystem. We just need to add a couple new links! Fortunately, apollo-link-context and apollo-link-error are perfect for our requirements and work really nicely with our Redux setup. We can simply add the following in client/src/app.js:

npm i apollo-link-context apollo-link-error

Step 7.25: Add authentication middleware for requests

Changed client/package.json

...		"apollo-cache-redux": "^0.1.0-alpha.7",
"apollo-client": "^2.2.5",
"apollo-link": "^1.1.0",
"apollo-link-context": "^1.0.5",
"apollo-link-error": "^1.0.7",
"apollo-link-http": "^1.3.3",
"apollo-link-redux": "^0.2.1",

Changed client/src/app.js

...import { PersistGate } from 'redux-persist/lib/integration/react';
import { persistStore, persistCombineReducers } from 'redux-persist';
import thunk from 'redux-thunk';
import { setContext } from 'apollo-link-context';
import _ from 'lodash';
import AppWithNavigationState, {
navigationReducer,
navigationMiddleware,
} from './navigation';
import auth from './reducers/auth.reducer';
import { logout } from './actions/auth.actions';
const URL = 'localhost:8080'; // set your comp's url here...
const httpLink = createHttpLink({ uri: `http://${URL}` });// middleware for requests
const middlewareLink = setContext((req, previousContext) => {
// get the authentication token from local storage if it exists
const { jwt } = store.getState().auth;
if (jwt) {
return {
headers: {
authorization: `Bearer ${jwt}`,
},
};
}
return previousContext;
});
// Create WebSocket client
export const wsClient = new SubscriptionClient(`ws://${URL}/graphql`, {
reconnect: true,
... reduxLink,
errorLink,
requestLink({
queryOrMutationLink: middlewareLink.concat(httpLink),
subscriptionLink: webSocketLink,
}),
]);

Before every request, we get the JWT from auth and stick it in the header. We can also run middleware after receiving responses to check for auth errors and log out the user if necessary (afterware?):

Step 7.26: Add authentication afterware for responses

Changed client/src/app.js

...
const reduxLink = new ReduxLink(store);const httpLink = createHttpLink({ uri: `http://${URL}` });// middleware for requests... return previousContext;
});
// afterware for responses
const errorLink = onError(({ graphQLErrors, networkError }) => {
let shouldLogout = false;
if (graphQLErrors) {
console.log({ graphQLErrors });
graphQLErrors.forEach(({ message, locations, path }) => {
console.log({ message, locations, path });
if (message === 'Unauthorized') {
shouldLogout = true;
}
});
if (shouldLogout) {
store.dispatch(logout());
}
}
if (networkError) {
console.log('[Network error]:');
console.log({ networkError });
if (networkError.statusCode === 401) {
logout();
}
}
});
// Create WebSocket client
export const wsClient = new SubscriptionClient(`ws://${URL}/graphql`, {
reconnect: true,

We simply parse the error and dispatch logout() if we receive an Unauthorized response message.

Subscriptions-Transport-WS Authentication

Luckily for us, SubscriptionClient has a nifty little feature that lets us lazily (on-demand) connect to our WebSocket by setting lazy: true. This flag means we will only try to connect the WebSocket when we make our first subscription call, which only happens in our app once the user is authenticated. When we make our connection call, we can pass the JWT credentials via connectionParams. When the user logs out, we’ll close the connection and lazily reconnect when a user log back in and resubscribes.

We can update client/src/app.js and client/actions/auth.actions.js as follows:

Step 7.27: Add lazy connecting to wsClient

Changed client/src/actions/auth.actions.js

...import { client, wsClient } from '../app';
import { SET_CURRENT_USER, LOGOUT } from '../constants/constants';
export const setCurrentUser = user => ({...
export const logout = () => {
client.resetStore();
wsClient.unsubscribeAll(); // unsubscribe from all subscriptions
wsClient.close(); // close the WebSocket connection
return { type: LOGOUT };
};

Changed client/src/app.js

...
// Create WebSocket client
export const wsClient = new SubscriptionClient(`ws://${URL}/graphql`, {
lazy: true,
reconnect: true,
connectionParams() {
// get the authentication token from local storage if it exists
return { jwt: store.getState().auth.jwt };
},
});

KaBLaM! We’re ready to start using auth across our app!

Refactoring the Client for Authentication

Our final major hurdle is going to be refactoring all our client code to use the Queries and Mutations we modified for auth and to handle auth UI.

Logout

To get our feet wet, let’s start by creating a new Screen instead of fixing up an existing one. Let’s create a new Screen for the Settings tab where we will show the current user’s details and give users the option to log out!

We’ll put our new Settings Screen in a new file client/src/screens/settings.screen.js:

Step 7.28: Create Settings Screen

Added client/src/screens/settings.screen.js

...import PropTypes from 'prop-types';
import React, { Component } from 'react';
import {
ActivityIndicator,
Button,
Image,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
import { connect } from 'react-redux';
import { graphql, compose } from 'react-apollo';
import USER_QUERY from '../graphql/user.query';
import { logout } from '../actions/auth.actions';
const styles = StyleSheet.create({
container: {
flex: 1,
},
email: {
borderColor: '#777',
borderBottomWidth: 1,
borderTopWidth: 1,
paddingVertical: 8,
paddingHorizontal: 16,
fontSize: 16,
},
emailHeader: {
backgroundColor: '#dbdbdb',
color: '#777',
paddingHorizontal: 16,
paddingBottom: 6,
paddingTop: 32,
fontSize: 12,
},
loading: {
justifyContent: 'center',
flex: 1,
},
userImage: {
width: 54,
height: 54,
borderRadius: 27,
},
imageContainer: {
paddingRight: 20,
alignItems: 'center',
},
input: {
color: 'black',
height: 32,
},
inputBorder: {
borderColor: '#dbdbdb',
borderBottomWidth: 1,
borderTopWidth: 1,
paddingVertical: 8,
},
inputInstructions: {
paddingTop: 6,
color: '#777',
fontSize: 12,
flex: 1,
},
userContainer: {
paddingLeft: 16,
},
userInner: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 16,
paddingRight: 16,
},
});
class Settings extends Component {
static navigationOptions = {
title: 'Settings',
};
constructor(props) {
super(props);
this.state = {}; this.logout = this.logout.bind(this);
}
logout() {
this.props.dispatch(logout());
}
// eslint-disable-next-line
updateUsername(username) {
// eslint-disable-next-line
console.log('TODO: update username');
}
render() {
const { loading, user } = this.props;
// render loading placeholder while we fetch data
if (loading || !user) {
return (
<View style={[styles.loading, styles.container]}>
<ActivityIndicator />
</View>
);
}
return (
<View style={styles.container}>
<View style={styles.userContainer}>
<View style={styles.userInner}>
<TouchableOpacity style={styles.imageContainer}>
<Image
style={styles.userImage}
source={{ uri: 'https://reactjs.org/logo-og.png' }}
/>
<Text>edit</Text>
</TouchableOpacity>
<Text style={styles.inputInstructions}>
Enter your name and add an optional profile picture
</Text>
</View>
<View style={styles.inputBorder}>
<TextInput
onChangeText={username => this.setState({ username })}
placeholder={user.username}
style={styles.input}
defaultValue={user.username}
/>
</View>
</View>
<Text style={styles.emailHeader}>EMAIL</Text>
<Text style={styles.email}>{user.email}</Text>
<Button title="Logout" onPress={this.logout} />
</View>
);
}
}
Settings.propTypes = {
auth: PropTypes.shape({
loading: PropTypes.bool,
jwt: PropTypes.string,
}).isRequired,
dispatch: PropTypes.func.isRequired,
loading: PropTypes.bool,
navigation: PropTypes.shape({
navigate: PropTypes.func,
}),
user: PropTypes.shape({
username: PropTypes.string,
}),
};
const userQuery = graphql(USER_QUERY, {
skip: ownProps => !ownProps.auth || !ownProps.auth.jwt,
options: ({ auth }) => ({ variables: { id: auth.id }, fetchPolicy: 'cache-only' }),
props: ({ data: { loading, user } }) => ({
loading, user,
}),
});
const mapStateToProps = ({ auth }) => ({
auth,
});
export default compose(
connect(mapStateToProps),
userQuery,
)(Settings);

The most important pieces of this code we need to focus on is any auth related code:

  1. We connect auth from our Redux store to the component via connect(mapStateToProps)
  2. We skip the userQuery unless we have a JWT (ownProps.auth.jwt)
  3. We show a loading spinner until we’re done loading the user

Let’s add the Settings screen to our settings tab in client/src/navigation.js. We will also use navigationReducer to handle pushing the Signin Screen whenever the user logs out or starts the application without being authenticated:

Step 7.29: Add Settings screen and auth logic to Navigation

Changed client/src/navigation.js

...import update from 'immutability-helper';
import { map } from 'lodash';
import { Buffer } from 'buffer';
import { REHYDRATE } from 'redux-persist';
import Groups from './screens/groups.screen';
import Messages from './screens/messages.screen';
...import GroupDetails from './screens/group-details.screen';
import NewGroup from './screens/new-group.screen';
import Signin from './screens/signin.screen';
import Settings from './screens/settings.screen';
import { USER_QUERY } from './graphql/user.query';
import MESSAGE_ADDED_SUBSCRIPTION from './graphql/message-added.subscription';
...
import { wsClient } from './app';import { LOGOUT } from './constants/constants';// tabs in main screen
const MainScreenNavigator = TabNavigator({
Chats: { screen: Groups },
Settings: { screen: Settings },
}, {
initialRouteName: 'Chats',
});
... ],
}));
// reducer code
export const navigationReducer = (state = initialState, action) => {
let nextState = AppNavigator.router.getStateForAction(action, state);
switch (action.type) {
case REHYDRATE:
// convert persisted data to Immutable and confirm rehydration
if (!action.payload || !action.payload.auth || !action.payload.auth.jwt) {
const { routes, index } = state;
if (routes[index].routeName !== 'Signin') {
nextState = AppNavigator.router.getStateForAction(
NavigationActions.navigate({ routeName: 'Signin' }),
state,
);
}
}
break;
case LOGOUT:
const { routes, index } = state;
if (routes[index].routeName !== 'Signin') {
nextState = AppNavigator.router.getStateForAction(
NavigationActions.navigate({ routeName: 'Signin' }),
state,
);
}
break;
default:
nextState = AppNavigator.router.getStateForAction(action, state);
break;
}
// Simply return the original `state` if `nextState` is null or undefined.
return nextState || state;

Though it’s typically best practice to keep reducers pure (not triggering actions directly), we’ve made an exception with NavigationActions in our navigationReducer to keep the code a little simpler in this particular case.

Let’s run it!

So close to the finish line!

Refactoring Queries and Mutations

We need to update all our client-side Queries and Mutations to match our modified Schema. We also need to update the variables we pass to these Queries and Mutations through graphql and attach to components.

Let’s look at the USER_QUERY in Groups and AppWithNavigationState for a full example:

Step 7.30: Update userQuery with auth in Groups and Navigation

Changed client/src/navigation.js

...      }, this);
}
if (nextProps.user && nextProps.user.id === nextProps.auth.id &&
(!this.props.user || nextProps.user.groups.length !== this.props.user.groups.length)) {
// unsubscribe from old
...}AppWithNavigationState.propTypes = {
auth: PropTypes.shape({
id: PropTypes.number,
jwt: PropTypes.string,
}),
dispatch: PropTypes.func.isRequired,
nav: PropTypes.object.isRequired,
refetch: PropTypes.func,
... }),
};
const mapStateToProps = ({ auth, nav }) => ({
auth,
nav,
});
const userQuery = graphql(USER_QUERY, {
skip: ownProps => !ownProps.auth || !ownProps.auth.jwt,
options: ownProps => ({ variables: { id: ownProps.auth.id } }),
props: ({ data: { loading, user, refetch, subscribeToMore } }) => ({
loading,
user,

Changed client/src/screens/groups.screen.js

...  TouchableHighlight,
View,
} from 'react-native';
import { graphql, compose } from 'react-apollo';
import moment from 'moment';
import Icon from 'react-native-vector-icons/FontAwesome';
import { connect } from 'react-redux';
import { USER_QUERY } from '../graphql/user.query';... onPress: PropTypes.func.isRequired,
};
class Group extends Component {
constructor(props) {
super(props);
... this.onRefresh = this.onRefresh.bind(this);
}
onRefresh() {
this.props.refetch();
// faking unauthorized status
...};const userQuery = graphql(USER_QUERY, {
skip: ownProps => !ownProps.auth || !ownProps.auth.jwt,
options: ownProps => ({ variables: { id: ownProps.auth.id } }),
props: ({ data: { loading, networkStatus, refetch, user } }) => ({
loading, networkStatus, refetch, user,
}),
});
const mapStateToProps = ({ auth }) => ({
auth,
});
export default compose(
connect(mapStateToProps),
userQuery,
)(Groups);
  1. We use connect(mapStateToProps) to attach auth from Redux to our component
  2. We modify the userQuery options to pass ownProps.auth.id instead of the 1 placeholder
  3. We change skip to use ownProps.auth.jwt to determine whether to run userQuery

We’ll also have to make similar changes in Messages:

Step 7.31: Update Messages Screen and createMessage with auth

Changed client/src/graphql/create-message.mutation.js

...import MESSAGE_FRAGMENT from './message.fragment';const CREATE_MESSAGE_MUTATION = gql`
mutation createMessage($text: String!, $groupId: Int!) {
createMessage(text: $text, groupId: $groupId) {
... MessageFragment
}
}

Changed client/src/screens/messages.screen.js

...import { Buffer } from 'buffer';
import _ from 'lodash';
import moment from 'moment';
import { connect } from 'react-redux';
import { wsClient } from '../app';... send(text) {
this.props.createMessage({
groupId: this.props.navigation.state.params.groupId,
text,
}).then(() => {
this.flatList.scrollToIndex({ index: 0, animated: true });
... return (
<Message
color={this.state.usernameColors[message.from.username]}
isCurrentUser={message.from.id === this.props.auth.id}
message={message}
/>
);
...}Messages.propTypes = {
auth: PropTypes.shape({
id: PropTypes.number,
username: PropTypes.string,
}),
createMessage: PropTypes.func,
navigation: PropTypes.shape({
navigate: PropTypes.func,
...});const createMessageMutation = graphql(CREATE_MESSAGE_MUTATION, {
props: ({ ownProps, mutate }) => ({
createMessage: ({ text, groupId }) =>
mutate({
variables: { text, groupId },
optimisticResponse: {
__typename: 'Mutation',
createMessage: {
... createdAt: new Date().toISOString(), // the time is now!
from: {
__typename: 'User',
id: ownProps.auth.id,
username: ownProps.auth.username,
},
to: {
__typename: 'Group',
... const userData = store.readQuery({
query: USER_QUERY,
variables: {
id: ownProps.auth.id,
},
});
... store.writeQuery({
query: USER_QUERY,
variables: {
id: ownProps.auth.id,
},
data: userData,
});
... }),
});
const mapStateToProps = ({ auth }) => ({
auth,
});
export default compose(
connect(mapStateToProps),
groupQuery,
createMessageMutation,
)(Messages);

We need to make similar changes in every other one of our components before we’re bug free. Here are all the major changes:

Step 7.32: Update Groups flow with auth

Changed client/src/graphql/create-group.mutation.js

...import MESSAGE_FRAGMENT from './message.fragment';const CREATE_GROUP_MUTATION = gql`
mutation createGroup($name: String!, $userIds: [Int!]) {
createGroup(name: $name, userIds: $userIds) {
id
name
users {

Changed client/src/graphql/leave-group.mutation.js

...import gql from 'graphql-tag';const LEAVE_GROUP_MUTATION = gql`
mutation leaveGroup($id: Int!) {
leaveGroup(id: $id) {
id
}
}

Changed client/src/screens/finalize-group.screen.js

...import { graphql, compose } from 'react-apollo';
import { NavigationActions } from 'react-navigation';
import update from 'immutability-helper';
import { connect } from 'react-redux';
import { USER_QUERY } from '../graphql/user.query';
import CREATE_GROUP_MUTATION from '../graphql/create-group.mutation';
...
createGroup({
name: this.state.name,
userIds: _.map(this.state.selected, 'id'),
}).then((res) => {
this.props.navigation.dispatch(goToNewGroup(res.data.createGroup));
...};const createGroupMutation = graphql(CREATE_GROUP_MUTATION, {
props: ({ ownProps, mutate }) => ({
createGroup: ({ name, userIds }) =>
mutate({
variables: { name, userIds },
update: (store, { data: { createGroup } }) => {
// Read the data from our cache for this query.
const data = store.readQuery({ query: USER_QUERY, variables: { id: ownProps.auth.id } });
// Add our message from the mutation to the end.
data.user.groups.push(createGroup);
... // Write our data back to the cache.
store.writeQuery({
query: USER_QUERY,
variables: { id: ownProps.auth.id },
data,
});
},
... }),
});
const mapStateToProps = ({ auth }) => ({
auth,
});
export default compose(
connect(mapStateToProps),
userQuery,
createGroupMutation,
)(FinalizeGroup);

Changed client/src/screens/group-details.screen.js

...} from 'react-native';
import { graphql, compose } from 'react-apollo';
import { NavigationActions } from 'react-navigation';
import { connect } from 'react-redux';
import GROUP_QUERY from '../graphql/group.query';
import USER_QUERY from '../graphql/user.query';
... leaveGroup() {
this.props.leaveGroup({
id: this.props.navigation.state.params.id,
})
.then(() => {
this.props.navigation.dispatch(resetAction);
})
... variables: { id },
update: (store, { data: { deleteGroup } }) => {
// Read the data from our cache for this query.
const data = store.readQuery({ query: USER_QUERY, variables: { id: ownProps.auth.id } });
// Add our message from the mutation to the end.
data.user.groups = data.user.groups.filter(g => deleteGroup.id !== g.id);
... // Write our data back to the cache.
store.writeQuery({
query: USER_QUERY,
variables: { id: ownProps.auth.id },
data,
});
},
...
const leaveGroupMutation = graphql(LEAVE_GROUP_MUTATION, {
props: ({ ownProps, mutate }) => ({
leaveGroup: ({ id }) =>
mutate({
variables: { id },
update: (store, { data: { leaveGroup } }) => {
// Read the data from our cache for this query.
const data = store.readQuery({ query: USER_QUERY, variables: { id: ownProps.auth.id } });
// Add our message from the mutation to the end.
data.user.groups = data.user.groups.filter(g => leaveGroup.id !== g.id);
... // Write our data back to the cache.
store.writeQuery({
query: USER_QUERY,
variables: { id: ownProps.auth.id },
data,
});
},
... }),
});
const mapStateToProps = ({ auth }) => ({
auth,
});
export default compose(
connect(mapStateToProps),
groupQuery,
deleteGroupMutation,
leaveGroupMutation,

Changed client/src/screens/new-group.screen.js

...import AlphabetListView from 'react-native-alpha-listview';
import update from 'immutability-helper';
import Icon from 'react-native-vector-icons/FontAwesome';
import { connect } from 'react-redux';
import SelectedUserList from '../components/selected-user-list.component';
import USER_QUERY from '../graphql/user.query';
...};const userQuery = graphql(USER_QUERY, {
options: ownProps => ({ variables: { id: ownProps.auth.id } }),
props: ({ data: { loading, user } }) => ({
loading, user,
}),
});
const mapStateToProps = ({ auth }) => ({
auth,
});
export default compose(
connect(mapStateToProps),
userQuery,
)(NewGroup);

Step 7.33: Update messageAdded flow with auth

Changed client/src/graphql/message-added.subscription.js

...import MESSAGE_FRAGMENT from './message.fragment';const MESSAGE_ADDED_SUBSCRIPTION = gql`
subscription onMessageAdded($groupIds: [Int]){
messageAdded(groupIds: $groupIds){
... MessageFragment
}
}

Changed client/src/navigation.js

...      return subscribeToMore({
document: MESSAGE_ADDED_SUBSCRIPTION,
variables: {
groupIds: map(user.groups, 'id'),
},
updateQuery: (previousResult, { subscriptionData }) => {

Changed client/src/screens/messages.screen.js

...        this.subscription = nextProps.subscribeToMore({
document: MESSAGE_ADDED_SUBSCRIPTION,
variables: {
groupIds: [nextProps.navigation.state.params.groupId],
},
updateQuery: (previousResult, { subscriptionData }) => {

When everything is said and done, we should have a beautifully running Chatty app 📱‼️‼️

SUCH WOW!!!

🎉 CONGRATULATIONS!!! 🎉

We made it! We made a secure, real-time chat app with React Native and GraphQL. How cool is that?! More importantly, we now have the skills and knowhow to make pretty much anything we want with some of the best tools out there.

I hope this series has been at least a little helpful in furthering your growth as a developer. I’m really stoked and humbled at the reception it has been getting, and I want to continue to do everything I can to make it the best it can be.

With that in mind, if you have any suggestions for making this series better, please leave your feedback!

You can follow me to stay tuned for future posts beyond this core series where we will add more complex features to Chatty like push notifications, file uploads, and query optimizations.

You can view the code for this tutorial here 🍻

Continue to Building Chatty — Part 8 (GraphQL Input Types)

--

--