Skip to main content

Chapter 5: Authentication & Authorization

Security is crucial for any API. In this chapter, we'll explore how to implement authentication and authorization in your GraphQL API using GraphQL Yoga.

Authentication Strategies​

JWT Authentication​

JWT (JSON Web Tokens) is a popular choice for authentication. Here's how to implement it:

  1. First, install the required dependencies:
npm install jsonwebtoken bcryptjs
  1. Create authentication resolvers:
import jwt from 'jsonwebtoken'
import bcrypt from 'bcryptjs'

const JWT_SECRET = process.env.JWT_SECRET!

const resolvers = {
Mutation: {
login: async (_, { email, password }, context) => {
const user = await context.prisma.user.findUnique({
where: { email }
})

if (!user) {
throw new GraphQLError('Invalid credentials')
}

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

const token = jwt.sign(
{ userId: user.id },
JWT_SECRET,
{ expiresIn: '1d' }
)

return {
token,
user
}
},

register: async (_, { input }, context) => {
const hashedPassword = await bcrypt.hash(input.password, 10)

const user = await context.prisma.user.create({
data: {
...input,
password: hashedPassword
}
})

const token = jwt.sign(
{ userId: user.id },
JWT_SECRET,
{ expiresIn: '1d' }
)

return {
token,
user
}
}
}
}
  1. Add authentication to your context:
import { createYoga } from '@graphql-yoga/node'
import jwt from 'jsonwebtoken'

const yoga = createYoga({
schema,
context: async ({ request }) => {
const token = request.headers.get('authorization')?.replace('Bearer ', '')

let user = null
if (token) {
try {
const decoded = jwt.verify(token, JWT_SECRET) as { userId: string }
user = await prisma.user.findUnique({
where: { id: decoded.userId }
})
} catch (error) {
// Token is invalid or expired
}
}

return {
prisma,
user
}
}
})

OAuth Integration​

For OAuth authentication (e.g., with Google, GitHub):

import { OAuth2Client } from 'google-auth-library'

const googleClient = new OAuth2Client(process.env.GOOGLE_CLIENT_ID)

const resolvers = {
Mutation: {
loginWithGoogle: async (_, { token }, context) => {
const ticket = await googleClient.verifyIdToken({
idToken: token,
audience: process.env.GOOGLE_CLIENT_ID
})

const payload = ticket.getPayload()!

let user = await context.prisma.user.findUnique({
where: { email: payload.email }
})

if (!user) {
user = await context.prisma.user.create({
data: {
email: payload.email!,
name: payload.name!,
picture: payload.picture
}
})
}

const jwtToken = jwt.sign(
{ userId: user.id },
JWT_SECRET,
{ expiresIn: '1d' }
)

return {
token: jwtToken,
user
}
}
}
}

Authorization Patterns​

Field-level Authorization​

Control access to specific fields:

const resolvers = {
User: {
email: (parent, _, context) => {
// Only show email to the user themselves or admins
if (context.user?.id !== parent.id && context.user?.role !== 'ADMIN') {
return null
}
return parent.email
},

posts: (parent, _, context) => {
// Only show posts to the user themselves or if they're public
if (context.user?.id !== parent.id) {
return context.prisma.post.findMany({
where: {
authorId: parent.id,
published: true
}
})
}
return context.prisma.post.findMany({
where: { authorId: parent.id }
})
}
}
}

Role-based Access Control (RBAC)​

Implement role-based authorization:

enum UserRole {
ADMIN
MODERATOR
USER
}

// Authorization middleware
const withRole = (roles: UserRole[]) => {
return (resolver: ResolverFn) => {
return async (parent, args, context, info) => {
if (!context.user) {
throw new AuthenticationError('You must be logged in')
}

if (!roles.includes(context.user.role)) {
throw new AuthorizationError('Insufficient permissions')
}

return resolver(parent, args, context, info)
}
}
}

const resolvers = {
Mutation: {
deleteUser: withRole([UserRole.ADMIN])(async (_, { id }, context) => {
return context.prisma.user.delete({
where: { id }
})
}),

moderatePost: withRole([UserRole.ADMIN, UserRole.MODERATOR])(async (_, { id, action }, context) => {
// Moderation logic
})
}
}

Using Envelop Plugins for Auth​

GraphQL Yoga uses Envelop for plugins. Here's how to use it for authentication:

import { useExtendContext } from '@envelop/core'
import { useAuth0 } from '@envelop/auth0'

const yoga = createYoga({
schema,
plugins: [
useExtendContext(async ({ request }) => {
const token = request.headers.get('authorization')?.replace('Bearer ', '')
let user = null

if (token) {
try {
const decoded = jwt.verify(token, JWT_SECRET) as { userId: string }
user = await prisma.user.findUnique({
where: { id: decoded.userId }
})
} catch (error) {
// Token is invalid or expired
}
}

return { user }
}),

// Optional: Use Auth0 plugin
useAuth0({
domain: process.env.AUTH0_DOMAIN!,
audience: process.env.AUTH0_AUDIENCE!
})
]
})

Security Best Practices​

  1. Token Security

    • Use secure, random secrets
    • Set appropriate token expiration
    • Store tokens securely
    • Use HTTPS
  2. Password Security

    • Hash passwords with bcrypt
    • Enforce strong password policies
    • Implement rate limiting
    • Use secure password reset flows
  3. API Security

    • Implement rate limiting
    • Use CORS properly
    • Validate all inputs
    • Log security events
  4. Error Handling

    • Don't expose sensitive information in errors
    • Use appropriate HTTP status codes
    • Log security-related errors

Example: Complete Auth Flow​

Here's a complete example of an authentication flow:

// schema.ts
const typeDefs = `
type User {
id: ID!
email: String!
name: String
role: UserRole!
}

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

enum UserRole {
ADMIN
MODERATOR
USER
}

type Query {
me: User
}

type Mutation {
login(email: String!, password: String!): AuthPayload!
register(input: RegisterInput!): AuthPayload!
loginWithGoogle(token: String!): AuthPayload!
}

input RegisterInput {
email: String!
password: String!
name: String
}
`

// resolvers.ts
const resolvers = {
Query: {
me: (_, __, context) => {
if (!context.user) {
throw new AuthenticationError('Not authenticated')
}
return context.user
}
},

Mutation: {
login: async (_, { email, password }, context) => {
const user = await context.prisma.user.findUnique({
where: { email }
})

if (!user) {
throw new AuthenticationError('Invalid credentials')
}

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

const token = jwt.sign(
{ userId: user.id },
JWT_SECRET,
{ expiresIn: '1d' }
)

return { token, user }
},

register: async (_, { input }, context) => {
const hashedPassword = await bcrypt.hash(input.password, 10)

const user = await context.prisma.user.create({
data: {
...input,
password: hashedPassword,
role: 'USER'
}
})

const token = jwt.sign(
{ userId: user.id },
JWT_SECRET,
{ expiresIn: '1d' }
)

return { token, user }
}
}
}

Summary​

In this chapter, we've covered:

  • JWT authentication implementation
  • OAuth integration
  • Field-level and role-based authorization
  • Using Envelop plugins for auth
  • Security best practices

These patterns will help you build secure GraphQL APIs. In the next chapter, we'll explore error handling in detail.