How to Set Up Basic Authentication with Apollo Server and Prisma

Introduction

In this tutorial, we are going to learn how to handle authentication with Apollo Server.

Authentication is a requirement of almost all applications. Authentication allows users to restrict access to sensitive data and can return the data specific to the current user.

In this tutorial, we are going to learn how to handle authentication using JSON Web Tokens and Apollo Server. The JSON Web Token will act as a claim that a user will send in every authenticated request to confirm their identity. Once we are able to confirm their identity using the token we will send them the resource they have asked for.

The authentication workflow used in this tutorial is as follows:

  1. User registers using the register mutation
  2. The user authenticates themselves using the login mutation using the credentials they made during registration
  3. The user gets back a JSON Web Token as a response from the login mutation
  4. The user authenticates further requests using that token
  5. We populate the user data in the context of each graphql request they make
  6. We use that user data to make our authorization checks in our resolvers

Prerequisites

This tutorial assumes you have a basic knowledge of building web servers using node.js and some knowledge of authentication and authorization. Additionally, if you want to try this tutorial out locally, you will also need to have Docker installed.

  • Node is a requirement to run Apollo Server. You can download it from the node website
  • (Optional) You can install Docker from their site. If you would prefer to avoid Docker, you can instead use the Prisma Demo server to complete this tutorial.

Setting up a basic Apollo Server

You will need a basic Apollo Server setup.

Step 1

Clone this starter repo to follow along with this tutorial.

To get started with the starter files, you should take the following steps:

  1. Run npm install to grab dependencies from npm.
  2. Start prisma and database instance using docker-compose up -d (Run npm run deploy -- -n to use prisma demo servers)
  3. Deploy the datamodel using prisma deploy
  4. Start the server using npm run dev

Creating the schema

In the graphql schema, we are implementing a Login and a Register mutation which will be used for authentication flow. We are also implementing a currentUser query which will be protected and can only be queried by a logged in user.

Step 2

Open src/typeDefs.js and paste the following schema between the gql tags:

type User {
  id: ID!
  username: String!
}

type Query {
  currentUser: User!
}

type Mutation {
  register(username: String!, password: String!): User!
  login(username: String!, password: String!): LoginResponse!
}

type LoginResponse {
  token: String
  user: User
}
Copy

The fact that we are not directly returning the User in the login mutation is because we need to send the JSON Web Token back in the response, so that the user may save that to authenticate their further requests.

Creating a register mutation resolver

In the register mutation resolver we need to hash the user's password and store the user details to our database. This is an important step as we don't want to store raw passwords in our database.

First, we need to install a hashing library. A hashing library converts a string into a digest so that we can safely save the passwords in the database. You can learn more about hashing here

This article uses bcryptjs for encryption, as it is easy to install. You can also use argon2 instead. However, while it is a bit faster, it requires you to install C++ bindings for it. This is why we are going to use bcryptjs in this tutorial.

Step 3
npm install bcryptjs
Copy

Don't forget to add an import to it in the src/resolvers.js file like so:

const bcrypt = require('bcryptjs')
Copy

Now we can create a resolver for the register mutation.

Open src/resolver.js. In the mutation section of your resolvers add the following resolver:

const resolvers = {
  Mutation: {
    register: async (parent, { username, password }, ctx, info) => {
      const hashedPassword = await bcrypt.hash(password, 10)
      const user = await ctx.prisma.createUser({
        username,
        password: hashedPassword,
      })
      return user
    },
  },
}
Copy

In this resolver, we are first fetching the username and the password from the arguments. Then we are hashing the password using the bcryptjs library. Finally, we are storing those details using Prisma and returning the user.

You can now access the Graphql Playground at http://localhost:4000 to test out this resolver.

Now if we make a mutation registering a new user, we get its details back.

mutation {
  register(username: "USERNAME", password: "PASSWORD") {
    id
    username
  }
}
Copy
{
  "data": {
    "register": {
      "id": "cjp9vb1ys00080951kxnu6t5m",
        "username": "USERNAME"
    }
  }
}

Creating a login mutation resolver

We are going to use the jsonwebtoken library to sign tokens and send them back to the user so that they can authenticate themselves using it.

Step 4
npm install jsonwebtoken
Copy

Don't forget to add an import to it in the src/resolvers.js file like so:

const jwt = require('jsonwebtoken')
Copy

Now we need to make a resolver for the login function.

In the login resolver, we are going to validate the user details and send back the jsonwebtoken to persist the authentication in the subsequent requests.

Open src/resolvers.js and add the following resolver in the Mutation part of your resolver object.

login: async (parent, { username, password }, ctx, info) => {
  const user = await ctx.prisma.user({ username })

  if (!user) {
    throw new Error('Invalid Login')
  }

  const passwordMatch = await bcrypt.compare(password, user.password)

  if (!passwordMatch) {
    throw new Error('Invalid Login')
  }

  const token = jwt.sign(
    {
      id: user.id,
      username: user.email,
    },
    'my-secret-from-env-file-in-prod',
    {
      expiresIn: '30d', // token will expire in 30days
    },
  )
  return {
    token,
    user,
  }
}
Copy

In this resolver we are taking a couple steps:

  1. Matching the provided details with a user in the database.
  2. If we cannot find a user matching the provided details, we will raise an error. 3. In the next step, we are comparing the passwords using our hashing library and throwing an error if we receive a wrong password.
  3. In the last step, we are signing the JSON Web Token and sending that back to the user.

You may notice a secret used while signing the tokens. That secret is very important—it verifies the identity of the token. A user may also pass a counterfeit token generated by themselves, but we are signing the token with this unique secret known only to us, therefore, we can guarantee the authenticity of the token by using this secret.

You should not hard code the secret and commit it into git, as using this secret anyone can create a valid token. Instead, you can use an env file to save it. Dotenv is great package for this purpose.
Step 5

Now when making a login mutation, we will get back a token. You should change "USERNAME" and "PASSWORD" to your preferred details.

mutation {
  login(username: "USERNAME", password: "PASSWORD") {
    token
    user {
      id
      username
    }
  }
}
{
  "data": {
    "login": {
      "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImNqcDl2ZzY3YzAwMGkwOTUxMWo5cWlveXAiLCJpYXQiOjE1NDM5MzU5MjMsImV4cCI6MTU0NjUyNzkyM30.jzJTQdS2EDQdMF2gFiMrC2xboHXbSBB5lQQ0mOVLTcg",
        "user": {
        "id": "cjp9vg67c000i09511j9qioyp",
          "username": "USERNAME"
      }
    }
  }
}

Now that we've covered how to sign a token and send it back to the user, we can write some code that uses the token to authenticate requests.

Populating context with user info

We need to populate the context with the user details so that we can easily access those in our resolvers.

Step 6

Locate the part of your code where are you instantiating the ApolloServer class in src/index.js, and add the context key and the getUser helper function:

// Add this import to top of your file
const jwt = require('jsonwebtoken')

const getUser = token => {
  try {
    if (token) {
      return jwt.verify(token, 'my-secret-from-env-file-in-prod')
    }
    return null
  } catch (err) {
    return null
  }
}

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: ({ req }) => {
    const tokenWithBearer = req.headers.authorization || ''
    const token = tokenWithBearer.split(' ')[1]
    const user = getUser(token)

    return {
      user,
      prisma, // the generated prisma client if you are using it
    }
  },
})
Copy

This code gets the token from the header and verifies and decodes it using the helper function getToken, that we have created. Then it returns the user in the context function along with the prisma instance so that these are available in every resolver we create.

The secret plays an important role here to verify the token. If anyone uses a token signed by a different secret, the JSON Web Token library will throw an error and the code will be able to unauthorize the request.

Creating a protected field

Let us create a resolver for the protected field. This will give you a basic idea of how you can protect any other field.

Step 7

Open src/resolvers.js and add the following code to the Query part of your resolver.

const resolvers = {
  Query: {
    currentUser: (parent, args, { user, prisma }) => {
      // this if statement is our authentication check
      if (!user) {
        throw new Error('Not Authenticated')
      }
      return prisma.user({ id: user.id })
    },
  },
}
// Mutation: ....
Copy

To authenticate the request, we are grabbing the user from the context which we populated in the previous step. If there is no user object, request is not made by an authenticated user, and we can simply throw an error to inform them that they are not authenticated. You can implement such an authentication check in any other resolver you want. Also, you can grab the data specific to the current user as we have done in this example.

Now when you query the API without passing any token it returns an error with additional information about it:

{
  currentUser {
    id
    username
  }
}
Copy
{
  "errors": [
    {
      "message": "Not Authenticated",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "currentUser"
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "exception": {
          "stacktrace": [
            "Error: Not Authenticated",
            "    at currentUser (/Users/USERNAME/code/prisma tutorials/apollo-server-starter/src/resolvers.js:9:15)",
            "    at field.resolve (/Users/USERNAME/code/prisma tutorials/apollo-server-starter/node_modules/graphql-extensions/dist/index.js:128:26)",
            "    at resolveFieldValueOrError (/Users/USERNAME/code/prisma tutorials/apollo-server-starter/node_modules/graphql/execution/execute.js:479:18)",
            "    at resolveField (/Users/USERNAME/code/prisma tutorials/apollo-server-starter/node_modules/graphql/execution/execute.js:446:16)",
            "    at executeFields (/Users/USERNAME/code/prisma tutorials/apollo-server-starter/node_modules/graphql/execution/execute.js:293:18)",
            "    at executeOperation (/Users/USERNAME/code/prisma tutorials/apollo-server-starter/node_modules/graphql/execution/execute.js:237:122)",
            "    at executeImpl (/Users/USERNAME/code/prisma tutorials/apollo-server-starter/node_modules/graphql/execution/execute.js:85:14)",
            "    at Object.execute (/Users/USERNAME/code/prisma tutorials/apollo-server-starter/node_modules/graphql/execution/execute.js:62:35)",
            "    at /Users/USERNAME/code/prisma tutorials/apollo-server-starter/node_modules/apollo-server-core/dist/requestPipeline.js:195:36",
            "    at Generator.next (<anonymous>)"
          ]
        }
      }
    }
  ],
  "data": null
}

But when we add the Authorization header in the HTTP header section of Playground like so (use your own token from the login mutation):

Step 8
{
  "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImNqcDl2ZzY3YzAwMGkwOTUxMWo5cWlveXAiLCJpYXQiOjE1NDM5MzU5MjMsImV4cCI6MTU0NjUyNzkyM30.jzJTQdS2EDQdMF2gFiMrC2xboHXbSBB5lQQ0mOVLTcg"
}

Then we are able to grab the data of the user according to the token:

{
  currentUser {
    id
    username
  }
}
Copy
{
  "data": {
    "currentUser": {
      "id": "cjp9vg67c000i09511j9qioyp",
      "username": "USERNAME"
    }
  }
}

Conclusion

As shown in this article, basic GraphQL authentication requires relatively few steps. Over the course of this article, we've seen how we can add authentication layer to Apollo Server. Additionally, we used JWT as a claim and authenticated our request with it.

In this tutorial, we used a simple if statement inline in our resolver to authenticate the request. To take this example further you can reuse the authentication logic with graphql-middleware. You can learn more about this functionality in the Prisma Authentication and Authorization tutorial.