Представьте клуб с одной входной дверью. Охранник проверяет документ и выдаёт браслет — это аутентификация: «кто вы?». Внутри у каждой комнаты свой сотрудник: он смотрит на браслет и решает, пускать ли вас, — это авторизация: «что вам можно?». В REST «комнаты» — это маршруты, и каждый закрывают своим middleware. В GraphQL дверь одна, /graphql, а «комнаты» — поля схемы, поэтому проверки живут в context и в резолверах. В этом уроке блог научится узнавать пользователя по токену, мы закроем чужие посты от правки, разберём ошибки и защитим сервер от дорогих запросов.

Один вход, много комнат

АутентификацияАвторизация
ВопросКто вы?Что вам можно?
Где в GraphQLcontext: раз на запросрезолверы: на каждом поле

Клиент присылает JWT в заголовке Authorization: Bearer <токен> (устройство JWT вы разбирали в теме про Node.js, библиотека здесь та же — jose). Сервер превращает токен в пользователя или в null и кладёт в context, а резолверы смотрят на context.user.

Токен из заголовка попадает в context, а резолвер выбирает один из трёх исходов
Токен из заголовка попадает в context, а резолвер выбирает один из трёх исходов

Шаг 1. Выдаём токен: мутация login

В модель User добавьте passwordHash и role (enum Role: USER, ADMIN) и выполните миграцию. В схеме появятся login и me, а из CreatePostInput уйдёт authorId: в вопросе «кто я» клиенту верить нельзя, автора возьмём из токена. Поле email станет необязательным — почему, узнаете ниже. Установите библиотеки: npm install jose bcrypt и npm install -D @types/bcrypt.

graphql
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:

ts
// 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
  }
}