# Step 7: GraphQL Authentication [//]: # (head-end) This is the seventh blog in a multipart series where we will be building Chatty, a WhatsApp clone, using [React Native](https://facebook.github.io/react-native/) and [Apollo](http://dev.apollodata.com/). 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)**](https://jwt.io/introduction/) 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)](http://jwt.io) is an open standard ([RFC 7519](https://tools.ietf.org/html/rfc7519)) 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](https://jwt.io/introduction/). 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: , password: }` 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 `). The server will decode the Authorization Header before executing every request, and the decoded JWT should contain `{email: , password: }`. 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`](https://www.npmjs.com/package/express-jwt) and [`jsonwebtoken`](https://github.com/auth0/node-jsonwebtoken) packages for all our JWT encoding/decoding needs. We’re also going to use [`bcrypt`](https://www.npmjs.com/package/bcrypt) for hashing passwords and [`dotenv`](https://www.npmjs.com/package/dotenv) to set our JWT secret key as an environment variable: ```sh npm i express-jwt jsonwebtoken bcrypt dotenv ``` In a new `.env` file on the root directory, let’s add a `JWT_SECRET` environment variable: [{]: (diffStep 7.1 files=".env") #### [Step 7.1: Add environment variables for JWT_SECRET](https://github.com/srtucker22/chatty/commit/46a72bf) ##### Added .env ```diff @@ -0,0 +1,3 @@ +┊ ┊1┊# .env +┊ ┊2┊# use your own secret!!! +┊ ┊3┊JWT_SECRET=your_secret🚫↵ ``` [}]: # We’ll process the `JWT_SECRET` inside a new file `server/config.js`: [{]: (diffStep 7.1 files="server/config.js") #### [Step 7.1: Add environment variables for JWT_SECRET](https://github.com/srtucker22/chatty/commit/46a72bf) ##### Added server/config.js ```diff @@ -0,0 +1,19 @@ +┊ ┊ 1┊import dotenv from 'dotenv'; +┊ ┊ 2┊ +┊ ┊ 3┊dotenv.config({ silent: true }); +┊ ┊ 4┊ +┊ ┊ 5┊export const { +┊ ┊ 6┊ JWT_SECRET, +┊ ┊ 7┊} = process.env; +┊ ┊ 8┊ +┊ ┊ 9┊const defaults = { +┊ ┊10┊ JWT_SECRET: 'your_secret', +┊ ┊11┊}; +┊ ┊12┊ +┊ ┊13┊Object.keys(defaults).forEach((key) => { +┊ ┊14┊ if (!process.env[key] || process.env[key] === defaults[key]) { +┊ ┊15┊ throw new Error(`Please enter a custom ${key} in .env on the root directory`); +┊ ┊16┊ } +┊ ┊17┊}); +┊ ┊18┊ +┊ ┊19┊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`: [{]: (diffStep 7.2) #### [Step 7.2: Add jwt middleware to express](https://github.com/srtucker22/chatty/commit/346363c) ##### Changed server/index.js ```diff @@ -1,7 +1,11 @@ ┊ 1┊ 1┊import { ApolloServer } from 'apollo-server'; +┊ ┊ 2┊import jwt from 'express-jwt'; +┊ ┊ 3┊ ┊ 2┊ 4┊import { typeDefs } from './data/schema'; ┊ 3┊ 5┊import { mocks } from './data/mocks'; ┊ 4┊ 6┊import { resolvers } from './data/resolvers'; +┊ ┊ 7┊import { JWT_SECRET } from './config'; +┊ ┊ 8┊import { User } from './data/connectors'; ┊ 5┊ 9┊ ┊ 6┊10┊const PORT = 8080; ┊ 7┊11┊ ``` ```diff @@ -9,6 +13,29 @@ ┊ 9┊13┊ resolvers, ┊10┊14┊ typeDefs, ┊11┊15┊ // mocks, +┊ ┊16┊ context: ({ req, res, connection }) => { +┊ ┊17┊ // web socket subscriptions will return a connection +┊ ┊18┊ if (connection) { +┊ ┊19┊ // check connection for metadata +┊ ┊20┊ return {}; +┊ ┊21┊ } +┊ ┊22┊ +┊ ┊23┊ const user = new Promise((resolve, reject) => { +┊ ┊24┊ jwt({ +┊ ┊25┊ secret: JWT_SECRET, +┊ ┊26┊ credentialsRequired: false, +┊ ┊27┊ })(req, res, (e) => { +┊ ┊28┊ if (req.user) { +┊ ┊29┊ resolve(User.findOne({ where: { id: req.user.id } })); +┊ ┊30┊ } else { +┊ ┊31┊ resolve(null); +┊ ┊32┊ } +┊ ┊33┊ }); +┊ ┊34┊ }); +┊ ┊35┊ return { +┊ ┊36┊ user, +┊ ┊37┊ }; +┊ ┊38┊ }, ┊12┊39┊}); ┊13┊40┊ ┊14┊41┊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! [{]: (diffStep 7.3) #### [Step 7.3: Update Schema with auth](https://github.com/srtucker22/chatty/commit/d2aa4ff) ##### Changed server/data/schema.js ```diff @@ -35,6 +35,7 @@ ┊35┊35┊ messages: [Message] # messages sent by user ┊36┊36┊ groups: [Group] # groups the user belongs to ┊37┊37┊ friends: [User] # user's friends/contacts +┊ ┊38┊ jwt: String # json web token for access ┊38┊39┊ } ┊39┊40┊ ┊40┊41┊ # a message sent from a user to a group ``` ```diff @@ -61,19 +62,19 @@ ┊61┊62┊ ┊62┊63┊ type Mutation { ┊63┊64┊ # send a message to a group -┊64┊ ┊ createMessage( -┊65┊ ┊ text: String!, userId: Int!, groupId: Int! -┊66┊ ┊ ): Message -┊67┊ ┊ createGroup(name: String!, userIds: [Int], userId: Int!): Group +┊ ┊65┊ createMessage(text: String!, groupId: Int!): Message +┊ ┊66┊ createGroup(name: String!, userIds: [Int]): Group ┊68┊67┊ deleteGroup(id: Int!): Group -┊69┊ ┊ leaveGroup(id: Int!, userId: Int!): Group # let user leave group +┊ ┊68┊ leaveGroup(id: Int!): Group # let user leave group ┊70┊69┊ updateGroup(id: Int!, name: String): Group +┊ ┊70┊ login(email: String!, password: String!): User +┊ ┊71┊ signup(email: String!, password: String!, username: String): User ┊71┊72┊ } ┊72┊73┊ ┊73┊74┊ type Subscription { ┊74┊75┊ # Subscription fires on every message added ┊75┊76┊ # for any of the groups with one of these groupIds -┊76┊ ┊ messageAdded(userId: Int, groupIds: [Int]): Message +┊ ┊77┊ messageAdded(groupIds: [Int]): Message ┊77┊78┊ groupAdded(userId: Int): Group ┊78┊79┊ } ``` [}]: # 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: [{]: (diffStep 7.4) #### [Step 7.4: Update Resolvers with login and signup mutations](https://github.com/srtucker22/chatty/commit/bfa7cb5) ##### Changed server/data/resolvers.js ```diff @@ -1,9 +1,12 @@ ┊ 1┊ 1┊import GraphQLDate from 'graphql-date'; ┊ 2┊ 2┊import { withFilter } from 'apollo-server'; ┊ 3┊ 3┊import { map } from 'lodash'; +┊ ┊ 4┊import bcrypt from 'bcrypt'; +┊ ┊ 5┊import jwt from 'jsonwebtoken'; ┊ 4┊ 6┊ ┊ 5┊ 7┊import { Group, Message, User } from './connectors'; ┊ 6┊ 8┊import { pubsub } from '../subscriptions'; +┊ ┊ 9┊import { JWT_SECRET } from '../config'; ┊ 7┊10┊ ┊ 8┊11┊const MESSAGE_ADDED_TOPIC = 'messageAdded'; ┊ 9┊12┊const GROUP_ADDED_TOPIC = 'groupAdded'; ``` ```diff @@ -88,6 +91,51 @@ ┊ 88┊ 91┊ return Group.findOne({ where: { id } }) ┊ 89┊ 92┊ .then(group => group.update({ name })); ┊ 90┊ 93┊ }, +┊ ┊ 94┊ login(_, { email, password }, ctx) { +┊ ┊ 95┊ // find user by email +┊ ┊ 96┊ return User.findOne({ where: { email } }).then((user) => { +┊ ┊ 97┊ if (user) { +┊ ┊ 98┊ // validate password +┊ ┊ 99┊ return bcrypt.compare(password, user.password).then((res) => { +┊ ┊100┊ if (res) { +┊ ┊101┊ // create jwt +┊ ┊102┊ const token = jwt.sign({ +┊ ┊103┊ id: user.id, +┊ ┊104┊ email: user.email, +┊ ┊105┊ }, JWT_SECRET); +┊ ┊106┊ user.jwt = token; +┊ ┊107┊ ctx.user = Promise.resolve(user); +┊ ┊108┊ return user; +┊ ┊109┊ } +┊ ┊110┊ +┊ ┊111┊ return Promise.reject('password incorrect'); +┊ ┊112┊ }); +┊ ┊113┊ } +┊ ┊114┊ +┊ ┊115┊ return Promise.reject('email not found'); +┊ ┊116┊ }); +┊ ┊117┊ }, +┊ ┊118┊ signup(_, { email, password, username }, ctx) { +┊ ┊119┊ // find user by email +┊ ┊120┊ return User.findOne({ where: { email } }).then((existing) => { +┊ ┊121┊ if (!existing) { +┊ ┊122┊ // hash password and create user +┊ ┊123┊ return bcrypt.hash(password, 10).then(hash => User.create({ +┊ ┊124┊ email, +┊ ┊125┊ password: hash, +┊ ┊126┊ username: username || email, +┊ ┊127┊ })).then((user) => { +┊ ┊128┊ const { id } = user; +┊ ┊129┊ const token = jwt.sign({ id, email }, JWT_SECRET); +┊ ┊130┊ user.jwt = token; +┊ ┊131┊ ctx.user = Promise.resolve(user); +┊ ┊132┊ return user; +┊ ┊133┊ }); +┊ ┊134┊ } +┊ ┊135┊ +┊ ┊136┊ return Promise.reject('email already exists'); // email already exists +┊ ┊137┊ }); +┊ ┊138┊ }, ┊ 91┊139┊ }, ┊ 92┊140┊ Subscription: { ┊ 93┊141┊ 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: [{]: (diffStep 7.5) #### [Step 7.5: Update fake data with hashed passwords](https://github.com/srtucker22/chatty/commit/4baba9b) ##### Changed server/data/connectors.js ```diff @@ -1,6 +1,7 @@ ┊1┊1┊import { _ } from 'lodash'; ┊2┊2┊import faker from 'faker'; ┊3┊3┊import Sequelize from 'sequelize'; +┊ ┊4┊import bcrypt from 'bcrypt'; ┊4┊5┊ ┊5┊6┊// initialize our database ┊6┊7┊const db = new Sequelize('chatty', null, null, { ``` ```diff @@ -53,10 +54,10 @@ ┊53┊54┊ name: faker.lorem.words(3), ┊54┊55┊}).then(group => _.times(USERS_PER_GROUP, () => { ┊55┊56┊ const password = faker.internet.password(); -┊56┊ ┊ return group.createUser({ +┊ ┊57┊ return bcrypt.hash(password, 10).then(hash => group.createUser({ ┊57┊58┊ email: faker.internet.email(), ┊58┊59┊ username: faker.internet.userName(), -┊59┊ ┊ password, +┊ ┊60┊ password: hash, ┊60┊61┊ }).then((user) => { ┊61┊62┊ console.log( ┊62┊63┊ '{email, username, password}', ``` ```diff @@ -68,7 +69,7 @@ ┊68┊69┊ text: faker.lorem.sentences(3), ┊69┊70┊ })); ┊70┊71┊ return user; -┊71┊ ┊ }); +┊ ┊72┊ })); ┊72┊73┊})).then((userPromises) => { ┊73┊74┊ // make users friends with all users in the group ┊74┊75┊ 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: ```js // 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**](http://graphql.org/learn/thinking-in-graphs/#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](https://github.com/apollographql/graphql-server/issues/118), 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: [{]: (diffStep 7.6) #### [Step 7.6: Create logic.js](https://github.com/srtucker22/chatty/commit/da16115) ##### Added server/data/logic.js ```diff @@ -0,0 +1,29 @@ +┊ ┊ 1┊import { ApolloError, AuthenticationError, ForbiddenError } from 'apollo-server'; +┊ ┊ 2┊import { Message } from './connectors'; +┊ ┊ 3┊ +┊ ┊ 4┊// reusable function to check for a user with context +┊ ┊ 5┊function getAuthenticatedUser(ctx) { +┊ ┊ 6┊ return ctx.user.then((user) => { +┊ ┊ 7┊ if (!user) { +┊ ┊ 8┊ throw new AuthenticationError('Unauthenticated'); +┊ ┊ 9┊ } +┊ ┊10┊ return user; +┊ ┊11┊ }); +┊ ┊12┊} +┊ ┊13┊ +┊ ┊14┊export const messageLogic = { +┊ ┊15┊ createMessage(_, { text, groupId }, ctx) { +┊ ┊16┊ return getAuthenticatedUser(ctx) +┊ ┊17┊ .then(user => user.getGroups({ where: { id: groupId }, attributes: ['id'] }) +┊ ┊18┊ .then((group) => { +┊ ┊19┊ if (group.length) { +┊ ┊20┊ return Message.create({ +┊ ┊21┊ userId: user.id, +┊ ┊22┊ text, +┊ ┊23┊ groupId, +┊ ┊24┊ }); +┊ ┊25┊ } +┊ ┊26┊ throw new ForbiddenError('Unauthorized'); +┊ ┊27┊ })); +┊ ┊28┊ }, +┊ ┊29┊}; ``` [}]: # 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: [{]: (diffStep 7.7) #### [Step 7.7: Apply messageLogic to createMessage resolver](https://github.com/srtucker22/chatty/commit/06415f4) ##### Changed server/data/resolvers.js ```diff @@ -7,6 +7,7 @@ ┊ 7┊ 7┊import { Group, Message, User } from './connectors'; ┊ 8┊ 8┊import { pubsub } from '../subscriptions'; ┊ 9┊ 9┊import { JWT_SECRET } from '../config'; +┊ ┊10┊import { messageLogic } from './logic'; ┊10┊11┊ ┊11┊12┊const MESSAGE_ADDED_TOPIC = 'messageAdded'; ┊12┊13┊const GROUP_ADDED_TOPIC = 'groupAdded'; ``` ```diff @@ -37,16 +38,13 @@ ┊37┊38┊ }, ┊38┊39┊ }, ┊39┊40┊ Mutation: { -┊40┊ ┊ createMessage(_, { text, userId, groupId }) { -┊41┊ ┊ return Message.create({ -┊42┊ ┊ userId, -┊43┊ ┊ text, -┊44┊ ┊ groupId, -┊45┊ ┊ }).then((message) => { -┊46┊ ┊ // publish subscription notification with the whole message -┊47┊ ┊ pubsub.publish(MESSAGE_ADDED_TOPIC, { [MESSAGE_ADDED_TOPIC]: message }); -┊48┊ ┊ return message; -┊49┊ ┊ }); +┊ ┊41┊ createMessage(_, args, ctx) { +┊ ┊42┊ return messageLogic.createMessage(_, args, ctx) +┊ ┊43┊ .then((message) => { +┊ ┊44┊ // Publish subscription notification with message +┊ ┊45┊ pubsub.publish(MESSAGE_ADDED_TOPIC, { [MESSAGE_ADDED_TOPIC]: message }); +┊ ┊46┊ return message; +┊ ┊47┊ }); ┊50┊48┊ }, ┊51┊49┊ createGroup(_, { name, userIds, userId }) { ┊52┊50┊ 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: [{]: (diffStep 7.8) #### [Step 7.8: Create logic for all Resolvers](https://github.com/srtucker22/chatty/commit/0b12211) ##### Changed server/data/logic.js ```diff @@ -1,5 +1,5 @@ ┊1┊1┊import { ApolloError, AuthenticationError, ForbiddenError } from 'apollo-server'; -┊2┊ ┊import { Message } from './connectors'; +┊ ┊2┊import { Group, Message, User } from './connectors'; ┊3┊3┊ ┊4┊4┊// reusable function to check for a user with context ┊5┊5┊function getAuthenticatedUser(ctx) { ``` ```diff @@ -12,6 +12,12 @@ ┊12┊12┊} ┊13┊13┊ ┊14┊14┊export const messageLogic = { +┊ ┊15┊ from(message) { +┊ ┊16┊ return message.getUser({ attributes: ['id', 'username'] }); +┊ ┊17┊ }, +┊ ┊18┊ to(message) { +┊ ┊19┊ return message.getGroup({ attributes: ['id', 'name'] }); +┊ ┊20┊ }, ┊15┊21┊ createMessage(_, { text, groupId }, ctx) { ┊16┊22┊ return getAuthenticatedUser(ctx) ┊17┊23┊ .then(user => user.getGroups({ where: { id: groupId }, attributes: ['id'] }) ``` ```diff @@ -27,3 +33,194 @@ ┊ 27┊ 33┊ })); ┊ 28┊ 34┊ }, ┊ 29┊ 35┊}; +┊ ┊ 36┊ +┊ ┊ 37┊export const groupLogic = { +┊ ┊ 38┊ users(group) { +┊ ┊ 39┊ return group.getUsers({ attributes: ['id', 'username'] }); +┊ ┊ 40┊ }, +┊ ┊ 41┊ messages(group, { first, last, before, after }) { +┊ ┊ 42┊ // base query -- get messages from the right group +┊ ┊ 43┊ const where = { groupId: group.id }; +┊ ┊ 44┊ +┊ ┊ 45┊ // because we return messages from newest -> oldest +┊ ┊ 46┊ // before actually means newer (date > cursor) +┊ ┊ 47┊ // after actually means older (date < cursor) +┊ ┊ 48┊ +┊ ┊ 49┊ if (before) { +┊ ┊ 50┊ // convert base-64 to utf8 iso date and use in Date constructor +┊ ┊ 51┊ where.id = { $gt: Buffer.from(before, 'base64').toString() }; +┊ ┊ 52┊ } +┊ ┊ 53┊ +┊ ┊ 54┊ if (after) { +┊ ┊ 55┊ where.id = { $lt: Buffer.from(after, 'base64').toString() }; +┊ ┊ 56┊ } +┊ ┊ 57┊ +┊ ┊ 58┊ return Message.findAll({ +┊ ┊ 59┊ where, +┊ ┊ 60┊ order: [['id', 'DESC']], +┊ ┊ 61┊ limit: first || last, +┊ ┊ 62┊ }).then((messages) => { +┊ ┊ 63┊ const edges = messages.map(message => ({ +┊ ┊ 64┊ cursor: Buffer.from(message.id.toString()).toString('base64'), // convert createdAt to cursor +┊ ┊ 65┊ node: message, // the node is the message itself +┊ ┊ 66┊ })); +┊ ┊ 67┊ +┊ ┊ 68┊ return { +┊ ┊ 69┊ edges, +┊ ┊ 70┊ pageInfo: { +┊ ┊ 71┊ hasNextPage() { +┊ ┊ 72┊ if (messages.length < (last || first)) { +┊ ┊ 73┊ return Promise.resolve(false); +┊ ┊ 74┊ } +┊ ┊ 75┊ +┊ ┊ 76┊ return Message.findOne({ +┊ ┊ 77┊ where: { +┊ ┊ 78┊ groupId: group.id, +┊ ┊ 79┊ id: { +┊ ┊ 80┊ [before ? '$gt' : '$lt']: messages[messages.length - 1].id, +┊ ┊ 81┊ }, +┊ ┊ 82┊ }, +┊ ┊ 83┊ order: [['id', 'DESC']], +┊ ┊ 84┊ }).then(message => !!message); +┊ ┊ 85┊ }, +┊ ┊ 86┊ hasPreviousPage() { +┊ ┊ 87┊ return Message.findOne({ +┊ ┊ 88┊ where: { +┊ ┊ 89┊ groupId: group.id, +┊ ┊ 90┊ id: where.id, +┊ ┊ 91┊ }, +┊ ┊ 92┊ order: [['id']], +┊ ┊ 93┊ }).then(message => !!message); +┊ ┊ 94┊ }, +┊ ┊ 95┊ }, +┊ ┊ 96┊ }; +┊ ┊ 97┊ }); +┊ ┊ 98┊ }, +┊ ┊ 99┊ query(_, { id }, ctx) { +┊ ┊100┊ return getAuthenticatedUser(ctx).then(user => Group.findOne({ +┊ ┊101┊ where: { id }, +┊ ┊102┊ include: [{ +┊ ┊103┊ model: User, +┊ ┊104┊ where: { id: user.id }, +┊ ┊105┊ }], +┊ ┊106┊ })); +┊ ┊107┊ }, +┊ ┊108┊ createGroup(_, { name, userIds }, ctx) { +┊ ┊109┊ return getAuthenticatedUser(ctx) +┊ ┊110┊ .then(user => user.getFriends({ where: { id: { $in: userIds } } }) +┊ ┊111┊ .then((friends) => { // eslint-disable-line arrow-body-style +┊ ┊112┊ return Group.create({ +┊ ┊113┊ name, +┊ ┊114┊ }).then((group) => { // eslint-disable-line arrow-body-style +┊ ┊115┊ return group.addUsers([user, ...friends]).then(() => { +┊ ┊116┊ group.users = [user, ...friends]; +┊ ┊117┊ return group; +┊ ┊118┊ }); +┊ ┊119┊ }); +┊ ┊120┊ })); +┊ ┊121┊ }, +┊ ┊122┊ deleteGroup(_, { id }, ctx) { +┊ ┊123┊ return getAuthenticatedUser(ctx).then((user) => { // eslint-disable-line arrow-body-style +┊ ┊124┊ return Group.findOne({ +┊ ┊125┊ where: { id }, +┊ ┊126┊ include: [{ +┊ ┊127┊ model: User, +┊ ┊128┊ where: { id: user.id }, +┊ ┊129┊ }], +┊ ┊130┊ }).then(group => group.getUsers() +┊ ┊131┊ .then(users => group.removeUsers(users)) +┊ ┊132┊ .then(() => Message.destroy({ where: { groupId: group.id } })) +┊ ┊133┊ .then(() => group.destroy())); +┊ ┊134┊ }); +┊ ┊135┊ }, +┊ ┊136┊ leaveGroup(_, { id }, ctx) { +┊ ┊137┊ return getAuthenticatedUser(ctx).then((user) => { +┊ ┊138┊ return Group.findOne({ +┊ ┊139┊ where: { id }, +┊ ┊140┊ include: [{ +┊ ┊141┊ model: User, +┊ ┊142┊ where: { id: user.id }, +┊ ┊143┊ }], +┊ ┊144┊ }).then((group) => { +┊ ┊145┊ if (!group) { +┊ ┊146┊ throw new ApolloError('No group found', 404); +┊ ┊147┊ } +┊ ┊148┊ +┊ ┊149┊ return group.removeUser(user.id) +┊ ┊150┊ .then(() => group.getUsers()) +┊ ┊151┊ .then((users) => { +┊ ┊152┊ // if the last user is leaving, remove the group +┊ ┊153┊ if (!users.length) { +┊ ┊154┊ group.destroy(); +┊ ┊155┊ } +┊ ┊156┊ return { id }; +┊ ┊157┊ }); +┊ ┊158┊ }); +┊ ┊159┊ }); +┊ ┊160┊ }, +┊ ┊161┊ updateGroup(_, { id, name }, ctx) { +┊ ┊162┊ return getAuthenticatedUser(ctx).then((user) => { // eslint-disable-line arrow-body-style +┊ ┊163┊ return Group.findOne({ +┊ ┊164┊ where: { id }, +┊ ┊165┊ include: [{ +┊ ┊166┊ model: User, +┊ ┊167┊ where: { id: user.id }, +┊ ┊168┊ }], +┊ ┊169┊ }).then(group => group.update({ name })); +┊ ┊170┊ }); +┊ ┊171┊ }, +┊ ┊172┊}; +┊ ┊173┊ +┊ ┊174┊export const userLogic = { +┊ ┊175┊ email(user, args, ctx) { +┊ ┊176┊ return getAuthenticatedUser(ctx).then((currentUser) => { +┊ ┊177┊ if (currentUser.id === user.id) { +┊ ┊178┊ return currentUser.email; +┊ ┊179┊ } +┊ ┊180┊ +┊ ┊181┊ throw new ForbiddenError('Unauthorized'); +┊ ┊182┊ }); +┊ ┊183┊ }, +┊ ┊184┊ friends(user, args, ctx) { +┊ ┊185┊ return getAuthenticatedUser(ctx).then((currentUser) => { +┊ ┊186┊ if (currentUser.id !== user.id) { +┊ ┊187┊ throw new ForbiddenError('Unauthorized'); +┊ ┊188┊ } +┊ ┊189┊ +┊ ┊190┊ return user.getFriends({ attributes: ['id', 'username'] }); +┊ ┊191┊ }); +┊ ┊192┊ }, +┊ ┊193┊ groups(user, args, ctx) { +┊ ┊194┊ return getAuthenticatedUser(ctx).then((currentUser) => { +┊ ┊195┊ if (currentUser.id !== user.id) { +┊ ┊196┊ throw new ForbiddenError('Unauthorized'); +┊ ┊197┊ } +┊ ┊198┊ +┊ ┊199┊ return user.getGroups(); +┊ ┊200┊ }); +┊ ┊201┊ }, +┊ ┊202┊ jwt(user) { +┊ ┊203┊ return Promise.resolve(user.jwt); +┊ ┊204┊ }, +┊ ┊205┊ messages(user, args, ctx) { +┊ ┊206┊ return getAuthenticatedUser(ctx).then((currentUser) => { +┊ ┊207┊ if (currentUser.id !== user.id) { +┊ ┊208┊ throw new ForbiddenError('Unauthorized'); +┊ ┊209┊ } +┊ ┊210┊ +┊ ┊211┊ return Message.findAll({ +┊ ┊212┊ where: { userId: user.id }, +┊ ┊213┊ order: [['createdAt', 'DESC']], +┊ ┊214┊ }); +┊ ┊215┊ }); +┊ ┊216┊ }, +┊ ┊217┊ query(_, args, ctx) { +┊ ┊218┊ return getAuthenticatedUser(ctx).then((user) => { +┊ ┊219┊ if (user.id === args.id || user.email === args.email) { +┊ ┊220┊ return user; +┊ ┊221┊ } +┊ ┊222┊ +┊ ┊223┊ throw new ForbiddenError('Unauthorized'); +┊ ┊224┊ }); +┊ ┊225┊ }, +┊ ┊226┊}; ``` [}]: # And now let’s apply that logic to the Resolvers in `server/data/resolvers.js`: [{]: (diffStep 7.9) #### [Step 7.9: Apply logic to all Resolvers](https://github.com/srtucker22/chatty/commit/99d0d74) ##### Changed server/data/resolvers.js ```diff @@ -7,7 +7,7 @@ ┊ 7┊ 7┊import { Group, Message, User } from './connectors'; ┊ 8┊ 8┊import { pubsub } from '../subscriptions'; ┊ 9┊ 9┊import { JWT_SECRET } from '../config'; -┊10┊ ┊import { messageLogic } from './logic'; +┊ ┊10┊import { groupLogic, messageLogic, userLogic } from './logic'; ┊11┊11┊ ┊12┊12┊const MESSAGE_ADDED_TOPIC = 'messageAdded'; ┊13┊13┊const GROUP_ADDED_TOPIC = 'groupAdded'; ``` ```diff @@ -24,17 +24,11 @@ ┊24┊24┊ }, ┊25┊25┊ }, ┊26┊26┊ Query: { -┊27┊ ┊ group(_, args) { -┊28┊ ┊ return Group.find({ where: args }); +┊ ┊27┊ group(_, args, ctx) { +┊ ┊28┊ return groupLogic.query(_, args, ctx); ┊29┊29┊ }, -┊30┊ ┊ messages(_, args) { -┊31┊ ┊ return Message.findAll({ -┊32┊ ┊ where: args, -┊33┊ ┊ order: [['createdAt', 'DESC']], -┊34┊ ┊ }); -┊35┊ ┊ }, -┊36┊ ┊ user(_, args) { -┊37┊ ┊ return User.findOne({ where: args }); +┊ ┊30┊ user(_, args, ctx) { +┊ ┊31┊ return userLogic.query(_, args, ctx); ┊38┊32┊ }, ┊39┊33┊ }, ┊40┊34┊ Mutation: { ``` ```diff @@ -46,48 +40,20 @@ ┊46┊40┊ return message; ┊47┊41┊ }); ┊48┊42┊ }, -┊49┊ ┊ createGroup(_, { name, userIds, userId }) { -┊50┊ ┊ return User.findOne({ where: { id: userId } }) -┊51┊ ┊ .then(user => user.getFriends({ where: { id: { $in: userIds } } }) -┊52┊ ┊ .then(friends => Group.create({ -┊53┊ ┊ name, -┊54┊ ┊ users: [user, ...friends], -┊55┊ ┊ }) -┊56┊ ┊ .then(group => group.addUsers([user, ...friends]) -┊57┊ ┊ .then((res) => { -┊58┊ ┊ // append the user list to the group object -┊59┊ ┊ // to pass to pubsub so we can check members -┊60┊ ┊ group.users = [user, ...friends]; -┊61┊ ┊ pubsub.publish(GROUP_ADDED_TOPIC, { [GROUP_ADDED_TOPIC]: group }); -┊62┊ ┊ return group; -┊63┊ ┊ })), -┊64┊ ┊ ), -┊65┊ ┊ ); -┊66┊ ┊ }, -┊67┊ ┊ deleteGroup(_, { id }) { -┊68┊ ┊ return Group.find({ where: id }) -┊69┊ ┊ .then(group => group.getUsers() -┊70┊ ┊ .then(users => group.removeUsers(users)) -┊71┊ ┊ .then(() => Message.destroy({ where: { groupId: group.id } })) -┊72┊ ┊ .then(() => group.destroy()), -┊73┊ ┊ ); -┊74┊ ┊ }, -┊75┊ ┊ leaveGroup(_, { id, userId }) { -┊76┊ ┊ return Group.findOne({ where: { id } }) -┊77┊ ┊ .then(group => group.removeUser(userId) -┊78┊ ┊ .then(() => group.getUsers()) -┊79┊ ┊ .then((users) => { -┊80┊ ┊ // if the last user is leaving, remove the group -┊81┊ ┊ if (!users.length) { -┊82┊ ┊ group.destroy(); -┊83┊ ┊ } -┊84┊ ┊ return { id }; -┊85┊ ┊ }), -┊86┊ ┊ ); +┊ ┊43┊ createGroup(_, args, ctx) { +┊ ┊44┊ return groupLogic.createGroup(_, args, ctx).then((group) => { +┊ ┊45┊ pubsub.publish(GROUP_ADDED_TOPIC, { [GROUP_ADDED_TOPIC]: group }); +┊ ┊46┊ return group; +┊ ┊47┊ }); ┊87┊48┊ }, -┊88┊ ┊ updateGroup(_, { id, name }) { -┊89┊ ┊ return Group.findOne({ where: { id } }) -┊90┊ ┊ .then(group => group.update({ name })); +┊ ┊49┊ deleteGroup(_, args, ctx) { +┊ ┊50┊ return groupLogic.deleteGroup(_, args, ctx); +┊ ┊51┊ }, +┊ ┊52┊ leaveGroup(_, args, ctx) { +┊ ┊53┊ return groupLogic.leaveGroup(_, args, ctx); +┊ ┊54┊ }, +┊ ┊55┊ updateGroup(_, args, ctx) { +┊ ┊56┊ return groupLogic.updateGroup(_, args, ctx); ┊91┊57┊ }, ┊92┊58┊ login(_, { email, password }, ctx) { ┊93┊59┊ // find user by email ``` ```diff @@ -162,88 +128,36 @@ ┊162┊128┊ }, ┊163┊129┊ }, ┊164┊130┊ Group: { -┊165┊ ┊ users(group) { -┊166┊ ┊ return group.getUsers(); +┊ ┊131┊ users(group, args, ctx) { +┊ ┊132┊ return groupLogic.users(group, args, ctx); ┊167┊133┊ }, -┊168┊ ┊ messages(group, { first, last, before, after }) { -┊169┊ ┊ // base query -- get messages from the right group -┊170┊ ┊ const where = { groupId: group.id }; -┊171┊ ┊ -┊172┊ ┊ // because we return messages from newest -> oldest -┊173┊ ┊ // before actually means newer (id > cursor) -┊174┊ ┊ // after actually means older (id < cursor) -┊175┊ ┊ -┊176┊ ┊ if (before) { -┊177┊ ┊ // convert base-64 to utf8 id -┊178┊ ┊ where.id = { $gt: Buffer.from(before, 'base64').toString() }; -┊179┊ ┊ } -┊180┊ ┊ -┊181┊ ┊ if (after) { -┊182┊ ┊ where.id = { $lt: Buffer.from(after, 'base64').toString() }; -┊183┊ ┊ } -┊184┊ ┊ -┊185┊ ┊ return Message.findAll({ -┊186┊ ┊ where, -┊187┊ ┊ order: [['id', 'DESC']], -┊188┊ ┊ limit: first || last, -┊189┊ ┊ }).then((messages) => { -┊190┊ ┊ const edges = messages.map(message => ({ -┊191┊ ┊ cursor: Buffer.from(message.id.toString()).toString('base64'), // convert id to cursor -┊192┊ ┊ node: message, // the node is the message itself -┊193┊ ┊ })); -┊194┊ ┊ -┊195┊ ┊ return { -┊196┊ ┊ edges, -┊197┊ ┊ pageInfo: { -┊198┊ ┊ hasNextPage() { -┊199┊ ┊ if (messages.length < (last || first)) { -┊200┊ ┊ return Promise.resolve(false); -┊201┊ ┊ } -┊202┊ ┊ -┊203┊ ┊ return Message.findOne({ -┊204┊ ┊ where: { -┊205┊ ┊ groupId: group.id, -┊206┊ ┊ id: { -┊207┊ ┊ [before ? '$gt' : '$lt']: messages[messages.length - 1].id, -┊208┊ ┊ }, -┊209┊ ┊ }, -┊210┊ ┊ order: [['id', 'DESC']], -┊211┊ ┊ }).then(message => !!message); -┊212┊ ┊ }, -┊213┊ ┊ hasPreviousPage() { -┊214┊ ┊ return Message.findOne({ -┊215┊ ┊ where: { -┊216┊ ┊ groupId: group.id, -┊217┊ ┊ id: where.id, -┊218┊ ┊ }, -┊219┊ ┊ order: [['id']], -┊220┊ ┊ }).then(message => !!message); -┊221┊ ┊ }, -┊222┊ ┊ }, -┊223┊ ┊ }; -┊224┊ ┊ }); +┊ ┊134┊ messages(group, args, ctx) { +┊ ┊135┊ return groupLogic.messages(group, args, ctx); ┊225┊136┊ }, ┊226┊137┊ }, ┊227┊138┊ Message: { -┊228┊ ┊ to(message) { -┊229┊ ┊ return message.getGroup(); +┊ ┊139┊ to(message, args, ctx) { +┊ ┊140┊ return messageLogic.to(message, args, ctx); ┊230┊141┊ }, -┊231┊ ┊ from(message) { -┊232┊ ┊ return message.getUser(); +┊ ┊142┊ from(message, args, ctx) { +┊ ┊143┊ return messageLogic.from(message, args, ctx); ┊233┊144┊ }, ┊234┊145┊ }, ┊235┊146┊ User: { -┊236┊ ┊ messages(user) { -┊237┊ ┊ return Message.findAll({ -┊238┊ ┊ where: { userId: user.id }, -┊239┊ ┊ order: [['createdAt', 'DESC']], -┊240┊ ┊ }); +┊ ┊147┊ email(user, args, ctx) { +┊ ┊148┊ return userLogic.email(user, args, ctx); +┊ ┊149┊ }, +┊ ┊150┊ friends(user, args, ctx) { +┊ ┊151┊ return userLogic.friends(user, args, ctx); +┊ ┊152┊ }, +┊ ┊153┊ groups(user, args, ctx) { +┊ ┊154┊ return userLogic.groups(user, args, ctx); ┊241┊155┊ }, -┊242┊ ┊ groups(user) { -┊243┊ ┊ return user.getGroups(); +┊ ┊156┊ jwt(user, args, ctx) { +┊ ┊157┊ return userLogic.jwt(user, args, ctx); ┊244┊158┊ }, -┊245┊ ┊ friends(user) { -┊246┊ ┊ return user.getFriends(); +┊ ┊159┊ messages(user, args, ctx) { +┊ ┊160┊ return userLogic.messages(user, args, ctx); ┊247┊161┊ }, ┊248┊162┊ }, ┊249┊163┊}; ``` [}]: # We also need to update our subscription filters with the user context. Fortunately, `withFilter` can return a `Boolean` or `Promise`. [{]: (diffStep "7.10") #### [Step 7.10: Apply user context to subscription filters](https://github.com/srtucker22/chatty/commit/bba8202) ##### Changed server/data/resolvers.js ```diff @@ -105,24 +105,28 @@ ┊105┊105┊ messageAdded: { ┊106┊106┊ subscribe: withFilter( ┊107┊107┊ () => pubsub.asyncIterator(MESSAGE_ADDED_TOPIC), -┊108┊ ┊ (payload, args) => { -┊109┊ ┊ return Boolean( -┊110┊ ┊ args.groupIds && -┊111┊ ┊ ~args.groupIds.indexOf(payload.messageAdded.groupId) && -┊112┊ ┊ args.userId !== payload.messageAdded.userId, // don't send to user creating message -┊113┊ ┊ ); +┊ ┊108┊ (payload, args, ctx) => { +┊ ┊109┊ return ctx.user.then((user) => { +┊ ┊110┊ return Boolean( +┊ ┊111┊ args.groupIds && +┊ ┊112┊ ~args.groupIds.indexOf(payload.messageAdded.groupId) && +┊ ┊113┊ user.id !== payload.messageAdded.userId, // don't send to user creating message +┊ ┊114┊ ); +┊ ┊115┊ }); ┊114┊116┊ }, ┊115┊117┊ ), ┊116┊118┊ }, ┊117┊119┊ groupAdded: { ┊118┊120┊ subscribe: withFilter( ┊119┊121┊ () => pubsub.asyncIterator(GROUP_ADDED_TOPIC), -┊120┊ ┊ (payload, args) => { -┊121┊ ┊ return Boolean( -┊122┊ ┊ args.userId && -┊123┊ ┊ ~map(payload.groupAdded.users, 'id').indexOf(args.userId) && -┊124┊ ┊ args.userId !== payload.groupAdded.users[0].id, // don't send to user creating group -┊125┊ ┊ ); +┊ ┊122┊ (payload, args, ctx) => { +┊ ┊123┊ return ctx.user.then((user) => { +┊ ┊124┊ return Boolean( +┊ ┊125┊ args.userId && +┊ ┊126┊ ~map(payload.groupAdded.users, 'id').indexOf(args.userId) && +┊ ┊127┊ user.id !== payload.groupAdded.users[0].id, // don't send to user creating group +┊ ┊128┊ ); +┊ ┊129┊ }); ┊126┊130┊ }, ┊127┊131┊ ), ┊128┊132┊ }, ``` [}]: # 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: [{]: (diffStep "7.11") #### [Step 7.11: Apply versioning to JWT auth](https://github.com/srtucker22/chatty/commit/8045bc6) ##### Changed server/data/connectors.js ```diff @@ -25,6 +25,7 @@ ┊25┊25┊ email: { type: Sequelize.STRING }, ┊26┊26┊ username: { type: Sequelize.STRING }, ┊27┊27┊ password: { type: Sequelize.STRING }, +┊ ┊28┊ version: { type: Sequelize.INTEGER }, // version the password ┊28┊29┊}); ┊29┊30┊ ┊30┊31┊// users belong to multiple groups ``` ```diff @@ -58,6 +59,7 @@ ┊58┊59┊ email: faker.internet.email(), ┊59┊60┊ username: faker.internet.userName(), ┊60┊61┊ password: hash, +┊ ┊62┊ version: 1, ┊61┊63┊ }).then((user) => { ┊62┊64┊ console.log( ┊63┊65┊ '{email, username, password}', ``` ##### Changed server/data/resolvers.js ```diff @@ -66,6 +66,7 @@ ┊66┊66┊ const token = jwt.sign({ ┊67┊67┊ id: user.id, ┊68┊68┊ email: user.email, +┊ ┊69┊ version: user.version, ┊69┊70┊ }, JWT_SECRET); ┊70┊71┊ user.jwt = token; ┊71┊72┊ ctx.user = Promise.resolve(user); ``` ```diff @@ -88,9 +89,10 @@ ┊88┊89┊ email, ┊89┊90┊ password: hash, ┊90┊91┊ username: username || email, +┊ ┊92┊ version: 1, ┊91┊93┊ })).then((user) => { ┊92┊94┊ const { id } = user; -┊93┊ ┊ const token = jwt.sign({ id, email }, JWT_SECRET); +┊ ┊95┊ const token = jwt.sign({ id, email, version: 1 }, JWT_SECRET); ┊94┊96┊ user.jwt = token; ┊95┊97┊ ctx.user = Promise.resolve(user); ┊96┊98┊ return user; ``` ##### Changed server/index.js ```diff @@ -26,7 +26,7 @@ ┊26┊26┊ credentialsRequired: false, ┊27┊27┊ })(req, res, (e) => { ┊28┊28┊ if (req.user) { -┊29┊ ┊ resolve(User.findOne({ where: { id: req.user.id } })); +┊ ┊29┊ resolve(User.findOne({ where: { id: req.user.id, version: req.user.version } })); ┊30┊30┊ } else { ┊31┊31┊ resolve(null); ┊32┊32┊ } ``` [}]: # # 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 ![Login Image](https://github.com/srtucker22/chatty/blob/master/.tortilla/media/step7-11-1.png) 2. Apply the JWT to the Authorization Header for future requests and make whatever authorized `query` or `mutation` requests we want ![Query Image Success](https://github.com/srtucker22/chatty/blob/master/.tortilla/media/step7-11-2.png) ![Query Image Fail](https://github.com/srtucker22/chatty/blob/master/.tortilla/media/step7-11-3.png) ![Query Image Partial](https://github.com/srtucker22/chatty/blob/master/.tortilla/media/step7-11-4.png) # 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: [{]: (diffStep 7.12) #### [Step 7.12: Add onConnect to ApolloServer config](https://github.com/srtucker22/chatty/commit/84bb74f) ##### Changed server/index.js ```diff @@ -1,5 +1,6 @@ -┊1┊ ┊import { ApolloServer } from 'apollo-server'; +┊ ┊1┊import { ApolloServer, AuthenticationError } from 'apollo-server'; ┊2┊2┊import jwt from 'express-jwt'; +┊ ┊3┊import jsonwebtoken from 'jsonwebtoken'; ┊3┊4┊ ┊4┊5┊import { typeDefs } from './data/schema'; ┊5┊6┊import { mocks } from './data/mocks'; ``` ```diff @@ -17,7 +18,7 @@ ┊17┊18┊ // web socket subscriptions will return a connection ┊18┊19┊ if (connection) { ┊19┊20┊ // check connection for metadata -┊20┊ ┊ return {}; +┊ ┊21┊ return connection.context; ┊21┊22┊ } ┊22┊23┊ ┊23┊24┊ const user = new Promise((resolve, reject) => { ``` ```diff @@ -36,6 +37,34 @@ ┊36┊37┊ user, ┊37┊38┊ }; ┊38┊39┊ }, +┊ ┊40┊ subscriptions: { +┊ ┊41┊ onConnect(connectionParams, websocket, wsContext) { +┊ ┊42┊ const userPromise = new Promise((res, rej) => { +┊ ┊43┊ if (connectionParams.jwt) { +┊ ┊44┊ jsonwebtoken.verify( +┊ ┊45┊ connectionParams.jwt, JWT_SECRET, +┊ ┊46┊ (err, decoded) => { +┊ ┊47┊ if (err) { +┊ ┊48┊ rej(new AuthenticationError('No token')); +┊ ┊49┊ } +┊ ┊50┊ +┊ ┊51┊ res(User.findOne({ where: { id: decoded.id, version: decoded.version } })); +┊ ┊52┊ }, +┊ ┊53┊ ); +┊ ┊54┊ } else { +┊ ┊55┊ rej(new AuthenticationError('No token')); +┊ ┊56┊ } +┊ ┊57┊ }); +┊ ┊58┊ +┊ ┊59┊ return userPromise.then((user) => { +┊ ┊60┊ if (user) { +┊ ┊61┊ return { user: Promise.resolve(user) }; +┊ ┊62┊ } +┊ ┊63┊ +┊ ┊64┊ return Promise.reject(new AuthenticationError('No user')); +┊ ┊65┊ }); +┊ ┊66┊ }, +┊ ┊67┊ }, ┊39┊68┊}); ┊40┊69┊ ┊41┊70┊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: [{]: (diffStep 7.13 files="server/data/logic.js") #### [Step 7.13: Create subscriptionLogic](https://github.com/srtucker22/chatty/commit/9849422) ##### Changed server/data/logic.js ```diff @@ -224,3 +224,28 @@ ┊224┊224┊ }); ┊225┊225┊ }, ┊226┊226┊}; +┊ ┊227┊ +┊ ┊228┊export const subscriptionLogic = { +┊ ┊229┊ groupAdded(params, args, ctx) { +┊ ┊230┊ return getAuthenticatedUser(ctx) +┊ ┊231┊ .then((user) => { +┊ ┊232┊ if (user.id !== args.userId) { +┊ ┊233┊ throw new ForbiddenError('Unauthorized'); +┊ ┊234┊ } +┊ ┊235┊ +┊ ┊236┊ return Promise.resolve(); +┊ ┊237┊ }); +┊ ┊238┊ }, +┊ ┊239┊ messageAdded(params, args, ctx) { +┊ ┊240┊ return getAuthenticatedUser(ctx) +┊ ┊241┊ .then(user => user.getGroups({ where: { id: { $in: args.groupIds } }, attributes: ['id'] }) +┊ ┊242┊ .then((groups) => { +┊ ┊243┊ // user attempted to subscribe to some groups without access +┊ ┊244┊ if (args.groupIds.length > groups.length) { +┊ ┊245┊ throw new ForbiddenError('Unauthorized'); +┊ ┊246┊ } +┊ ┊247┊ +┊ ┊248┊ return Promise.resolve(); +┊ ┊249┊ })); +┊ ┊250┊ }, +┊ ┊251┊}; ``` [}]: # 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. [{]: (diffStep 7.13 files="server/subscriptions.js") #### [Step 7.13: Create subscriptionLogic](https://github.com/srtucker22/chatty/commit/9849422) ##### Changed server/subscriptions.js ```diff @@ -1,5 +1,24 @@ +┊ ┊ 1┊import { $$asyncIterator } from 'iterall'; ┊ 1┊ 2┊import { PubSub } from 'apollo-server'; ┊ 2┊ 3┊ ┊ 3┊ 4┊export const pubsub = new PubSub(); ┊ 4┊ 5┊ +┊ ┊ 6┊pubsub.asyncAuthIterator = (messages, authPromise) => { +┊ ┊ 7┊ const asyncIterator = pubsub.asyncIterator(messages); +┊ ┊ 8┊ return { +┊ ┊ 9┊ next() { +┊ ┊10┊ return authPromise.then(() => asyncIterator.next()); +┊ ┊11┊ }, +┊ ┊12┊ return() { +┊ ┊13┊ return authPromise.then(() => asyncIterator.return()); +┊ ┊14┊ }, +┊ ┊15┊ throw(error) { +┊ ┊16┊ return asyncIterator.throw(error); +┊ ┊17┊ }, +┊ ┊18┊ [$$asyncIterator]() { +┊ ┊19┊ return asyncIterator; +┊ ┊20┊ }, +┊ ┊21┊ }; +┊ ┊22┊}; +┊ ┊23┊ ┊ 5┊24┊export default pubsub; ``` [}]: # We can stick this `pubsub.asyncAuthIterator` in our resolvers like so: [{]: (diffStep 7.13 files="server/data/resolvers.js") #### [Step 7.13: Create subscriptionLogic](https://github.com/srtucker22/chatty/commit/9849422) ##### Changed server/data/resolvers.js ```diff @@ -7,7 +7,7 @@ ┊ 7┊ 7┊import { Group, Message, User } from './connectors'; ┊ 8┊ 8┊import { pubsub } from '../subscriptions'; ┊ 9┊ 9┊import { JWT_SECRET } from '../config'; -┊10┊ ┊import { groupLogic, messageLogic, userLogic } from './logic'; +┊ ┊10┊import { groupLogic, messageLogic, userLogic, subscriptionLogic } from './logic'; ┊11┊11┊ ┊12┊12┊const MESSAGE_ADDED_TOPIC = 'messageAdded'; ┊13┊13┊const GROUP_ADDED_TOPIC = 'groupAdded'; ``` ```diff @@ -106,7 +106,10 @@ ┊106┊106┊ Subscription: { ┊107┊107┊ messageAdded: { ┊108┊108┊ subscribe: withFilter( -┊109┊ ┊ () => pubsub.asyncIterator(MESSAGE_ADDED_TOPIC), +┊ ┊109┊ (payload, args, ctx) => pubsub.asyncAuthIterator( +┊ ┊110┊ MESSAGE_ADDED_TOPIC, +┊ ┊111┊ subscriptionLogic.messageAdded(payload, args, ctx), +┊ ┊112┊ ), ┊110┊113┊ (payload, args, ctx) => { ┊111┊114┊ return ctx.user.then((user) => { ┊112┊115┊ return Boolean( ``` ```diff @@ -120,7 +123,10 @@ ┊120┊123┊ }, ┊121┊124┊ groupAdded: { ┊122┊125┊ subscribe: withFilter( -┊123┊ ┊ () => pubsub.asyncIterator(GROUP_ADDED_TOPIC), +┊ ┊126┊ (payload, args, ctx) => pubsub.asyncAuthIterator( +┊ ┊127┊ GROUP_ADDED_TOPIC, +┊ ┊128┊ subscriptionLogic.groupAdded(payload, args, ctx), +┊ ┊129┊ ), ┊124┊130┊ (payload, args, ctx) => { ┊125┊131┊ return ctx.user.then((user) => { ┊126┊132┊ 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: [{]: (diffStep 7.14) #### [Step 7.14: Create Signup Screen](https://github.com/srtucker22/chatty/commit/cf59a3b) ##### Added client/src/screens/signin.screen.js ```diff @@ -0,0 +1,150 @@ +┊ ┊ 1┊import React, { Component } from 'react'; +┊ ┊ 2┊import PropTypes from 'prop-types'; +┊ ┊ 3┊import { +┊ ┊ 4┊ ActivityIndicator, +┊ ┊ 5┊ KeyboardAvoidingView, +┊ ┊ 6┊ Button, +┊ ┊ 7┊ StyleSheet, +┊ ┊ 8┊ Text, +┊ ┊ 9┊ TextInput, +┊ ┊ 10┊ TouchableOpacity, +┊ ┊ 11┊ View, +┊ ┊ 12┊} from 'react-native'; +┊ ┊ 13┊ +┊ ┊ 14┊const styles = StyleSheet.create({ +┊ ┊ 15┊ container: { +┊ ┊ 16┊ flex: 1, +┊ ┊ 17┊ justifyContent: 'center', +┊ ┊ 18┊ backgroundColor: '#eeeeee', +┊ ┊ 19┊ paddingHorizontal: 50, +┊ ┊ 20┊ }, +┊ ┊ 21┊ inputContainer: { +┊ ┊ 22┊ marginBottom: 20, +┊ ┊ 23┊ }, +┊ ┊ 24┊ input: { +┊ ┊ 25┊ height: 40, +┊ ┊ 26┊ borderRadius: 4, +┊ ┊ 27┊ marginVertical: 6, +┊ ┊ 28┊ padding: 6, +┊ ┊ 29┊ backgroundColor: 'rgba(0,0,0,0.2)', +┊ ┊ 30┊ }, +┊ ┊ 31┊ loadingContainer: { +┊ ┊ 32┊ left: 0, +┊ ┊ 33┊ right: 0, +┊ ┊ 34┊ top: 0, +┊ ┊ 35┊ bottom: 0, +┊ ┊ 36┊ position: 'absolute', +┊ ┊ 37┊ flexDirection: 'row', +┊ ┊ 38┊ justifyContent: 'center', +┊ ┊ 39┊ alignItems: 'center', +┊ ┊ 40┊ }, +┊ ┊ 41┊ switchContainer: { +┊ ┊ 42┊ flexDirection: 'row', +┊ ┊ 43┊ justifyContent: 'center', +┊ ┊ 44┊ marginTop: 12, +┊ ┊ 45┊ }, +┊ ┊ 46┊ switchAction: { +┊ ┊ 47┊ paddingHorizontal: 4, +┊ ┊ 48┊ color: 'blue', +┊ ┊ 49┊ }, +┊ ┊ 50┊ submit: { +┊ ┊ 51┊ marginVertical: 6, +┊ ┊ 52┊ }, +┊ ┊ 53┊}); +┊ ┊ 54┊ +┊ ┊ 55┊class Signin extends Component { +┊ ┊ 56┊ static navigationOptions = { +┊ ┊ 57┊ title: 'Chatty', +┊ ┊ 58┊ headerLeft: null, +┊ ┊ 59┊ }; +┊ ┊ 60┊ +┊ ┊ 61┊ constructor(props) { +┊ ┊ 62┊ super(props); +┊ ┊ 63┊ this.state = { +┊ ┊ 64┊ view: 'login', +┊ ┊ 65┊ }; +┊ ┊ 66┊ this.login = this.login.bind(this); +┊ ┊ 67┊ this.signup = this.signup.bind(this); +┊ ┊ 68┊ this.switchView = this.switchView.bind(this); +┊ ┊ 69┊ } +┊ ┊ 70┊ +┊ ┊ 71┊ // fake for now +┊ ┊ 72┊ login() { +┊ ┊ 73┊ console.log('logging in'); +┊ ┊ 74┊ this.setState({ loading: true }); +┊ ┊ 75┊ setTimeout(() => { +┊ ┊ 76┊ console.log('signing up'); +┊ ┊ 77┊ this.props.navigation.goBack(); +┊ ┊ 78┊ }, 1000); +┊ ┊ 79┊ } +┊ ┊ 80┊ +┊ ┊ 81┊ // fake for now +┊ ┊ 82┊ signup() { +┊ ┊ 83┊ console.log('signing up'); +┊ ┊ 84┊ this.setState({ loading: true }); +┊ ┊ 85┊ setTimeout(() => { +┊ ┊ 86┊ this.props.navigation.goBack(); +┊ ┊ 87┊ }, 1000); +┊ ┊ 88┊ } +┊ ┊ 89┊ +┊ ┊ 90┊ switchView() { +┊ ┊ 91┊ this.setState({ +┊ ┊ 92┊ view: this.state.view === 'signup' ? 'login' : 'signup', +┊ ┊ 93┊ }); +┊ ┊ 94┊ } +┊ ┊ 95┊ +┊ ┊ 96┊ render() { +┊ ┊ 97┊ const { view } = this.state; +┊ ┊ 98┊ +┊ ┊ 99┊ return ( +┊ ┊100┊ +┊ ┊104┊ {this.state.loading ? +┊ ┊105┊ +┊ ┊106┊ +┊ ┊107┊ : undefined} +┊ ┊108┊ +┊ ┊109┊ this.setState({ email })} +┊ ┊111┊ placeholder={'Email'} +┊ ┊112┊ style={styles.input} +┊ ┊113┊ /> +┊ ┊114┊ this.setState({ password })} +┊ ┊116┊ placeholder={'Password'} +┊ ┊117┊ secureTextEntry +┊ ┊118┊ style={styles.input} +┊ ┊119┊ /> +┊ ┊120┊ +┊ ┊121┊