Перейти к содержимому

16. Authentication

GraphQL Auth

Аутентификация (кто ты?) и авторизация (что ты можешь делать?) — критически важные части любого API.

Окно терминала
npm install jsonwebtoken bcrypt
import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';
const JWT_SECRET = process.env.JWT_SECRET;
const JWT_EXPIRES_IN = '7d';
const typeDefs = `#graphql
type AuthPayload {
token: String!
user: User!
}
type Mutation {
register(name: String!, email: String!, password: String!): AuthPayload!
login(email: String!, password: String!): AuthPayload!
refreshToken(refreshToken: String!): AuthPayload!
logout: Boolean!
}
`;
const resolvers = {
Mutation: {
register: async (_, { name, email, password }, { db }) => {
// Проверяем уникальность email
const existing = await db.users.findByEmail(email);
if (existing) {
throw new GraphQLError('Email уже используется', {
extensions: { code: 'BAD_USER_INPUT', field: 'email' },
});
}
// Хешируем пароль
const passwordHash = await bcrypt.hash(password, 12);
// Создаём пользователя
const user = await db.users.create({ name, email, passwordHash });
// Генерируем токен
const token = jwt.sign({ userId: user.id }, JWT_SECRET, {
expiresIn: JWT_EXPIRES_IN,
});
return { token, user };
},
login: async (_, { email, password }, { db }) => {
const user = await db.users.findByEmail(email);
if (!user) {
throw new GraphQLError('Неверный email или пароль', {
extensions: { code: 'UNAUTHENTICATED' },
});
}
const valid = await bcrypt.compare(password, user.passwordHash);
if (!valid) {
throw new GraphQLError('Неверный email или пароль', {
extensions: { code: 'UNAUTHENTICATED' },
});
}
const token = jwt.sign({ userId: user.id }, JWT_SECRET, {
expiresIn: JWT_EXPIRES_IN,
});
return { token, user };
},
},
};
import { startStandaloneServer } from '@apollo/server/standalone';
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => {
const authHeader = req.headers.authorization;
let user = null;
if (authHeader) {
const token = authHeader.replace('Bearer ', '');
try {
const decoded = jwt.verify(token, JWT_SECRET);
user = await db.users.findById(decoded.userId);
} catch (error) {
// Токен невалиден — user остаётся null
// Не бросаем ошибку здесь — resolver сам решит
}
}
return { db, user, loaders: createLoaders(db) };
},
});
const resolvers = {
Query: {
me: (_, __, { user }) => {
if (!user) {
throw new GraphQLError('Необходима авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
});
}
return user;
},
},
};
auth/helpers.js
import { GraphQLError } from 'graphql';
export function assertAuthenticated(user) {
if (!user) {
throw new GraphQLError('Необходима авторизация', {
extensions: {
code: 'UNAUTHENTICATED',
http: { status: 401 },
},
});
}
}
export function assertRole(user, ...roles) {
assertAuthenticated(user);
if (!roles.includes(user.role)) {
throw new GraphQLError('Недостаточно прав', {
extensions: {
code: 'FORBIDDEN',
http: { status: 403 },
},
});
}
}
export function assertOwner(user, resourceUserId) {
assertAuthenticated(user);
if (user.id !== resourceUserId && user.role !== 'ADMIN') {
throw new GraphQLError('Нет доступа к этому ресурсу', {
extensions: { code: 'FORBIDDEN' },
});
}
}
// Использование:
const resolvers = {
Mutation: {
createPost: (_, { input }, { user }) => {
assertAuthenticated(user);
return db.posts.create({ ...input, authorId: user.id });
},
deleteUser: (_, { id }, { user }) => {
assertRole(user, 'ADMIN');
return db.users.delete(id);
},
updatePost: async (_, { id, input }, { user }) => {
const post = await db.posts.findById(id);
assertOwner(user, post.authorId);
return db.posts.update(id, input);
},
},
};
Окно терминала
npm install graphql-shield
import { shield, rule, and, or, not } from 'graphql-shield';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { applyMiddleware } from 'graphql-middleware';
// Правила
const isAuthenticated = rule()(async (parent, args, { user }) => {
return !!user;
});
const isAdmin = rule()(async (parent, args, { user }) => {
return user?.role === 'ADMIN';
});
const isPostOwner = rule()(async (parent, args, { user, db }) => {
const post = await db.posts.findById(args.id);
return post?.authorId === user?.id;
});
// Матрица прав
const permissions = shield({
Query: {
me: isAuthenticated,
adminUsers: isAdmin,
posts: not(isAuthenticated), // Только публичные
},
Mutation: {
createPost: isAuthenticated,
updatePost: or(isAdmin, isPostOwner),
deletePost: or(isAdmin, isPostOwner),
deleteUser: isAdmin,
},
Post: {
// Поле доступно только автору или админу
draftContent: or(isAdmin, isPostOwner),
},
}, {
allowExternalErrors: true, // Показывать оригинальные ошибки
fallbackError: 'Нет доступа',
});
// Применяем middleware к схеме
let schema = makeExecutableSchema({ typeDefs, resolvers });
schema = applyMiddleware(schema, permissions);
const resolvers = {
Query: {
posts: async (_, { status }, { user, db }) => {
// Без авторизации — только опубликованные
if (!user) {
return db.posts.findMany({ where: { status: 'PUBLISHED' } });
}
// Адмиин — видит всё
if (user.role === 'ADMIN') {
return db.posts.findMany({ where: { status } });
}
// Обычный пользователь — свои черновики + чужие опубликованные
return db.posts.findMany({
where: {
OR: [
{ status: 'PUBLISHED' },
{ authorId: user.id },
],
...(status ? { status } : {}),
},
});
},
},
};
const typeDefs = `#graphql
type Mutation {
login(email: String!, password: String!): LoginPayload!
refreshToken(refreshToken: String!): LoginPayload!
logout: Boolean!
}
type LoginPayload {
accessToken: String!
refreshToken: String!
user: User!
}
`;
const resolvers = {
Mutation: {
login: async (_, { email, password }, { db }) => {
// ... проверка пароля
// Короткий access token
const accessToken = jwt.sign(
{ userId: user.id },
process.env.JWT_ACCESS_SECRET,
{ expiresIn: '15m' }
);
// Длинный refresh token
const refreshToken = jwt.sign(
{ userId: user.id },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '30d' }
);
// Сохраняем refresh token в БД
await db.refreshTokens.create({
token: refreshToken,
userId: user.id,
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
});
return { accessToken, refreshToken, user };
},
refreshToken: async (_, { refreshToken }, { db }) => {
let decoded;
try {
decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
} catch {
throw new GraphQLError('Невалидный refresh token');
}
// Проверяем в БД (токен не был отозван?)
const stored = await db.refreshTokens.findOne({ token: refreshToken });
if (!stored) throw new GraphQLError('Токен отозван');
const user = await db.users.findById(decoded.userId);
const accessToken = jwt.sign(
{ userId: user.id },
process.env.JWT_ACCESS_SECRET,
{ expiresIn: '15m' }
);
return { accessToken, refreshToken, user };
},
logout: async (_, __, { db, req }) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (token) {
await db.refreshTokens.delete({ token });
}
return true;
},
},
};
import { ApolloClient, InMemoryCache, createHttpLink, from } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import { onError } from '@apollo/client/link/error';
const httpLink = createHttpLink({ uri: '/graphql' });
// Добавляем токен в каждый запрос
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem('accessToken');
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : '',
},
};
});
// Обработка ошибок авторизации
const errorLink = onError(({ graphQLErrors, operation, forward }) => {
if (graphQLErrors?.some(e => e.extensions?.code === 'UNAUTHENTICATED')) {
// Попытка обновить токен
return new Observable((observer) => {
refreshAccessToken()
.then(newToken => {
localStorage.setItem('accessToken', newToken);
// Повторяем запрос с новым токеном
operation.setContext(({ headers }) => ({
headers: { ...headers, authorization: `Bearer ${newToken}` },
}));
forward(operation).subscribe(observer);
})
.catch(() => {
// Refresh провалился — логаут
localStorage.clear();
window.location.href = '/login';
});
});
}
});
export const client = new ApolloClient({
link: from([errorLink, authLink, httpLink]),
cache: new InMemoryCache(),
});
  1. Реализуй register и login мутации с JWT
  2. Добавь текущего пользователя в Apollo context
  3. Создай helper assertAuthenticated и защити приватные resolvers
  4. Реализуй графику прав с graphql-shield (admin/editor/reader)
  5. Добавь обновление access token через refresh token
  • JWT в Authorization: Bearer <token> заголовке
  • Извлекаем пользователя в context функции сервера
  • assertAuthenticated / assertRole — helpers для проверок
  • graphql-shield — декларативная матрица прав
  • Access token (короткий) + Refresh token (длинный) — best practice

Следующий урокPagination →