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:
- First, install the required dependencies:
npm install jsonwebtoken bcryptjs
- 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
}
}
}
}
- 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
-
Token Security
- Use secure, random secrets
- Set appropriate token expiration
- Store tokens securely
- Use HTTPS
-
Password Security
- Hash passwords with bcrypt
- Enforce strong password policies
- Implement rate limiting
- Use secure password reset flows
-
API Security
- Implement rate limiting
- Use CORS properly
- Validate all inputs
- Log security events
-
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.