Skip to content

Implementing Authentication using JWT, Bcrypt and GraphQL Nexus

Henry Yang edited this page May 30, 2019 · 3 revisions

You've finished coding the skeleton for your application, but it's missing one thing - authentication. This can be added using JSON Web Tokens and Bcrypt. The basis of this tutorial should be similar for most schema construction frameworks, but we will be using GraphQL Nexus. We're also using Prisma as our ORM, but any other ORM or database would work.

This tutorial assumes you have knowledge of GraphQL mutations, queries, resolvers and context- If you don't know GraphQL, How to GraphQL is a great place to start.

The final application will allow users to create an account and log in by storing and using a JSON Web Token. JWTs are strings that contain information to transfer between parties, and are a great way to authenticate users because they can securely store user information, and provide a digital signatures.

Our application will allow users to log in and register using these JWTs. On the backend, we will create a payload, add a JWT secret, and set up login and signup mutations to properly generate authorization headers. On the frontend, we'll pass an authorization token into our headers, and set up our queries to get the current logged in user.

Backend

1. Installing Our Tools ๐Ÿ› 

First thing's first, we'll need to install Bcrypt and JSON Web Tokens!

yarn add bcrypt jsonwebtoken

Now you're ready to get startedโœจ

2. Creating our JWT Secret ๐Ÿ—๏ธ

We can set up our JWT secret- in our config.ts file, the following was added:

export default {
  ...
  jwt: {
    JWT_SECRET: 'super-secret',
  },
}

3. Creating the Payload ๐Ÿšš

For us to properly return the token and user information to the requester, we need to set up a payload.

export const UserLoginPayload = objectType({
  name: 'UserLoginPayload',
  definition: t => {
    t.field('user', {
      type: 'User',
    })
    t.string('token')
  },
})

What we're doing here is creating an object type named userLoginPayload. We define the type as being able to return our User field, along with the token generated when the user registers or logs in.

4. Setting up the Login and Signup Mutations ๐Ÿšช๐Ÿšถ

To set up user registration and login, we create two new mutation fields, userLogin and userRegister. We can set the return type to UserLoginPayload to return the User and a token, and our arguments are the username and password collected from a form in the frontend. Here is what the mutations would look like in GraphQL Nexus:

export const userLogin = mutationField('userLogin', {
  type: UserLoginPayload,
  args: {
    username: stringArg({ required: true }),
    password: stringArg({ required: true }),
  },
})

export const userRegister = mutationField('userRegister', {
  type: UserLoginPayload,
  args: {
    username: stringArg({ required: true }),
    password: stringArg({ required: true }),
  },
})

After this, a resolver is added to the mutations.

export const userLogin = mutationField('userLogin', {
  type: UserLoginPayload,
  args: {
    username: stringArg({ required: true }),
    password: stringArg({ required: true }),
  },
  resolve: async (root, args, context, info) => {
    try {
      const { password, ...user } = await context.prisma.user({
        where: {
          userName: args.username,
        },
      })
      var validpass = await bcrypt.compareSync(args.password, password)
      if (validpass) {
        const token = jwt.sign(user, config.jwt.JWT_SECRET)
        return {
          user: user,
          token,
        }
      }
      return null
    } catch (e) {
      console.log(e)
    }
  },
})

We've added our resolver. This might be a bit overwhelming, so let's break it up into pieces.

const { password, ...user } = await context.prisma.user({
        where: {
          userName: args.username,
        },
      })

Here, we're trying to get User data. await context.prisma.users({where: {userName: args.username} gets our User information from the database, storing the information in password, ...user. We've separated the password so it won't be included in our user variable or the JSON Web Token data, as shown in the next step.

var validpass = await bcrypt.compareSync(args.password, password)
      if (validpass) {
        const token = jwt.sign(user, config.jwt.JWT_SECRET)
        return {
          user: user,
          token,
        }
      }
      return null

We use Bcrypt to compare to see if our password values are equal. If the passwords match, a JWT is generated using our JWT secret from the config file and user. (If we didn't separate the password data beforehand, it would have been returned with the user data and stored in the JWT ๐Ÿ˜ฑ!) Though at last, we're now returning our payload (the user data along with the JWT)!

The process for registration is relatively similar.

export const userRegister = mutationField('userRegister', {
  type: UserLoginPayload,
  args: {
    username: stringArg({ required: true }),
    password: stringArg({ required: true }),
  },
  resolve: async (root, args, context) => {
    try {
      const existingUser = await context.prisma.user({
        where: {
          userName: args.username,
        },
      })
      if (existingUser) {
        throw new Error('ERROR: Username already used.')
      }
      var hash = bcrypt.hashSync(args.password, 10)

      const { password, ...register } = await context.prisma.createUser({
        userName: args.username,
        password: hash,
      })
      const token = jwt.sign(register, config.jwt.JWT_SECRET)
      return {
        user: register,
        token: token,
      }
    } catch (e) {
      console.log(e)
      return null
    }
  },
})

Let's break this up again.

const existingUser = await context.prisma.user({
        where: {
          userName: args.username,
        },
      })
      if (existingUser) {
        throw new Error('ERROR: Username already used.')
      }

Previously, we queried to see if an username existed. This is relatively the same, only now we are throwing an error if something is returned, because each username should be unique.

var hash = bcrypt.hashSync(args.password, 10)

      const { password, ...register } = await context.prisma.createUser({
        userName: args.username,
        password: hash,
      })

We hash the password passed into the form using bcrypt, passing in the password and the salt length we want to generate. After that, the createUser mutation makes a new user with our username and newly hashed password.

const token = jwt.sign(register, config.jwt.JWT_SECRET)
      return {
        user: register,
        token: token,
      }

The payload is generated and returned in the same way as the user login.

5. Adding User to the Context ๐Ÿงฎ

Our user can now log in and register! Now we can create a query and viewer field to return that information to the frontend.

Let's start out by adding the current user to the context.

export interface Context {
  prisma: Prisma
  currentUser: User
}

export default async ({ req }) => {
  const currentUser = await getUser(
    req.get('Authorization'),
    config.jwt,
    prisma,
  )
  return {
    prisma,
    currentUser
  }
}

Here, we're adding the variable currentUser of type User to be exported from our Context. We can use a getUser function (we'll go over how to make this function in the next step- in summary, it returns our User type) to return our user's information by passing in our token with req.get('Authorization') (which fetches our token from our header), our JWT secret, and the Prisma client.

6. Creating a getUser Function ๐Ÿ‘ถ

Because we want to query for user information in our application, we need to get our user's token from the headers.

export default async (authorization, secrets, prisma: Prisma) => {
  const bearerLength = 'Bearer '.length
  if (authorization && authorization.length > bearerLength) {
    const token = authorization.slice(bearerLength)
    const { ok, result } = await new Promise(resolve =>
      jwt.verify(token, secrets.JWT_SECRET, (err, result) => {
        if (err) {
          resolve({
            ok: false,
            result: err,
          })
        } else {
          resolve({
            ok: true,
            result,
          })
        }
      }),
    )
    if (ok) {
      const user = await prisma.user({
        id: result.id,
      })
      return user
    } else {
      console.error(result)
      return null
    }
  }
  return null
}

Let's go through this step by step.

const bearerLength = 'Bearer '.length
  if (authorization && authorization.length > bearerLength) {
    const token = authorization.slice(bearerLength)
    ...
  }
  return null
}

Here we have some basic error checking to see if the token is longer than our Bearer string- If it is, we can extract the token by slicing off the Bearer string.

const { ok, result } = await new Promise(resolve =>
      jwt.verify(token, secrets.JWT_SECRET, (err, result) => {
        if (err) {
          resolve({
            ok: false,
            result: err,
          })
        } else {
          resolve({
            ok: true,
            result,
          })
        }
      })
    )

Now we are verifying the token with our secret, and resolving our promise with whether the passed in token was valid or not, along with the result from our JWT (which is our user type).

if (ok) {
      const user = await prisma.user({
        id: result.id,
      })
      return user
    } else {
      console.error(result)
      return null
    }
  }

Lastly, if the token was valid, we query for the user with the ID we got from our token and return it!

7. Creating a User Query and Viewer Field ๐Ÿ”ฌ

We can create a viewer field and user query so we are able to query for the currently logged in user's information in our application.

t.string('getCurrentUser', {
  resolve: async (root, args, context, info) => {
    return context.prisma.user
  },
})

We can create a new query, getCurrentUser- this returns the value of what we got in our Context function, making it so we can now easily query for whatever user is currently logged in!

Lastly, we should add a viewer field to our query.

t.field('viewer', {
      type: 'User',
      nullable: true,
      resolve: (root, args, context) => {
        return context.currentUser
      },
    })

This simply returns the currentUser that we added to our context.

Frontend

1. Login and Registration ๐Ÿ’Ž

Now that our backend is complete, we can implement a simple frontend solution using the resolvers we created in the backend.

const SIGNUP_MUTATION = gql`
  mutation UserRegister($username: String!, $password: String!) {
    userRegister(username: $username, password: $password) {
      user {
        id
        userName
      }
      token
    }
  }
`;

Here is a simple signup mutation that creates a new user when the form is submitted. We are using the userRegister function that we created on the backend, and simply passing in a username and password while returning any desired information.

<Mutation
    mutation={SIGNUP_MUTATION}
    onCompleted={data => _confirm(data)}
  >
...
</Mutation>

Next, we can add the signup mutation to our Mutation component provided by react-apollo. When the mutation is completed, we call the function _confirm.

_confirm = async data => {
  const { token } = data.userLogin;
  this._saveUserData(token);
};

_saveUserData = async token => {
  try {
    await AsyncStorage.setItem(AUTH_TOKEN, token);
  } catch (e) {
    console.log("ERROR: ", e);
  }
};

What our _confirm function does is take the data we were returned from our mutation and extracts the token from it, passing it to _saveUserData. This function stores the token in AsyncStorage (if you're not developing using Native, the token would be stored in LocalStorage).

The process for logging in is extremely similar, we would just swap out our SIGNUP_MUTATION with our LOGIN_MUTATION.

As a side note, using localStorage to store our JWT isnโ€™t the best idea in production- you can read more about that here.

2. Inserting the token into the header ๐Ÿ’ฏ

const authLink = setContext(async (_, { headers }) => {
  const token = await AsyncStorage.getItem(AUTH_TOKEN);
  return {
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : ""
    }
  };
});

We are using apollo-link-context's setContext function to set the headers of our application. We are getting our authorization token from AsyncStorage and then storing it in our header.

3. Querying for User Info ๐Ÿ™†

Because of all of our hard work, we can query for the user's information anywhere we want in the app- Yep, it's that simple!

const GET_USER = gql`
  query getUser {
    viewer {
      id
    }
  }
`;

Conclusion

And with that, your authentication is now set up! We have now created resolvers to return the desired payload and can query for the current logged in user anywhere in the application. This tutorial was inspired by Spencer Carli's great tutorial, GraphQL Authentication with React Native & Apollo - give it a look if you'd like a more in depth look on the things we went over in this tutorial. If you have any questions or suggestions, feel free to create an issue or pull request. Thank you!