graphql-security
from pluginagentmarketplace/custom-plugin-graphql
GraphQL Development Plugin
1 stars0 forksUpdated Jan 5, 2026
npx skills add https://github.com/pluginagentmarketplace/custom-plugin-graphql --skill graphql-securitySKILL.md
GraphQL Security Skill
Protect your GraphQL APIs from attacks
Overview
Learn essential security patterns for GraphQL: JWT authentication, role-based authorization, rate limiting, query complexity limits, and input validation.
Security Checklist
| 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 |
Core Patterns
1. JWT Authentication
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 };
},
},
};
2. Authorization with graphql-shield
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);
3. Rate Limiting
// 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")
}
`;
4. Query Limits
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',
});
5. Input Validation
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);
},
},
};
6. Error Masking
const server = new A
...
Repository Stats
Stars1
Forks0
LicenseOther