Autenticação JWT no express com BlockList
Vamos começar instalando o Express e uma biblioteca para fazer a criação e validação do token JWT:
npm i express jsonwebtoken dayjs uuid
Agora vamos criar o nosso serviço com um endpoint que queremos muito proteger:
// server.js
const express = require("express")
const app = express()
app.use(express.json())
app.get("/secret", (req, res) => {
return res.json({
message: "uma mensagem super secreta que deve ser protegida"
})
})
app.listen(4000, () => {
console.log(`Listening on port 4000`)
})
O primeiro passo vai ser criar uma rota para o login:
const dayjs = require("dayjs")
app.post("/login", (req, res) => {
const { username, password } = req.body
// Isso e so um exemplo, aqui entraria alguma chamada para o seu banco de dados
if (username !== "admin" || password !== "admin") {
return res.status(401).json({
message: "Invalid credentials"
})
}
const token = jwt.sign({
username,
// Isso aqui define uma data de expiração para o seu token, ele irá expirar em um dia (24h)
exp: dayjs().add(1, "day").unix()
}, "secret")
return res.json({
token
})
})
Para testar isso com httpie podem-mo executar o seguinte comando:
http POST localhost:4000/login username=admin password=admin
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNjgxODUwOTA2LCJpYXQiOjE2ODE3NjQ1MDZ9.fK8sQn0vqjFuQT7mq9fTnzuN2CARVOlH1mRCVTDZl6M"
}
Em implementações com tokens JWT o front-end irá armazenar esse token no browser e envia-lo junto a todas as requisições que fizer na sua API.
Para validar essas requisições vamos usar um middleware do express:
function verifyToken(req, res, next) {
const token = req.headers.authorization?.replace("Bearer ", "")?.trim()
if (!token) {
return res.status(401).json({
message: "Token missing"
})
}
try {
// Já valida o segredo e data de expiração do token, além de ja devolver o token decodificado
const payload = jwt.verify(token, "secret")
req.user = payload
return next()
} catch {
return res.status(401).json({
message: "Invalid token"
})
}
}
Agora é so adicionar esse middleware na nossa rota:
app.get("/secret", verifyToken, (req, res) => {
return res.json({
message: "uma mensagem super secreta que deve ser protegida"
})
})
Testando com um token válido:
http GET localhost:4000/secret "Authorization:Bearer eyJhb..."
{
"message": "uma mensagem super secreta que deve ser protegida"
}
Ok, mas e se o usuario se deslogar antes das 24h o token continua valido certo? Sim, geralmente apenas descarta-mos o token no front-end! Essa é uma solução bem simples e geralmente amais recomendada, mas se isso não for possivel por alguma regra de negocio também podemos criar uma blocklist que invalide o token.
Par aisso vamos precisar fazer algumas alterações no nosso código e no funcionamento da nossa autenticação atual:
- Agora os tokens vão ter também um id, uma “claim” que sera um identificador unico para aquele token, vamos adicionar isso no login
- Criar um rota de logout, que ira armazenar esse ID, uma boa ideia e salvar no redis com um TTL assim os dados somem depois de um tempo, mas pode salvar em um banco de dados comum, como aqui estou só brincando e escrevendo um post rápido, vou salvar em um Array, jamais faça isso em produção
- Vamos precisar validar esse id contra uma lista de tokens que foram invalidados, o acesso so deve ser liberado se o token não existir nessa lista
O login será bem simples de alterar, vamos adicionar uma “claim” jti
, ou JWT ID, com um valor aleatorio:
const { v4: uuidv4 } = require('uuid');
const token = jwt.sign({
username,
exp: dayjs().add(1, "day").unix(),
jti: uuidv4()
}, "secret")
A rota de logou será mais ou menos assim:
const blocklist = []
app.delete("/logout", verifyToken,(req, res) => {
blocklist.push(req.user.jti)
return res.json({
message: "Logout"
})
})
Só lembrando mais uma vez: Não salve nada em variavel global em um código de verdade, isso e apenas um exercicio, e possivelmente depois eu vou fazer um post usando redis.
E ao verificar o token adicionamos mais uma condição:
const payload = jwt.verify(token, "secret")
if (blocklist.includes(payload.jti)) {
return res.status(401).json({
message: "Token invalid"
})
}
req.user = payload
return next()
Testando a rota de logout:
http DELETE localhost:4000/logout "Authorization:Bearer eyJh..."
{
"message": "Logout"
}
E agora chamando esse endpoint depois de chamar o logout:
http GET localhost:4000/secret "Authorization:Bearer eyJh..."
{
"message": "Token invalid"
}
Este é um post super rapidinho para cumprir um desafio de escrever posts por 30 dias, você pode ver outros posts na tag 30daysOfPosts