← Back to DevBytes

GraphQL Yoga Authentication: JWT, Sessions, and OAuth Integration

Authentication in GraphQL Yoga: JWT, Sessions, and OAuth

Authentication is a critical part of any GraphQL API. GraphQL Yoga, a fully-featured GraphQL server library for Node.js, provides a flexible architecture that allows you to integrate various authentication strategies—JSON Web Tokens (JWT), session-based authentication, and OAuth providers. This tutorial will walk you through each method with practical code examples, showing you how to attach authenticated user data to your Yoga context and protect your resolvers.

What is GraphQL Yoga?

GraphQL Yoga is built on top of modern web standards and provides a simple yet powerful way to create GraphQL servers. It supports subscriptions, file uploads, and a plugin system that makes it easy to add authentication logic. In this guide, we'll use Yoga v3 (or later) with its context factory and web-standard APIs.

Why Authentication Matters

Without authentication, your GraphQL API is open to the public, exposing sensitive data and mutations. Implementing authentication allows you to identify users, restrict access to certain queries or mutations, and personalize responses. Yoga's context mechanism lets you attach the authenticated user to every resolver, making authorization straightforward and consistent across your entire schema.

Setting Up a Basic Yoga Server

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

First, let's create a minimal Yoga server to understand where authentication fits. Install the required packages:

npm install graphql-yoga graphql

Create a file server.js:

import { createYoga, createSchema } from 'graphql-yoga'
import { createServer } from 'node:http'

const typeDefs = `
  type Query {
    hello: String
    me: User
  }
  type User {
    id: ID!
    email: String!
  }
`

const resolvers = {
  Query: {
    hello: () => 'Hello world!',
    me: (parent, args, context) => {
      // context will contain the authenticated user later
      return context.user
    },
  },
}

const yoga = createYoga({
  schema: createSchema({ typeDefs, resolvers }),
  // context factory will be added here
})

const server = createServer(yoga)
server.listen(4000, () => {
  console.log('Yoga server running on http://localhost:4000/graphql')
})

Now, the me query returns null until we implement authentication. The context object is built per request via a factory function that we'll soon add.

JWT Authentication

JSON Web Tokens are widely used for stateless authentication. A client obtains a token (usually via a login mutation) and sends it in the Authorization header as Bearer <token>. Yoga's context factory can verify and decode the token, attaching the user to the context for every subsequent resolver.

Installing Dependencies

npm install jsonwebtoken bcryptjs

bcryptjs is used for password hashing in the login mutation.

Creating a Login Mutation

Add a mutation to the schema and resolvers that issues a JWT after validating credentials.

import jwt from 'jsonwebtoken'
import bcrypt from 'bcryptjs'

// Assume you have a User model or database (here a simple in-memory array for demo)
const JWT_SECRET = 'your-secret-key-at-least-32-chars'

const typeDefs = `
  type Query {
    hello: String
    me: User
  }
  type Mutation {
    login(email: String!, password: String!): AuthPayload
  }
  type AuthPayload {
    token: String
    user: User
  }
  type User {
    id: ID!
    email: String!
  }
`

const users = [
  { id: '1', email: 'test@example.com', passwordHash: bcrypt.hashSync('password123', 10) }
]

const resolvers = {
  Query: {
    hello: () => 'Hello world!',
    me: (parent, args, context) => {
      if (!context.user) throw new Error('Not authenticated')
      return context.user
    },
  },
  Mutation: {
    login: async (parent, { email, password }) => {
      const user = users.find(u => u.email === email)
      if (!user) throw new Error('User not found')
      const valid = await bcrypt.compare(password, user.passwordHash)
      if (!valid) throw new Error('Invalid password')
      const token = jwt.sign(
        { userId: user.id, email: user.email },
        JWT_SECRET,
        { expiresIn: '1h' }
      )
      return { token, user: { id: user.id, email: user.email } }
    },
  },
}

Context Factory for JWT

Now we extract the token from the request headers, verify it, and populate context.user. Yoga's context option accepts an async function that receives the incoming request (a standard Fetch API Request object in Yoga v3+).

const yoga = createYoga({
  schema: createSchema({ typeDefs, resolvers }),
  context: async ({ request }) => {
    const authHeader = request.headers.get('authorization')
    if (authHeader) {
      const token = authHeader.replace('Bearer ', '')
      try {
        const decoded = jwt.verify(token, JWT_SECRET)
        // You could fetch the full user from the database using decoded.userId
        // For simplicity, we attach the decoded payload directly
        return { user: { id: decoded.userId, email: decoded.email } }
      } catch (err) {
        // Token invalid or expired – return null user
        return { user: null }
      }
    }
    return { user: null }
  },
})

Now the me query returns the authenticated user. Unauthenticated requests will receive an error because our resolver throws if context.user is null. You can also return a generic "Not authenticated" error to clients.

Protecting Multiple Resolvers with a Wrapper

You can create a higher-order function to wrap resolvers that require authentication, keeping your code DRY:

const authenticate = (resolver) => (parent, args, context, info) => {
  if (!context.user) throw new Error('Authentication required')
  return resolver(parent, args, context, info)
}

// Usage
const resolvers = {
  Query: {
    me: authenticate((parent, args, context) => context.user),
    sensitiveData: authenticate(async () => { /* ... */ }),
  },
}

Session-Based Authentication

Session-based auth uses a server-side session store and a cookie to maintain state. This is common in traditional web applications. Yoga integrates smoothly with Express middleware like express-session.

Setting Up with Express and express-session

npm install express express-session graphql-yoga

Create an Express app, apply session middleware, and mount Yoga as a router.

import express from 'express'
import session from 'express-session'
import { createYoga, createSchema } from 'graphql-yoga'

const app = express()

app.use(session({
  secret: 'session-secret-key',
  resave: false,
  saveUninitialized: false,
  cookie: { secure: false, httpOnly: true, maxAge: 3600000 } // 1 hour
}))

const typeDefs = `
  type Query {
    me: User
  }
  type Mutation {
    login(email: String!, password: String!): User
    logout: Boolean
  }
  type User {
    id: ID!
    email: String!
  }
`

const resolvers = {
  Query: {
    me: (parent, args, context) => {
      const user = context.req.session.user
      if (!user) throw new Error('Not logged in')
      return user
    },
  },
  Mutation: {
    login: async (parent, { email, password }, context) => {
      // Verify credentials (dummy check here)
      if (email === 'test@example.com' && password === 'password123') {
        const user = { id: '1', email }
        context.req.session.user = user
        return user
      }
      throw new Error('Invalid credentials')
    },
    logout: (parent, args, context) => {
      context.req.session.destroy()
      return true
    },
  },
}

const yoga = createYoga({
  schema: createSchema({ typeDefs, resolvers }),
  context: ({ req }) => {
    // Pass the Express req object to context so resolvers can access session
    return { req }
  },
})

app.use('/graphql', yoga)
app.listen(4000, () => console.log('Running on http://localhost:4000/graphql'))

Because Yoga runs on Express, the req object is the standard Express request, complete with session. The context factory simply returns { req }, giving resolvers direct access to session data. Note that in Yoga v3 standalone mode (without Express), session handling would require a custom middleware or cookie-based approach; the Express integration shown here is the most straightforward.

Session vs. JWT

Sessions require server-side storage (memory, Redis, etc.) and a cookie, making them stateful. JWTs are stateless and ideal for mobile apps and SPAs. Choose based on your architecture: sessions for traditional server-rendered apps with cookie-based flows, JWT for APIs consumed by a variety of clients that cannot rely on cookies.

OAuth Integration (Google, GitHub, etc.)

OAuth allows users to authenticate via third-party providers. In a GraphQL context, you typically implement OAuth flows (authorization code grant) using a callback endpoint, then issue a JWT or session for subsequent API requests. Yoga can integrate with Passport.js for a full OAuth dance, or you can handle the token exchange entirely within a GraphQL mutation.

Using Passport.js with Yoga

passport is a popular authentication middleware for Node.js. We'll integrate Google OAuth as an example.

npm install passport passport-google-oauth20 express express-session

Configuring Passport and Express

import passport from 'passport'
import { Strategy as GoogleStrategy } from 'passport-google-oauth20'

// Configure Google strategy
passport.use(new GoogleStrategy({
  clientID: 'YOUR_GOOGLE_CLIENT_ID',
  clientSecret: 'YOUR_GOOGLE_CLIENT_SECRET',
  callbackURL: 'http://localhost:4000/auth/google/callback'
}, (accessToken, refreshToken, profile, done) => {
  // Find or create user in your database using profile.id
  return done(null, profile)
}))

// Serialize user to session (required when using sessions)
passport.serializeUser((user, done) => done(null, user))
passport.deserializeUser((obj, done) => done(null, obj))

Express Routes for OAuth

Create routes that initiate authentication and handle the callback, then redirect or return a token.

app.get('/auth/google',
  passport.authenticate('google', { scope: ['profile', 'email'] })
)

app.get('/auth/google/callback',
  passport.authenticate('google', { failureRedirect: '/login' }),
  (req, res) => {
    // Successful authentication – now we can issue a JWT or set session
    // Option 1: Set session and redirect
    req.session.user = req.user
    res.redirect('/')
    // Option 2: Generate a JWT and send it as a response or redirect with token
    // const token = jwt.sign({ userId: req.user.id, email: req.user.emails[0].value }, JWT_SECRET)
    // res.redirect(`/auth-success?token=${token}`)
  }
)

Integrating with Yoga Context

Similar to the session-based approach, we pass the req object to Yoga's context. Resolvers can access context.req.user (Passport attaches the user to req when sessions are configured). Alternatively, if you choose to generate a JWT after OAuth login, the client stores that token and sends it as a Bearer token for all subsequent requests—this brings us back to the JWT pattern described earlier.

GraphQL Mutation for OAuth Token Exchange

You can handle OAuth entirely within GraphQL by accepting an authorization code or access token in a mutation, verifying it, and returning a JWT. This avoids redirects and works exceptionally well with single-page applications.

type Mutation {
  oauthLogin(provider: String!, code: String!): AuthPayload
}

The resolver verifies the code with the provider's token endpoint, fetches the user profile, creates or finds the user, and returns a JWT.

Implementation using axios and Google's token endpoint (simplified):

import axios from 'axios'
import jwt from 'jsonwebtoken'

const resolvers = {
  Mutation: {
    oauthLogin: async (parent, { provider, code }) => {
      if (provider === 'google') {
        // Exchange code for tokens
        const { data } = await axios.post('https://oauth2.googleapis.com/token', {
          code,
          client_id: process.env.GOOGLE_CLIENT_ID,
          client_secret: process.env.GOOGLE_CLIENT_SECRET,
          redirect_uri: 'postmessage', // or your configured redirect URI
          grant_type: 'authorization_code'
        })
        // data contains access_token, id_token, etc.
        // Verify the id_token or use access_token to fetch user info
        // Here we simply decode the id_token (using jwt.decode or proper verification)
        const decoded = jwt.decode(data.id_token)
        const user = {
          id: decoded.sub,
          email: decoded.email,
          name: decoded.name
        }
        // Create or update user in your database…
        const token = jwt.sign({ userId: user.id, email: user.email }, JWT_SECRET, { expiresIn: '1h' })
        return { token, user }
      }
      throw new Error('Unsupported provider')
    },
  },
}

This approach keeps the authentication flow entirely within your GraphQL API, which is neat and frontend-framework agnostic.

Best Practices

Conclusion

GraphQL Yoga's flexible context factory makes it straightforward to integrate JWT, sessions, or OAuth authentication. By attaching the authenticated user to the context, you can protect resolvers and personalize responses with minimal overhead. The choice between JWT and sessions depends on your application's architecture, while OAuth integration can be handled either via Express middleware or entirely within GraphQL mutations. Following security best practices ensures your API remains robust and trustworthy, whether you're building a simple side project or a production-grade service.

Now you're ready to secure your Yoga-powered GraphQL API with confidence, using the authentication strategy that best fits your needs.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles