Проверка JWT и авторизация — Примеры

Пример 1 — Токен, который раскодировался без нареканий

Утверждения ниже пришли в сервис заказов и были приняты, потому что обработчик вызывает помощника, который декодирует токен и возвращает полезную нагрузку. Напишите проверку, которую сервис должен выполнять вместо этого. Укажите, какое утверждение проверка обязана сверить помимо подписи, и опишите словами, как проверяется сама подпись.

{
  "sub": "user_1042",
  "iss": "identity.example.test",
  "aud": "reporting-api",
  "exp": 1760000000,
  "scope": "read:orders"
}

Решение

1  подпись     сверена с опубликованными ключами издателя, выбранными по идентификатору ключа в токене
2  алгоритм    зафиксирован на том, который ожидает этот сервис, никогда не берётся из заголовка токена
3  издатель    должен совпадать с ожидаемым, а не просто выглядеть правильно
4  аудитория   должна называть orders-api; токен выше называет reporting-api и здесь проваливается
5  срок        должен быть в будущем, с небольшим допуском на расхождение часов

Шаги с первого по пятый должны пройти все, а помощник, который только декодирует, не выполняет ни один из них, — значит, любой вызывающий может написать любые утверждения. Утверждение, решающее именно этот случай, — aud: токен подлинный и правильно подписан, однако выпущен он для другого приложения, а не для того сервиса, который его проверяет. Фиксация алгоритма — самая важная строка, потому что проверяющий, доверяющий заголовку, позволяет вызывающему назвать более слабый алгоритм или вообще никакой, и обычно ошибаются, полагая, что вызов декодирования что-то проверил.

Пример 2 — Любой аутентифицированный пользователь читает любой заказ

Обработчик ниже проверяет токен и затем возвращает любую запись, которую назовёт путь, поэтому измените его так, чтобы вызывающий мог читать только свой собственный заказ и чтобы ответ при этом не раскрывал, существует ли запись.

app.get("/orders/:id", async (req, res) => {
  const claims = verify(req.headers.authorization);
  const order = await db.orders.findById(req.params.id);
  res.json(order);
});

Решение

app.get("/orders/:id", async (req, res) => {
  const claims = verify(req.headers.authorization, { audience: "orders-api" });
  const order = await db.orders.findById(req.params.id);
  if (!order || order.userId !== claims.sub) return res.status(404).end();
  res.json(order);
});

Проверенный токен говорит, кто вызывает систему, но о том, какие записи этому вызывающему можно читать, он не говорит ничего, поэтому сравнение с владельцем записи — это и есть шаг авторизации, а не формальность. Один и тот же код состояния и для отсутствующей записи, и для чужой не позволяет ответу подтвердить, что заказ вообще существует. Проверка аудитории в вызове проверки — то, что не даёт переиграть здесь токен, выпущенный для другого приложения, вызывающему, который меняет идентификатор в пути.