Secure GraphQL APIs - authentication, authorization, rate limiting, and validation
Implement JWT authentication, role-based authorization, rate limiting, and query complexity limits for GraphQL APIs. Use it when securing production GraphQL endpoints or adding auth to existing schemas.
/plugin marketplace add pluginagentmarketplace/custom-plugin-graphql/plugin install developer-roadmap@pluginagentmarketplace-graphqlThis skill inherits all available tools. When active, it can use any tool Claude has access to.
assets/config.yamlassets/schema.jsonreferences/GUIDE.mdreferences/PATTERNS.mdscripts/validate.pyProtect your GraphQL APIs from attacks
Learn essential security patterns for GraphQL: JWT authentication, role-based authorization, rate limiting, query complexity limits, and input validation.
| Check | Priority | Implementation |
|---|---|---|
| Authentication | Critical | JWT with refresh tokens |
| Authorization | Critical | Field-level with graphql-shield |
| Rate Limiting | Critical | Per-user/IP with Redis |
| Query Depth | High | graphql-depth-limit |
| Query Complexity | High | graphql-query-complexity |
| Introspection | High | Disable in production |
| Input Validation | High | Validate all inputs |
| Error Masking | Medium | Hide internal errors |
import jwt from 'jsonwebtoken';
// Token creation
function createTokens(user) {
const accessToken = jwt.sign(
{ userId: user.id, roles: user.roles },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ userId: user.id },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '7d' }
);
return { accessToken, refreshToken };
}
// Context setup
const context = async ({ req }) => {
const token = req.headers.authorization?.replace('Bearer ', '');
let user = null;
if (token) {
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
user = await db.users.findById(payload.userId);
} catch (e) {
// Token invalid or expired
}
}
return { user };
};
// Login resolver
const resolvers = {
Mutation: {
login: async (_, { email, password }) => {
const user = await db.users.findByEmail(email);
if (!user || !await bcrypt.compare(password, user.passwordHash)) {
throw new GraphQLError('Invalid credentials', {
extensions: { code: 'UNAUTHORIZED' }
});
}
return { ...createTokens(user), user };
},
},
};
import { rule, shield, and, or } from 'graphql-shield';
// Rules
const isAuthenticated = rule()((_, __, { user }) => user !== null);
const isAdmin = rule()((_, __, { user }) =>
user?.roles?.includes('ADMIN')
);
const isOwner = rule()(async (_, { id }, { user, dataSources }) => {
const resource = await dataSources.findById(id);
return resource?.userId === user?.id;
});
// Permissions
const permissions = shield({
Query: {
me: isAuthenticated,
users: and(isAuthenticated, isAdmin),
user: and(isAuthenticated, or(isOwner, isAdmin)),
},
Mutation: {
updateUser: and(isAuthenticated, or(isOwner, isAdmin)),
deleteUser: and(isAuthenticated, isAdmin),
},
User: {
email: or(isOwner, isAdmin),
privateField: isOwner,
},
}, {
fallbackError: new GraphQLError('Not authorized'),
});
// Apply
import { applyMiddleware } from 'graphql-middleware';
const protectedSchema = applyMiddleware(schema, permissions);
// Express-level (basic)
import rateLimit from 'express-rate-limit';
app.use('/graphql', rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100,
keyGenerator: (req) => req.user?.id || req.ip,
}));
// GraphQL-level (granular)
const typeDefs = gql`
directive @rateLimit(max: Int!, window: String!) on FIELD_DEFINITION
type Mutation {
login(email: String!, password: String!): AuthPayload!
@rateLimit(max: 5, window: "15m")
sendEmail(input: SendEmailInput!): Boolean!
@rateLimit(max: 10, window: "1h")
}
`;
import depthLimit from 'graphql-depth-limit';
import { createComplexityLimitRule } from 'graphql-validation-complexity';
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
// Max depth of 10
depthLimit(10),
// Max complexity of 1000
createComplexityLimitRule(1000, {
scalarCost: 1,
objectCost: 2,
listFactor: 10,
}),
],
// Disable introspection in production
introspection: process.env.NODE_ENV !== 'production',
});
import validator from 'validator';
import xss from 'xss';
const validate = {
email: (v) => {
if (!validator.isEmail(v)) throw new Error('Invalid email');
return validator.normalizeEmail(v);
},
password: (v) => {
if (v.length < 8) throw new Error('Password too short');
if (!/[A-Z]/.test(v)) throw new Error('Need uppercase');
if (!/[0-9]/.test(v)) throw new Error('Need number');
return v;
},
html: (v) => xss(v),
};
const resolvers = {
Mutation: {
createUser: async (_, { input }) => {
const clean = {
email: validate.email(input.email),
password: validate.password(input.password),
bio: input.bio ? validate.html(input.bio) : null,
};
return db.users.create(clean);
},
},
};
const server = new ApolloServer({
formatError: (error) => {
// Log full error
console.error(error);
// In production, hide internal errors
if (process.env.NODE_ENV === 'production') {
if (error.extensions?.code === 'INTERNAL_SERVER_ERROR') {
return { message: 'Internal error', extensions: { code: 'INTERNAL_ERROR' } };
}
}
return error;
},
});
import helmet from 'helmet';
import cors from 'cors';
app.use(helmet());
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(','),
credentials: true,
}));
app.use(express.json({ limit: '100kb' }));
| Issue | Cause | Solution |
|---|---|---|
| Token always invalid | Clock skew | Add grace period |
| Rate limit bypass | Wrong key | Use user ID when authenticated |
| Auth not working | Context async | Await context setup |
| Introspection exposed | Wrong env check | Verify NODE_ENV |
# Test introspection (should fail in prod)
curl -X POST $API \
-H "Content-Type: application/json" \
-d '{"query":"{ __schema { types { name } } }"}'
# Test rate limit
for i in {1..20}; do
curl -X POST $API \
-d '{"query":"mutation { login(email:\"x\",password:\"y\") { token } }"}'
done
# Test depth limit (should fail)
curl -X POST $API \
-d '{"query":"{ user { posts { author { posts { author { id } } } } } }"}'
Skill("graphql-security")
graphql-apollo-server - Server configurationgraphql-resolvers - Auth in resolversgraphql-schema-design - Auth-aware schema06-graphql-security - For detailed guidanceThis skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.