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
Link
s are posted by User
s.
Open prisma/schema.prisma
and add the following code, making sure to also update your existing
Link
model accordingly:
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 Link
s.
To do this, you need to also define the relation by annotating the postedBy
field with
the @relation
attribute.
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.
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):
export const APP_SECRET = 'this is my secret'
Implementing signup resolvers
Open src/schema.ts
and add the new signup
resolver, under Mutation
:
// ... 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
.
- In the
signup
mutation, the first thing to do is encrypt theUser
’s password using thebcryptjs
library which you’ll install soon. - The next step is to use your
PrismaClient
instance (viaprisma
as we covered in the steps aboutcontext
) to store the newUser
record in the database. - You’re then generating a JSON Web Token which is signed with an
APP_SECRET
. You still need to create thisAPP_SECRET
and also install thejwt
library that’s used here. - Finally, you return the
token
and theuser
in an object that adheres to the shape of anAuthPayload
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
}
}
}
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
}
}
}
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!
- Instead of creating a new
User
object, you’re now using yourPrismaClient
instance to retrieve an existingUser
record by theemail
address that was sent along as an argument in thelogin
mutation. If noUser
with that email address was found, you’re returning a corresponding error. - 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.
- In the end, you’re returning
token
anduser
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:
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?
- Take the
Authorization
for the incoming HTTP request headers. - Use
verify
ofjsonwebtoken
to check that the token is valid, and extract theuserId
from the token payload. - Use Prisma API to fetch the user from the database.
- 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:
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!
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:
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
}
}
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:
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:
// ... 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
}
}
}