MaintainGraphcool to Prisma

Authentication & Authorization

Overview

In this section, you'll learn how you can migrate your authentication functionality from the Graphcool Framework to Prisma.

Authentication & Authorization with the Graphcool Framework

With the Graphcool Framework, authentication is implemented using resolver functions. The GraphQL schema of the API is extended with dedicated mutations that provide signup and login functionality.

To achieve a cleaner architecture and better separation of concern, with Prisma this functionality is now moved into the application layer.

With the Graphcool Framework, authentication involved the following steps:

  1. Define your signup and login resolver functions as schema extension on the Mutation type in your GraphQL schema
  2. Provide the implementation of the resolver functions in JavaScript directly through the Graphcool Framework or use a webhook to invoke a function hosted by yourself
  3. Connect the mutation definitions with the implementation by adjusting your service definition file

To secure data access and define permission rules, the Graphcool Framework used the concept of permission queries. Each API operation could be associated with one (or more) permission rules that would be checked before performing the operations.

Authentication with Prisma

Prisma has a different authentication concept than the Graphcool Framework. Rather than tying authentication to a permission system, Prisma only provides a simple token-based (think: API Key) system for accessing the Prisma API.

User authentication and permission rules are now implemented in the application layer of your GraphQL server. The exists function from the prisma-binding package serves a similar purpose as permissions queries inside the Graphcool Framework.

Another major change to the Graphcool Framework is that in Prisma you now generate the JWT tokens for your users yourself, rather than having them generated by graphcool-lib.

Going from the Framework to Prisma

The following instructions assume you're using graphql-yoga as your GraphQL server and that your schema definition is written in SDL.

To learn what the implementation authentication and permissions with Prisma looks like in practice, you can check out our examples: auth & permissions.

Step 1: Migrate schema definitions

In your Graphcool Framework service, you might have had a schema extension defined that looks as follows:

type Mutation {
  signupUser(email: String!, password: String!): SignupUserPayload
  authenticateUser(email: String!, password: String!): AuthenticateUserPayload
}

type SignupUserPayload{
  userId: ID!
  token: String!
}

type AuthenticateUserPayload {
  token: String!
}

The signupUser and authenticateUser mutations can be used for signup and login. The returned token is a JSON web token that's generated by graphcool-lib and can be sent in the Authorization HTTP header to authenticate requests against the API.

You can now move these definitions into the schema definition of your graphql-yoga server. Note that you can also get rid of one workaround that was required due to the limitation that resolver functions were not able to return model types.

Here is a suggestion for what your new defintion could look like in your GraphQL server:

type Mutation {
  signup(email: String!, password: String!): AuthPayload
  login(email: String!, password: String!): AuthPayload
}

type AuthPayload {
  token: String!
  user: User!
}

Step 2: Migrate resolver functions

Next, you need to implement the resolvers for the above defined signup and login mutations. In these resolvers, you're now generating a JWT token which you then return to your users. In the signup resolver, you also create a new node of the User type.

auth.js:

const bcrypt = require('bcryptjs')
const jwt = require('jsonwebtoken')

const auth = {
  async signup(parent, args, ctx, info) {
    const password = await bcrypt.hash(args.password, 10)
    const user = await ctx.db.mutation.createUser({
      data: { ...args, password },
    })

    return {
      token: jwt.sign({ userId: user.id }, process.env.JWT_SECRET),
      user,
    }
  },

  async login(parent, { email, password }, ctx, info) {
    const user = await ctx.db.query.user({ where: { email } })
    if (!user) {
      throw new Error(`No such user found for email: ${email}`)
    }

    const valid = await bcrypt.compare(password, user.password)
    if (!valid) {
      throw new Error('Invalid password')
    }

    return {
      token: jwt.sign({ userId: user.id }, process.env.JWT_SECRET),
      user,
    }
  },
}

module.exports = { auth }

AuthPayload.js:

const AuthPayload = {
  user: async ({ user: { id } }, args, ctx, info) => {
    return ctx.db.query.user({ where: { id } }, info)
  },
}

module.exports = { AuthPayload }

Step 3: Migrate permission rules

As mentioned before, the exists function of the prisma-binding package is the tool of choice for migrating your permission queries.

Assume you have the following datamodel defined for your Prisma service

type User @model {
  id: ID! @unique
  name: String!
  posts: [Post!]!
}

type Post @model {
  id: ID! @unique
  title: String!
  author: User!
}

With the Graphcool Framework, to express that you only wanted the author of a Post to be able to update it, you'd have to associate the following permission query with the updatePost mutation:

query ($user_id: ID!, $post_id: ID!) {
  SomePostExists(filter: {
    id: $post_id
    author: {
      id: $user_id
    }
  })
}

With Prisma, you'll need to perform the check inside the corresponding updatePost resolver in the application layer.

The implementation of the resolver could looks as follows:

async function updatePost(parent, { id, title, text }, ctx, info) {
   // `getUserId` throws an error if the requesting user is not authenticated
  const userId = getUserId(ctx)

  // this expresses the same condition as the permission query above
  const requestingUserIsAuthor = await ctx.db.exists.Post({
    id,
    author: {
      id: userId,
    },
  })

  // only if the condition is true, the post is actually updated
  if (requestingUserIsAuthor) {
    return await ctx.db.mutation.updatePost({
      where: { id },
      data: { title, text },
    }, info)
  }
  throw new Error(
    'Invalid permissions, you must be an admin or the author of a post to update it',
  )
}

The getUserId function could be implemented as follows:

function getUserId(ctx) {
  const Authorization = ctx.request.get('Authorization')
  if (Authorization) {
    const token = Authorization.replace('Bearer ', '')
    const { userId } = jwt.verify(token, process.env.APP_SECRET)
    return userId
  }

  throw new AuthError()
}

class AuthError extends Error {
  constructor() {
    super('Not authorized')
  }
}

It reads the Authorization field from the incoming HTTP header which contains a JWT. If the JWT is valid, if retrieves the userId from it, otherwise it throws an error.