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'.
Desafio
FácilOs 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.
- Em
server.js, importeSignJWTdejosejunto dedecodeJwt. Acima das rotas, transformeprocess.env.JWT_SECRETem bytes com umTextEncoder, como a lição mostra, poisjoseassina com bytes. Esta lição configuraJWT_SECRETpara você. - 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 quePOST /login. Quando estiverem incorretos, responda com o status 401 e{"error": "Wrong username or password"}. - 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
usernameno payload; - ter o
_iddo usuário como texto no campo subject. UsetoHexString(), poisjosenã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 - usar o algoritmo HS256, assinado com os bytes de
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