Проверка 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);
});
Проверенный токен говорит, кто вызывает систему, но о том, какие записи этому вызывающему можно читать, он не говорит ничего, поэтому сравнение с владельцем записи — это и есть шаг авторизации, а не формальность. Один и тот же код состояния и для отсутствующей записи, и для чужой не позволяет ответу подтвердить, что заказ вообще существует. Проверка аудитории в вызове проверки — то, что не даёт переиграть здесь токен, выпущенный для другого приложения, вызывающему, который меняет идентификатор в пути.