Menu
CoddyTech

Assinando tokens

Parte da seção Autenticação e segurança do Journey de Node.js da Coddy. Lição 15 de 24.

Um token só tem valor se ninguém mais puder criar um. Com HS256, jose combina o cabeçalho e o conteúdo com um segredo que só o servidor conhece e calcula o hash do resultado. Esse hash é a assinatura. Sem o segredo, ninguém consegue criar um que corresponda.

O segredo vem de uma variável de ambiente, process.env.JWT_SECRET, nunca do código: o código é compartilhado, copiado e enviado para o repositório. Esta lição configura JWT_SECRET para você. jose precisa dele em bytes.

const key = new TextEncoder().encode(process.env.INVITE_SECRET);

const invite = await new SignJWT({ room: 'design' })
    .setProtectedHeader({ alg: 'HS256' })
    .setSubject(guestId)
    .setIssuedAt()
    .setExpirationTime('2d')
    .sign(key);

setSubject recebe texto, e setExpirationTime recebe um intervalo como '30m' ou '2d'.

challenge icon

Desafio

Fácil

Os aplicativos que chamam sua API, como um aplicativo de celular, não mantêm cookies da mesma forma que um navegador. Em vez disso, forneça a eles um token: um JWT assinado que eles enviam de volta a cada solicitação. POST /token é o login desses aplicativos.

  1. Em server.js, importe SignJWT de jose junto de decodeJwt. Acima das rotas, transforme process.env.JWT_SECRET em bytes com um TextEncoder, como a lição mostra, pois jose assina com bytes. Esta lição configura JWT_SECRET para você.
  2. Adicione POST /token. Ele recebe o mesmo corpo que o login, {"username": "ada", "password": "correct horse"}, e verifica o nome de usuário e a senha da mesma forma que POST /login. Quando estiverem incorretos, responda com o status 401 e {"error": "Wrong username or password"}.
  3. Quando estiverem corretos, assine um token e responda com ele como {"token": "..."}. O token deve:
    • usar o algoritmo HS256, assinado com os bytes de JWT_SECRET;
    • conter a declaração username no payload;
    • ter o _id do usuário como texto no campo subject. Use toHexString(), pois jose não aceita um subject que não seja texto;
    • expirar uma hora após ser criado.

    Um token desta lição tem esta aparência: eyJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkYSIsInN1YiI6IjY2ZjFjMmE5ZTRiMGExYjJjM2Q0ZTVmNiIsImlhdCI6MTc5MzM2MTYwMCwiZXhwIjoxNzkzMzY1MjAwfQ.EdCrjAKSjZowcjE3-IjKttBLVK1VKKbL9DPnJDLdgug

Experimente: Pressione Run. Envie POST /signup com {"username": "ada", "password": "correct horse"}. Envie POST /token com o mesmo corpo: status 200 e {"token": "..."}. Copie o token e envie GET /inspect?token= seguido dele. Você verá algo como {"username": "ada", "sub": "66f1c2a9e4b0a1b2c3d4e5f6", "iat": 1793361600, "exp": 1793365200}: exp é 3600 segundos depois de iat. Envie POST /token com {"username": "ada", "password": "wrong horse"}: status 401.

Experimente você mesmo

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

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 });
});

// Mostra o que um token diz: 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' });
    }
});

// A página de administração. Nenhuma declaração pode ser considerada confiável antes que sua assinatura seja verificada.
app.get('/admin', (req, res) => {
    res.status(403).json({ error: 'Admins only' });
});

app.listen(3000, () => {
    console.log('Vault is running on port 3000');
});

Todas as lições de Autenticação e segurança

Pratique por conta própria: Playground de Express