Catch the highlights of GraphQLConf 2023!Click for recordings.Or check out our recap blog post.
Tutorial
Advanced
Authentication

Authentication

In this section, you're going to implement signup and login functionality that allows your users to authenticate against your GraphQL server.

Adding a User Model

The first thing you need is a way to represent user data in the database. To do so, you can add a User type to your Prisma data model.

You'll also want to add a relation between the User and the existing Link type to express that Links are posted by Users.

Open prisma/schema.prisma and add the following code, making sure to also update your existing Link model accordingly:

prisma/schema.prisma
model Link {
  id          Int      @id @default(autoincrement())
  createdAt   DateTime @default(now())
  description String
  url         String
  postedBy    User?    @relation(fields: [postedById], references: [id])
  postedById  Int?
}
 
model User {
  id        Int      @id @default(autoincrement())
  name      String
  email     String   @unique
  password  String
  links     Link[]
}

Now you see even more how Prisma helps you to reason about your data in a way that is more aligned with how it is represented in the underlying database.

Understanding relation fields

Notice how you're adding a new relation field called postedBy to the Link model that points to a User instance. The User model then has a links field that's a list of Links.

To do this, you need to also define the relation by annotating the postedBy field with the @relation attribute (opens in a new tab). This is required for every relation field in your Prisma schema, and all you're doing is defining what the foreign key of the related table will be. So in this case, we're adding an extra field to store the id of the User who posts a Link, and then telling Prisma that postedById will be equal to the id field in the User table (if you are familiar with SQL, this kind of relation is being represented as one-to-many).

If this is quite new to you, don't worry! We're going to be adding a few of these relational fields and you'll get the hang of it as you go! For a deeper dive on relations with Prisma, check out these docs (opens in a new tab).

Updating Prisma Client

This is a great time to refresh your memory on the workflow we described for your project at the end of chapter 4!

After every change you make to the data model, you need to migrate your database and then re-generate Prisma Client.

In the root directory of the project, run the following command:

npx prisma migrate dev --name "add-user-model"

This command has now generated your second migration inside of prisma/migrations, and you can start to see how this becomes a historical record of how your database evolves over time. This script also run the Prisma migration, so your new models and types are ready-to-use.

That might feel like a lot of steps, but the workflow will become automatic by the end of this tutorial!

Your database is ready and Prisma Client is now updated to expose all the CRUD queries for the newly added User model – woohoo! 🎉

Extending the GraphQL schema

Remember back when we were setting up your GraphQL server and discussed the process of schema-driven development? It all starts with extending your schema definition with the new operations that you want to add to the API - in this case a signup and login mutation.

Open the application schema in src/schema.ts and update schema types as follows:

type Query {
  hello: String!
  feed: [Link!]!
}
 
type Mutation {
  post(url: String!, description: String!): Link!
  signup(email: String!, password: String!, name: String!): AuthPayload
  login(email: String!, password: String!): AuthPayload
}
 
type Link {
  id: ID!
  description: String!
  url: String!
}
 
type AuthPayload {
  token: String
  user: User
}
 
type User {
  id: ID!
  name: String!
  email: String!
  links: [Link!]!
}

The signup and login mutations behave very similarly: both return information about the User who's signing up (or logging in) as well as a token which can be used to authenticate subsequent requests against your GraphQL API. This information is bundled in the AuthPayload type.

Finally, you need to reflect that the relation between User and Link should be bi-directional by adding the postedBy field to the existing Link model definition in schema.ts:

type Link {
  id: ID!
  description: String!
  url: String!
  postedBy: User
}

Implementing the resolver functions

After extending the schema definition with the new operations, you need to implement resolver functions for them.

Setup for authentication

In this tutorial, you will implement simple, naive implementation of a JWT (Json Web Token) implementation. This is a simple solution for creating token-based authentication.

You'll also use bcryptjs for simple encryption for the user's password.

Start by installing jsonwebtoken library from NPM:

npm i --save-exact jsonwebtoken bcryptjs

And to get better integration with TypeScript, you need to install the typing libraries:

npm i -D --save-exact @types/jsonwebtoken @types/bcryptjs

Create a new file called src/auth.ts, and for now just app a simple variable to hold our signing secret (you'll later use that as the base for our encryption):

src/auth.ts
export const APP_SECRET = 'this is my secret'

Implementing signup resolvers

Open src/schema.ts and add the new signup resolver, under Mutation:

src/schema.ts
// ... other imports ...
import { hash } from 'bcryptjs'
import { sign } from 'jsonwebtoken'
import { APP_SECRET } from './auth'
 
const resolvers = {
  // ... other resolver maps ...
  Mutation: {
    // ... other Mutation field resolvers ...
    async signup(
      parent: unknown,
      args: { email: string; password: string; name: string },
      context: GraphQLContext
    ) {
      // 1
      const password = await hash(args.password, 10)
 
      // 2
      const user = await context.prisma.user.create({
        data: { ...args, password }
      })
 
      // 3
      const token = sign({ userId: user.id }, APP_SECRET)
 
      // 4
      return { token, user }
    }
  }
}

Let's use the good ol' numbered comments again to understand what's going on here – starting with signup.

  1. In the signup mutation, the first thing to do is encrypt the User's password using the bcryptjs library which you'll install soon.
  2. The next step is to use your PrismaClient instance (via prisma as we covered in the steps about context) to store the new User record in the database.
  3. You're then generating a JSON Web Token which is signed with an APP_SECRET. You still need to create this APP_SECRET and also install the jwt library that's used here.
  4. Finally, you return the token and the user in an object that adheres to the shape of an AuthPayload object from your GraphQL schema.

You can now open GraphiQL and play around with your new schema and resolvers, try to run the following mutations:

mutation {
  signup(email: "test@mail.com", name: "Dotan Simha", password: "123456") {
    token
    user {
      id
      name
      email
    }
  }
}

signup mutation

Implementing login resolvers

Now, the login mutation, add it under the signup resolvers.

Add the following right under the signup mutation:

// ... other imports ...
import { compare, hash } from 'bcryptjs'
 
const resolvers = {
  // ... other resolver maps ...
  Mutation: {
    // ... other Mutation field resolvers ...
    async login(
      parent: unknown,
      args: { email: string; password: string },
      context: GraphQLContext
    ) {
      // 1
      const user = await context.prisma.user.findUnique({
        where: { email: args.email }
      })
      if (!user) {
        throw new Error('No such user found')
      }
 
      // 2
      const valid = await compare(args.password, user.password)
      if (!valid) {
        throw new Error('Invalid password')
      }
 
      const token = sign({ userId: user.id }, APP_SECRET)
 
      // 3
      return { token, user }
    }
  }
}

And if you'll open Playground, you should be able to login with the user you previously created:

mutation {
  login(email: "test@mail.com", password: "123456") {
    token
    user {
      id
      name
      email
    }
  }
}

login mutation

⚠️

You should be able to get the information of the user.

Please save the authentication token you get, we'll need that on the next step!

Now on the login mutation!

  1. Instead of creating a new User object, you're now using your PrismaClient instance to retrieve an existing User record by the email address that was sent along as an argument in the login mutation. If no User with that email address was found, you're returning a corresponding error.
  2. The next step is to compare the provided password with the one that is stored in the database. If the two don't match, you're returning an error as well.
  3. In the end, you're returning token and user again.

Detecting the current user

Now, you have our users' database ready to use, and our next step is to be able to detect who's the current user that queries the server.

To do that, you'll need to add the option to pass the authentication token along with our GraphQL operation.

You are not going to use the GraphQL schema in this case, since you don't want to mix the authentication flow with the GraphQL contract that you have. So you'll use HTTP headers.

The authentication token will be passed as a HTTP header, in the following form:

Authorization: "Bearer MY_TOKEN_HERE"

To add support for this kind of authentication in our server, you'll need to be able to access the raw incoming HTTP request, then verify the token and identify the current user.

You also want to be able to tell who's the current authenticated user within our resolvers, so you'll inject the current user into the GraphQL context.

So let's do that:

You'll now modify the context building phase of your GraphQL server, by detecting the current authenticated user. Use the following code in src/auth.ts and add a function for that:

src/auth.ts
import { JwtPayload, verify } from 'jsonwebtoken'
import { PrismaClient, User } from '@prisma/client'
 
export const APP_SECRET = 'this is my secret'
 
export async function authenticateUser(
  prisma: PrismaClient,
  request: Request
): Promise<User | null> {
  const header = request.headers.get('authorization')
  if (header !== null) {
    // 1
    const token = header.split(' ')[1]
    // 2
    const tokenPayload = verify(token, APP_SECRET) as JwtPayload
    // 3
    const userId = tokenPayload.userId
    // 4
    return await prisma.user.findUnique({ where: { id: userId } })
  }
 
  return null
}

So what happened here?

  1. Take the Authorization for the incoming HTTP request headers.
  2. Use verify of jsonwebtoken to check that the token is valid, and extract the userId from the token payload.
  3. Use Prisma API to fetch the user from the database.
  4. Return the current user, or null in case of missing/invalid token.

Now, modify your createContext function in src/context.ts function to call this function:

src/context.ts
import { YogaInitialContext } from 'graphql-yoga'
import { PrismaClient, User } from '@prisma/client'
import { authenticateUser } from './auth'
 
const prisma = new PrismaClient()
 
export type GraphQLContext = {
  prisma: PrismaClient
  currentUser: null | User
}
 
export async function createContext(initialContext: YogaInitialContext): Promise<GraphQLContext> {
  return {
    prisma,
    currentUser: await authenticateUser(prisma, initialContext.request)
  }
}

Now, every incoming GraphQL request that has a valid token and a user, will also have the context.currentUser available with the authenticated user details. If an incoming request doesn't have that, the context.currentUser will be set to null.

So that's really cool, and to test that, you can add a new GraphQL field under type Query called me that just exposes the current user information.

Start by adding the me field to the GraphQL schema under Query:

type Query {
  hello: String!
  feed: [Link!]!
  me: User!
}

And then implement the resolver for this new field:

const resolvers = {
  // ... other resolver maps ...
  Query: {
    // ... other Query Object Type field resolver functions ...
    me(parent: unknown, args: {}, context: GraphQLContext) {
      if (context.currentUser === null) {
        throw new Error('Unauthenticated!')
      }
 
      return context.currentUser
    }
  }
}

You can now try it in GraphiQL with the following query:

query {
  me {
    id
    name
  }
}

And under the HEADERS section of GraphiQL, add your authentication token in the following structure:

{
  "Authorization": "Bearer YOUR_TOKEN_HERE"
}

And if you'll run it, you'll see that the GraphQL server now being able to authenticate you based on the token!

me query

Connecting other resolvers

If you remember, you added more new fields to the GraphQL schema (such as Link.postedBy), so let's implement the missing resolvers!

To make sure our server knows how to identify the creator of each Link, let's modify the resolver of Mutation.post to ensure that only authenticated users can use it, and also add the current authenticated user id to the object created on our database.

Protecting resolvers

Let's go and finish up the implementation, and connect everything together with the rest of the resolvers.

Modify src/schema.ts and change the resolver of post field to the following:

src/schema.ts
const resolvers = {
  Mutation: {
    async post(
      parent: unknown,
      args: { url: string; description: string },
      context: GraphQLContext
    ) {
      if (context.currentUser === null) {
        throw new Error('Unauthenticated!')
      }
 
      const newLink = await context.prisma.link.create({
        data: {
          url: args.url,
          description: args.description,
          postedBy: { connect: { id: context.currentUser.id } }
        }
      })
 
      return newLink
    }
  }
}

You can now try it from GraphiQL:

mutation {
  post(url: "www.graphqlconf.org", description: "An awesome GraphQL conference") {
    id
  }
}

carrying the authentication token

Resolving relations

There's one more thing you need to do before you can launch the GraphQL server again and test the new functionality: ensuring the relation between User and Link gets properly resolved.

To resolve the postedBy relation, open src/schema.ts and add the following code to your resolvers:

src/schema.ts
const resolvers = {
  Link: {
    id: (parent: Link) => parent.id,
    description: (parent: Link) => parent.description,
    url: (parent: Link) => parent.url,
    postedBy(parent: Link, args: {}, context: GraphQLContext) {
      if (!parent.postedById) {
        return null
      }
 
      return context.prisma.link.findUnique({ where: { id: parent.id } }).postedBy()
    }
  }
}

In the postedBy resolver, you're first fetching the Link from the database using the prisma instance and then invoke postedBy on it. Notice that the resolver needs to be called postedBy because it resolves the postedBy field from the Link type in our type-definitions.

You can resolve the links relation in a similar way.

In src/schema.ts, add a field resolvers for User.links to your resolvers variable:

src/schema.ts
// ... other imports ...
import { Link, User } from '@prisma/client'
 
const resolvers = {
  // ... other resolver maps ...
  User: {
    // ... other User object type field resolver functions ...
    links: (parent: User, args: {}, context: GraphQLContext) =>
      context.prisma.user.findUnique({ where: { id: parent.id } }).links()
  }
}

That's all! Now you have resolvers for all fields, and you can sign up, login, identify the user as part of our GraphQL server!

You should be able to run complex GraphQL queries, for example:

query {
  feed {
    id
    description
    url
    postedBy {
      id
      name
    }
  }
}

linked types example