Menu
CoddyTech

Проверка токенов

Часть раздела Аутентификация и безопасность путешествия по Node.js на Coddy. Урок 16 из 24.

Клиент API отправляет свой токен в заголовке каждого запроса: Authorization: Bearer <token>. Если прочитать его с помощью decodeJwt, будет принят любой токен, который кто угодно ввёл. jwtVerify(token, key) заново вычисляет подпись с помощью вашего секретного ключа и выдаёт ошибку, если подпись не совпадает, срок действия токена истёк или это вообще не JWT. Доверять payload можно только после успешного выполнения этой функции.

async function requireDevice(req, res, next) {
    const token = (req.get('authorization') ?? '').replace(/^Bearer /, '');
    try {
        const { payload } = await jwtVerify(token, deviceKey);
        req.device = payload.sub;
    } catch {
        return res.status(401).json({ error: 'Unknown device' });
    }
    next();
}
challenge icon

Задание

Легко

Теперь принимай токены, которые ты подписываешь. Приложение отправляет свой токен в заголовке каждого запроса, а сервер проверяет подпись, прежде чем доверять чему-либо внутри токена.

  1. В server.js импортируй jwtVerify из jose.
  2. Напиши middleware requireUser(req, res, next). Оно читает заголовок Authorization. В заголовке содержится слово Bearer, пробел, затем токен. Извлеки из него токен.
  3. Проверь токен с помощью jwtVerify, используя тот же ключ, которым ты подписываешь токен. jwtVerify выбрасывает ошибку, если подпись не совпадает, срок действия токена истёк или текст вообще не является токеном. Во всех этих случаях, а также если заголовка нет, ответь со статусом 401 и {"error": "Log in first"}.
  4. Если токен действителен, установи для req.user объект с полем id, взятым из утверждения sub токена, и полем username, взятым из утверждения username. Затем вызови next().
  5. Добавь GET /me с requireUser. Он возвращает req.user: {"id": "66f1c2a9e4b0a1b2c3d4e5f6", "username": "ada"}.

Попробуй: Нажми «Запустить». Отправь GET /me: статус 401 и {"error": "Log in first"}. Отправь POST /signup, затем POST /token, в обоих случаях передав {"username": "ada", "password": "correct horse"}, и скопируй токен. В поле заголовков добавь заголовок с именем Authorization, значением которого будет Bearer, пробел и твой токен. Отправь GET /me: статус 200, с твоим id и "username": "ada". Теперь измени одну букву в средней части токена, между двумя точками, и снова отправь GET /me: статус 401, потому что подпись больше не совпадает.

Попробуйте сами

import express from 'express';
import { SignJWT, decodeJwt } from 'jose';
import { users } from './db.js';
import { hashPassword, verifyPassword } from './passwords.js';

const secret = new TextEncoder().encode(process.env.JWT_SECRET);

const app = express();
app.use(express.json());

app.post('/signup', async (req, res) => {
    const username = String(req.body.username ?? '').trim().toLowerCase();
    const password = String(req.body.password ?? '');
    if (!username || password.length < 8) {
        return res.status(400).json({ error: 'Pick a username and a password of at least 8 characters' });
    }
    if (await users.findOne({ username })) {
        return res.status(409).json({ error: 'That username is taken' });
    }
    await users.insertOne({ username, passwordHash: await hashPassword(password) });
    res.status(201).json({ username });
});

app.post('/login', async (req, res) => {
    const username = String(req.body.username ?? '').trim().toLowerCase();
    const user = await users.findOne({ username });
    if (!user || !(await verifyPassword(String(req.body.password ?? ''), user.passwordHash))) {
        return res.status(401).json({ error: 'Wrong username or password' });
    }
    res.json({ username: user.username });
});

app.post('/token', async (req, res) => {
    const username = String(req.body.username ?? '').trim().toLowerCase();
    const user = await users.findOne({ username });
    if (!user || !(await verifyPassword(String(req.body.password ?? ''), user.passwordHash))) {
        return res.status(401).json({ error: 'Wrong username or password' });
    }
    const token = await new SignJWT({ username: user.username })
        .setProtectedHeader({ alg: 'HS256' })
        .setSubject(user._id.toHexString())
        .setIssuedAt()
        .setExpirationTime('1h')
        .sign(secret);
    res.json({ token });
});

// Показывает содержимое токена: GET /inspect?token=...
app.get('/inspect', (req, res) => {
    try {
        res.json(decodeJwt(String(req.query.token ?? '')));
    } catch {
        res.status(400).json({ error: 'Not a JWT' });
    }
});

// Страница администратора. Ни одному утверждению в токене нельзя доверять до проверки его подписи.
app.get('/admin', (req, res) => {
    res.status(403).json({ error: 'Admins only' });
});

app.listen(3000, () => {
    console.log('Vault is running on port 3000');
});
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Аутентификация и безопасность

Потренируйтесь самостоятельно: Песочница Express