Защита GraphQL API с помощью JWT в Express и Apollo
Введение
GraphQL — гибкая альтернатива традиционным REST API. Она позволяет клиенту запрашивать ровно те поля, которые нужны. Эта гибкость упрощает разработку, но одновременно меняет модель рисков: вместо множества конечных точек у вас один эндпоинт, который должен реализовывать тонкую и надёжную проверку прав доступа.
JSON Web Tokens (JWT) — распространённый механизм для передачи утверждений об аутентификации и авторизации между клиентом и сервером. В статье показано, как интегрировать JWT в стек Express + Apollo Server, как проверять роли (например, Admin) и какие дополнительные меры безопасности стоит применять.
Важно: кодовые примеры используют минимальную реализацию для демонстрации концепций. Перед применением в продакшене добавьте шифрование паролей, управление отказоустойчивостью, мониторинг и безопасное хранение секретов.
Аутентификация и авторизация в GraphQL
В GraphQL обычно один эндпоинт, поэтому доступ к разным данным должен отличаться в зависимости от пользователя и его ролей. Неправильная проверка прав приводит к уязвимостям с нарушением контроля доступа. Лучше реализовать центральную проверку токенов в контексте запроса и затем использовать роль или другие утверждения из токена для фильтрации данных.
Рекомендация: проверяйте права не только на уровне Query/Mutation, но и на уровне полей, если поля возвращают чувствительную информацию.
Вы можете найти код проекта в репозитории автора (ссылка должна быть добавлена разработчиком проекта).
Быстрая установка проекта (Express + Apollo)
- Создайте папку проекта и перейдите в неё:
mkdir graphql-API-jwt
cd graphql-API-jwt- Инициализируйте npm:
npm init --yes- Установите зависимости (обратите внимание на корректные имена пакетов):
npm install apollo-server graphql mongoose jsonwebtoken dotenvПримечание: в примерах часто встречается пакет jsonwebtoken. Убедитесь, что в package.json указан он, а не ошибочные варианты.
- Создайте файл server.js в корне и вставьте этот минимальный сервер:
const { ApolloServer } = require('apollo-server');
const mongoose = require('mongoose');
require('dotenv').config();
const typeDefs = require("./graphql/typeDefs");
const resolvers = require("./graphql/resolvers");
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => ({ req }),
});
const MONGO_URI = process.env.MONGO_URI;
mongoose
.connect(MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => {
console.log("Connected to DB");
return server.listen({ port: 5000 });
})
.then((res) => {
console.log(`Server running at ${res.url}`);
})
.catch(err => {
console.log(err.message);
});Пояснение: опция context передаёт объект запроса в резолверы. Там мы будем читать заголовок Authorization и проверять JWT.
Настройка MongoDB
Для подключения создайте базу данных в MongoDB или кластер в MongoDB Atlas. В корне проекта добавьте файл .env:
MONGO_URI=""
SECRET_KEY='' Храните секретный ключ в безопасном хранилище (Vault, AWS KMS, Azure Key Vault). Не коммитите .env в git.
Определение модели данных (Mongoose)
Создайте файл models/user.js:
const {model, Schema} = require('mongoose');
const userSchema = new Schema({
name: String,
password: String,
role: String
});
module.exports = model('user', userSchema);Важно: в примере пароли хранятся в открытом виде для простоты. В продакшене используйте bcrypt/argon2 для хеширования паролей и сопутствующие меры (соль, work factor).
Определение схемы GraphQL
Создайте папку graphql и файлы typeDefs.js и resolvers.js.
В файле graphql/typeDefs.js:
const { gql } = require("apollo-server");
const typeDefs = gql`
type User {
id: ID!
name: String!
password: String!
role: String!
}
input UserInput {
name: String!
password: String!
role: String!
}
type TokenResult {
message: String
token: String
}
type Query {
users: [User]
}
type Mutation {
register(userInput: UserInput): User
login(name: String!, password: String!, role: String!): TokenResult
}
`;
module.exports = typeDefs;Примечание: сигнатура login в typeDefs включает role. В примере резолвера роль может передаваться либо при логине, либо определяться сервером. Согласуйте сигнатуру API и реализацию. Если роль не нужна при логине, удалите её из typeDefs.
Реализация резолверов и JWT
Создайте graphql/resolvers.js. В начале файла — импорт моделей и библиотек:
const User = require("../models/user");
const jwt = require('jsonwebtoken');
const secretKey = process.env.SECRET_KEY;Добавьте функцию генерации токена:
function generateToken(user) {
const token = jwt.sign(
{ id: user.id, role: user.role },
secretKey,
{ expiresIn: '1h', algorithm: 'HS256' }
);
return token;
}Комментарий: в примере срок жизни “1h” (1 час). Для длинных сессий рассмотрите использование refresh token и коротко живущего access token.
Добавьте функцию верификации токена:
function verifyToken(token) {
if (!token) {
throw new Error('Token not provided');
}
try {
const decoded = jwt.verify(token, secretKey, { algorithms: ['HS256'] });
return decoded;
} catch (err) {
throw new Error('Invalid token');
}
}Резолверы (с исправлением явных ошибок):
const resolvers = {
Mutation: {
register: async (_, { userInput: { name, password, role } }) => {
if (!name || !password || !role) {
throw new Error('Name password, and role required');
}
const newUser = new User({
name: name,
password: password,
role: role,
});
try {
const response = await newUser.save();
return {
id: response._id,
...response._doc,
};
} catch (error) {
console.error(error);
throw new Error('Failed to create user');
}
},
login: async (_, { name, password }) => {
try {
const user = await User.findOne({ name: name });
if (!user) {
throw new Error('User not found');
}
if (password !== user.password) {
throw new Error('Incorrect password');
}
const token = generateToken(user);
if (!token) {
throw new Error('Failed to generate token');
}
return {
message: 'Login successful',
token: token,
};
} catch (error) {
console.error(error);
throw new Error('Login failed');
}
}
},
Query: {
users: async (parent, args, context) => {
try {
const token = context.req.headers.authorization || '';
const decodedToken = verifyToken(token);
if (decodedToken.role !== 'Admin') {
throw new Error('Unauthorized. Only Admins can access this data.');
}
const users = await User.find({}, { name: 1, _id: 1, role:1 });
return users;
} catch (error) {
console.error(error);
throw new Error('Failed to fetch users');
}
},
},
};
module.exports = resolvers;Важно: код выше демонстрационный. Обязательно замените хранение паролей на хеши и рассмотрите использование refresh-token механизма.
Как тестировать
Запустите сервер:
node server.jsОткройте UI Apollo Server (песочница) в браузере. Примеры мутации и запроса можно выполнять там.
Добавьте токен в заголовок Authorization следующим образом:
Authorization: Bearer <ваш_JWT>Безопасное использование JWT — чек-лист
- Используйте короткоживущие access-токены (например, несколько минут — час). Долгие токены повышают риск компрометации.
- Храните refresh-токены отдельно и в безопасном хранилище (HttpOnly куки или защищённое хранилище на сервере).
- Подписывайте токены надёжным секретом или асимметричными ключами (RS256) при большом масштабе.
- Реализуйте механизм отзыва токенов (blacklist) для экстренных случаев.
- Включите проверку алгоритма при верификации токена.
- Передавайте токен в заголовке Authorization как Bearer.
- Используйте HTTPS на всех уровнях.
- Валидируйте входные данные (input validation) и ограничивайте глубину запросов (query depth) и вычислительную сложность.
- Скрывайте подробные ошибки в продакшене (masking errors).
- Логируйте попытки неудачной авторизации и аномалии.
Когда JWT не подходит или имеет ограничения
- Нужна мгновенная отзывка большого количества токенов — JWT без централизованного хранилища отзывов неудобен.
- Приложение требует полной серверной сессии с хранением состояния (в этом случае лучше серверные сессии).
- Требуется высокий уровень защиты ключей без возможности ротации — применяйте асимметричную подпись и централизованное управление ключами.
Альтернативы: OAuth 2.0 (для делегированной авторизации), OpenID Connect (аутентификация), серверные сессии (stateful auth).
Ментальные модели и эвристики
- “Минимальные права” — всегда давайте ровно те роли и права, которые нужны для конкретного действия.
- “Беспокоиться о нарушениях на ранней стадии” — предполагайте, что токен может быть скомпрометирован, и постройте защиту с учётом этого.
- “Делегируй аутентификацию, контролируй авторизацию” — внешние провайдеры (Auth0, Cognito) удобны для аутентификации; бизнес-логику по правам оставляйте в приложении.
Ролевые чек-листы
Разработчик:
- Реализовать хеширование паролей.
- Добавить ограничения по сложности паролей.
- Убедиться в корректной обработке ошибок.
DevOps / Инженер платформы:
- Хранение секретов в KMS/Vault.
- Настроить HTTPS и HSTS.
- Регулярно ротировать секреты.
Команда безопасности:
- Провести тесты на контроль доступа (privilege escalation).
- Провести SAST/DAST сканирование.
- Настроить мониторинг и оповещения на подозрительную активность.
Мини‑методика внедрения JWT в проект (шаги)
- Прототип: реализовать basic login/register с генерацией JWT.
- Тесты: добавить юнит- и интеграционные тесты для логина и access-контроля.
- Безопасность: заменить открытые пароли на хеши, настроить хранение секретов.
- Отзыв: реализовать таблицу revoked tokens или версионирование ключей.
- Нагрузочное тестирование и лимитирование запросов.
- Релиз и мониторинг.
Decision tree для выбора метода аутентификации
flowchart TD
A[Требуется аутентификация?] -->|Нет| B[Нет защиты]
A -->|Да| C{Требуется делегирование}
C -->|Да| D[OAuth2 / OIDC]
C -->|Нет| E{Нужно ли хранить состояние сессии}
E -->|Да| F[Серверные сессии]
E -->|Нет| G[JWT с access/refresh]
G --> H{Нужна мгновенная отзывка}
H -->|Да| I[JWT + централизованная таблица отзывов]
H -->|Нет| J[Стандартный JWT]Критерии приёмки
- Регистрация создаёт пользователя с хешированным паролем.
- Логин возвращает валидный JWT со сроком действия и ролью в payload.
- Запрос users возвращает данные только при наличии корректного токена Admin.
- Попытка получить users без токена или с токеном не‑Admin даёт ошибку 403/Unauthorized.
Тест-кейсы и acceptance
- Регистрация: передать валидные данные — пользователь создан (200/ок).
- Логин: верный пароль — получен токен; неверный — ошибка.
- Доступ к users с Admin токеном — данные возвращены.
- Доступ к users с токеном не‑Admin — отказ.
Инцидентный план: компрометация секретного ключа
- Блокируйте ingress в минимально необходимом объёме (если нужно).
- Ротация секретов: выпустите новый ключ и опубликуйте короткое время жизни для предыдущих токенов.
- Включите принудительное invalidation — добавьте текущие токены в блоклист.
- Уведомите пользователей и примите меры по расследованию.
- Восстановите нормальную работу, проверив логи и обновив контроль доступа.
Пример заголовка Authorization
Authorization: Bearer Glossary — 1 строка
- JWT: стандартизированный компактный токен с подписью для передачи утверждений.
- Access token: короткоживущий токен для доступа к ресурсам.
- Refresh token: долгоживущий токен для получения новых access-токенов.
Приватность и соответствие требованиям (GDPR)
- Минимизируйте объём персональных данных в токене.
- Не включайте в токен чувствительную информацию (пароли, PII) в явном виде.
- Реализуйте процессы удаления данных по требованию (right to be forgotten): удаление из БД должно учитывать зависимые сущности и кеши.
Советы по hardening и дополнительные механики
- Ограничьте размер и глубину запросов (depth limiting).
- Используйте persisted queries для снижения рискованных динамических запросов.
- Включите rate limiting и WAF на уровне API-шлюза.
- Подключите мониторинг SLI/SLO: время ответа, ошибки 4xx/5xx, частота отвергнутых авторизаций.
Совместимость и миграция
- jwt библиотеки есть для большинства языков — Node, Java, Go, Python, Ruby.
- При переходе с stateful на stateless auth: планируйте миграцию с поддержкой обоих режимов и механизмом ревокации.
Короткое резюме
Мы разобрали, как добавить JWT в GraphQL API на базе Apollo Server и MongoDB. Показаны примеры кода, исправлены распространённые ошибки и описаны практики безопасности: хранение секретов, ротация ключей, отзыв токенов, защита от чрезмерно сложных запросов и рекомендации по тестированию.
Ключевые выводы:
- JWT удобны, но требуют правильной архитектуры отзывов и хранения секретов.
- Валидируйте входные данные и ограничивайте сложность запросов.
- Не храните пароли в открытом виде — используйте хеши.
Дополнительные материалы и контрольные списки можно адаптировать под вашу инфраструктуру и требования безопасности.
Похожие материалы
RDP: полный гид по настройке и безопасности
Android как клавиатура и трекпад для Windows
Советы и приёмы для работы с PDF
Calibration в Lightroom Classic: как и когда использовать
Отключить Siri Suggestions на iPhone