Представьте клуб с одной входной дверью. Охранник проверяет документ и выдаёт браслет — это аутентификация: «кто вы?». Внутри у каждой комнаты свой сотрудник: он смотрит на браслет и решает, пускать ли вас, — это авторизация: «что вам можно?». В REST «комнаты» — это маршруты, и каждый закрывают своим middleware. В GraphQL дверь одна, /graphql, а «комнаты» — поля схемы, поэтому проверки живут в context и в резолверах. В этом уроке блог научится узнавать пользователя по токену, мы закроем чужие посты от правки, разберём ошибки и защитим сервер от дорогих запросов.
Один вход, много комнат
| Аутентификация | Авторизация | |
|---|---|---|
| Вопрос | Кто вы? | Что вам можно? |
| Где в GraphQL | context: раз на запрос | резолверы: на каждом поле |
Клиент присылает JWT в заголовке Authorization: Bearer <токен> (устройство JWT вы разбирали в теме про Node.js, библиотека здесь та же — jose). Сервер превращает токен в пользователя или в null и кладёт в context, а резолверы смотрят на context.user.
Шаг 1. Выдаём токен: мутация login
В модель User добавьте passwordHash и role (enum Role: USER, ADMIN) и выполните миграцию. В схеме появятся login и me, а из CreatePostInput уйдёт authorId: в вопросе «кто я» клиенту верить нельзя, автора возьмём из токена. Поле email станет необязательным — почему, узнаете ниже. Установите библиотеки: npm install jose bcrypt и npm install -D @types/bcrypt.
type User {
id: ID!
name: String!
email: String # optional on purpose, see below
posts: [Post!]!
}
input CreatePostInput {
title: String!
body: String!
published: Boolean = false # no authorId: the author is the logged-in user
}
input LoginInput { email: String!, password: String! }
type AuthPayload {
token: String
user: User
errors: [UserError!]!
}
type Query {
me: User # new, the other fields as before
}
type Mutation {
login(input: LoginInput!): AuthPayload # new
}Токены выпускает и проверяет tokens.ts — те же SignJWT и jwtVerify, что в теме про JWT; секрет лежит в .env:
// src/auth/tokens.ts
import { SignJWT, jwtVerify } from 'jose';
const key = new TextEncoder().encode(process.env.JWT_SECRET); // 32+ characters
export type AuthUser = { id: number; role: 'USER' | 'ADMIN' };
export function signToken(user: AuthUser): Promise<string> {
return new SignJWT({ role: user.role })
.setProtectedHeader({ alg: 'HS256' })
.setSubject(String(user.id))
.setExpirationTime('2h')
.sign(key);
}
export async function readUser(header?: string): Promise<AuthUser | null> {
if (!header?.startsWith('Bearer ')) return null;
try {
const { payload } = await jwtVerify(header.slice(7), key, {
algorithms: ['HS256'],
});
const id = Number(payload.sub);
if (!Number.isInteger(id)) return null;
return { id, role: payload.role === 'ADMIN' ? 'ADMIN' : 'USER' };
} catch {
return null; // expired, forged or broken: treat as anonymous
}
}