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:
- Define your signup and login resolver functions as schema extension on the
Mutation
type in your GraphQL schema - 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
- 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.