GraphQL - Authenticating Client
GraphQL - Authentication
Section titled “GraphQL - Authentication”Authentication is the process of verifying a user’s identity. Protecting your GraphQL API often involves ensuring that only authenticated (and sometimes authorized) users can perform certain operations or access specific data.
GraphQL itself doesn’t prescribe a specific authentication mechanism. Authentication is typically handled outside of GraphQL, often using standard web techniques like tokens (JWT, OAuth), sessions, or API keys. The authenticated user’s identity is then made available to your GraphQL resolvers via the context object.
Common Approach: JWT Authentication
Section titled “Common Approach: JWT Authentication”A popular method is using JSON Web Tokens (JWT). The flow generally works like this:
- Login: The user provides credentials (e.g., email/password) to a traditional REST endpoint (e.g.,
/login) or a dedicated GraphQL mutation. - Token Issuance: If credentials are valid, the server generates a JWT containing user information (like user ID, roles) and signs it with a secret key.
- Token Storage: The server sends the JWT back to the client, which stores it securely (e.g., in
localStorage,sessionStorage, or memory). - Authenticated Requests: For subsequent requests to the GraphQL endpoint, the client includes the JWT in an HTTP header (commonly
Authorization: Bearer <token>). - Token Verification: A middleware layer on the server intercepts incoming requests, extracts the token from the header, verifies its signature using the secret key, and decodes the user information.
- Context Population: The verified user information is added to the GraphQL
contextobject for that request. - Resolver Access: Resolvers can access the user information from the
contextto perform authentication checks or fetch user-specific data.
Illustration: JWT Auth with Apollo Server & Express
Section titled “Illustration: JWT Auth with Apollo Server & Express”We’ll build a server with a protected query and a /login endpoint, then show how clients can authenticate.
Setting up the Server
Section titled “Setting up the Server”Step 1: Project Setup & Dependencies
Section titled “Step 1: Project Setup & Dependencies”Create a project (auth-server-app), initialize npm, and install dependencies:
mkdir auth-server-appcd auth-server-appnpm init -ynpm install @apollo/server graphql express cors body-parser jsonwebtoken# Optional: npm install --save-dev nodemonWe added jsonwebtoken for creating and verifying JWTs.
Step 2: Mock Data (Optional)
Section titled “Step 2: Mock Data (Optional)”Create a simple mock user store (e.g., db.js). In a real app, this would be a database.
// db.js (Mock Data)const users = [ { id: 'user-1', email: 'test@example.com', password: 'password123', // In real apps, store hashed passwords! firstName: 'Test', lastName: 'User' }];
export const findUserByEmail = (email) => users.find(user => user.email === email);export const findUserById = (id) => users.find(user => user.id === id);// Or use module.exports if not using ES ModulesStep 3: Define Schema (schema.graphql or in server.js)
Section titled “Step 3: Define Schema (schema.graphql or in server.js)”Define a query that should require authentication:
#graphqltype User { id: ID! email: String! firstName: String}
type Query { # This query requires authentication me: User # Public query (optional) publicGreeting: String}Step 4: Create Resolvers (resolvers.js)
Section titled “Step 4: Create Resolvers (resolvers.js)”The me resolver will check the context for an authenticated user.
// resolvers.jsimport { GraphQLError } from 'graphql';import { findUserById } from './db.js'; // Assuming db.js uses export
const resolvers = { Query: { // Resolver for the protected 'me' query me: (parent, args, context, info) => { // Check if user information exists in the context if (!context.user) { // Throw an authentication error if not logged in throw new GraphQLError('User is not authenticated', { extensions: { code: 'UNAUTHENTICATED' }, }); } // Return the user data from context return context.user; }, publicGreeting: () => 'Hello from the public API!', },};
export default resolvers; // Or module.exportsStep 5: Set up Server, Middleware & Login Route (server.js)
Section titled “Step 5: Set up Server, Middleware & Login Route (server.js)”Configure Apollo Server, add middleware to verify JWTs and populate context, and create the /login endpoint.
import { ApolloServer } from '@apollo/server';import { expressMiddleware } from '@apollo/server/express4';import express from 'express';import http from 'http';import cors from 'cors';import bodyParser from 'body-parser';import jwt from 'jsonwebtoken';import { readFileSync } from 'fs';
// Mock DB functions (assuming db.js uses export)import { findUserByEmail, findUserById } from './db.js';// import resolvers from './resolvers.js'; // If using export defaultconst resolvers = require('./resolvers'); // If using module.exports
const typeDefs = readFileSync('./schema.graphql', { encoding: 'utf-8' });
// Secret key for signing JWTs (store securely in env variables in real apps!)const JWT_SECRET = 'your-very-secret-key-change-me';
const app = express();const httpServer = http.createServer(app);
// Middleware to parse JSON and enable CORSapp.use(cors());app.use(bodyParser.json());
// --- Authentication Middleware ---// This middleware runs before the GraphQL middlewareapp.use((req, res, next) => { const authHeader = req.headers.authorization || ''; const token = authHeader.startsWith('Bearer ') ? authHeader.substring(7) : null;
if (token) { try { // Verify the token const decoded = jwt.verify(token, JWT_SECRET); // Add decoded user ID to the request object (to be used in context) req.userId = decoded.sub; // 'sub' is standard JWT claim for subject (user ID) } catch (err) { console.warn('Invalid token:', err.message); // Don't throw error here, let GraphQL resolver handle unauthenticated state } } next(); // Proceed to the next middleware (GraphQL)});// --- End Authentication Middleware ---
// Apollo Server setupconst server = new ApolloServer({ typeDefs, resolvers,});
await server.start();
// Apply GraphQL middleware, passing authenticated user to contextapp.use( '/graphql', expressMiddleware(server, { context: async ({ req }) => { // If middleware added userId, fetch user details const userId = req.userId; if (userId) { // Fetch user from DB based on ID from token const user = findUserById(userId); return { user }; // Add user object to context } return {}; // Return empty context if no user ID }, }),);
// --- Login Endpoint ---app.post('/login', (req, res) => { const { email, password } = req.body;
if (!email || !password) { return res.status(400).send({ error: 'Email and password required' }); }
const user = findUserByEmail(email);
// IMPORTANT: In real apps, compare hashed passwords! if (!user || user.password !== password) { return res.status(401).send({ error: 'Invalid credentials' }); }
// Credentials valid: Generate JWT const token = jwt.sign( { sub: user.id, email: user.email }, // Payload: standard 'sub' claim for user ID JWT_SECRET, { expiresIn: '1h' } // Token expires in 1 hour );
// Send token back to client res.send({ token });});// --- End Login Endpoint ---
const PORT = 9000;await new Promise((resolve) => httpServer.listen({ port: PORT }, resolve));console.log(`🚀 Server ready at http://localhost:${PORT}`);console.log(`🔓 Login endpoint at POST http://localhost:${PORT}/login`);console.log(`🧠 GraphQL endpoint at http://localhost:${PORT}/graphql`);Key points:
- A
JWT_SECRETis defined (use environment variables in production!). - An Express middleware extracts the
Authorization: Bearer <token>header. - It verifies the token using
jwt.verifyand attaches theuserIdfrom the token payload (subclaim) to thereqobject. - The
expressMiddleware’scontextfunction checks forreq.userId. If present, it fetches the full user details (mocked here) and adds theuserobject to the GraphQL context. - The
/loginendpoint validates credentials (using plaintext password here - NEVER DO THIS IN PRODUCTION, use hashing!) and issues a signed JWT usingjwt.signcontaining the user ID (sub).
Step 6: Run the Server
Section titled “Step 6: Run the Server”Add start scripts to package.json and run npm run dev or npm start.
Testing Authentication
Section titled “Testing Authentication”-
Try Protected Query without Token: Open Apollo Sandbox (
http://localhost:9000/graphql) and runquery { me { id email } }. You should get an “User is not authenticated” error. -
Login: Use a tool like
curl, Postman, or Insomnia to send a POST request tohttp://localhost:9000/loginwith a JSON body:
{ "email": "test@example.com", "password": "password123"}You should receive a response like {"token": "eyJhbGciOi..."}. Copy the token value.
- Try Protected Query with Token: Go back to Apollo Sandbox. Find the ‘Headers’ tab or section. Add a new header:
- Header Name:
Authorization - Header Value:
Bearer <paste_your_token_here>(Replace<paste_your_token_here>with the actual token)
Now, re-run the query { me { id email } }. This time, it should succeed and return the user’s data.
Client-Side Implementation
Section titled “Client-Side Implementation”1. Basic Fetch / jQuery Client
Section titled “1. Basic Fetch / jQuery Client”In a simple HTML/JS client (like the one in the jQuery Integration chapter):
- Login: Add form fields for email/password. On submit, use
fetchto POST credentials to the/loginendpoint. - Store Token: On successful login, store the received token (e.g., in
localStorageor a variable). - Send Token: When making GraphQL requests using
fetchto/graphql, include theAuthorization: Bearer <token>header in theheadersoption.
// Example: Fetching protected data after loginconst graphqlQuery = { query: '{ me { id firstName } }' };const storedToken = localStorage.getItem('authToken'); // Get token from storage
fetch('http://localhost:9000/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', // Add the Authorization header if token exists ...(storedToken && { 'Authorization': `Bearer ${storedToken}` }) }, body: JSON.stringify(graphqlQuery)}).then(response => response.json()).then(data => { if (data.errors) { // Handle GraphQL errors (e.g., UNAUTHENTICATED) console.error('GraphQL Error:', data.errors); if (data.errors.some(err => err.extensions?.code === 'UNAUTHENTICATED')){ alert('Please log in again.'); // Redirect to login or clear token localStorage.removeItem('authToken'); } } else { // Process successful data console.log('User data:', data.data.me); // Update UI }}).catch(error => { // Handle network errors console.error('Network Error:', error);});2. React Client with Apollo Client
Section titled “2. React Client with Apollo Client”Apollo Client provides a more robust way using Apollo Link middleware.
a) Install Link library: npm install @apollo/client @apollo/link-context graphql (if not already installed)
b) Configure Apollo Client with Auth Link:
// In your Apollo Client setup file (e.g., main.jsx or a dedicated apollo.js)import { ApolloClient, InMemoryCache, ApolloProvider, createHttpLink // Use createHttpLink instead of just uri} from '@apollo/client';import { setContext } from '@apollo/link-context'; // Import setContext
// Standard HTTP linkconst httpLink = createHttpLink({ uri: 'http://localhost:9000/graphql',});
// Middleware link to set the Authorization headerconst authLink = setContext((_, { headers }) => { // Get the authentication token from local storage if it exists const token = localStorage.getItem('authToken'); // Return the headers to the context so httpLink can read them return { headers: { ...headers, // Spread existing headers authorization: token ? `Bearer ${token}` : "", // Add Authorization header } }});
// Create the Apollo Client instance using the link chainconst client = new ApolloClient({ link: authLink.concat(httpLink), // Chain authLink and httpLink cache: new InMemoryCache(),});
// ... rest of your setup (e.g., wrap App in ApolloProvider)c) Login/Logout Logic:
- Create a login component that uses
fetch(or a dedicateduseMutationhook if you create a login mutation) to call the/loginendpoint. - On successful login, store the token in
localStorage.setItem('authToken', receivedToken);. - Use
client.resetStore()orclient.refetchQueries()after login/logout to clear the cache and refetch data with the new auth status. - On logout, remove the token
localStorage.removeItem('authToken');and reset the store.
With the authLink configured, Apollo Client will automatically include the token in the headers for all subsequent GraphQL operations.
Security Considerations
Section titled “Security Considerations”- HTTPS: Always use HTTPS in production.
- Secret Keys: Store JWT secrets securely (environment variables, secrets management).
- Password Hashing: NEVER store plaintext passwords. Use strong hashing algorithms (e.g., bcrypt).
- Token Expiration: Set reasonable expiration times for JWTs.
- Token Storage: Be mindful of where you store tokens on the client (localStorage is common but susceptible to XSS; HttpOnly cookies are generally more secure for web apps if feasible).
- Authorization: Authentication verifies who the user is; authorization determines what they are allowed to do. Implement authorization checks within resolvers based on user roles or permissions (often stored in the token or fetched based on user ID).