api-rest-autenticacao-json-web-token

Eu sempre tive uma treta séria com autenticação. Quando precisei autenticar em uma API REST, então, aí ferrou! Mas existe uma luz ao fim do túnel, e ela se chama JSON Web Token, ou JWT.

Não vou entrar em detalhes da minha briga com métodos de autenticar - no momento, mas acredite quando digo que testei e usei vários. Eis o por que do JWT.

Analisando a Demanda:

  • Necessidade: Gerenciar rotas protegidas por login e níveis de usuário de uma API REST em NodeJS.

  • Requisitos: Não depender de sessão (rest é sessionless, lembra?), permitir identificar o usuário (e seu nível), não utilizar Providers de terceiros, integrar com qualquer plataforma frontend.


JSON Web Token: o que é JWT?

JWT pronuncia-se ‘JõT’.

É um jeito simples de encaminhar informações que podem ser confiadas, pois são assinadas e confirmadas.

Isso é um JWT:

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjEzODY4OTkxMzEsImlzcyI6ImppcmE6MTU0ODk1OTUiLCJxc2giOiI4MDYzZmY0Y2ExZTQxZGY3YmM5MGM4YWI2ZDBmNjIwN2Q0OTFjZjZkYWQ3YzY2ZWE3OTdiNDYxNGI3MTkyMmU5IiwiaWF0IjoxMzg2ODk4OTUxfQ.uKqU9dTB6gKwG6jQCuXYAiMNdfNRw98Hw_IWuA5MaMo

São 3 partes, separadas por um ., encodadas por base64 individualmente:

<base64-encoded header>.<base64-encoded claims>.<base64-encoded assinatura>

Mas atenção: JWT não encripta o payload, apenas o assina. O que isso quer dizer é que você não deve enviar informações confidenciais por JWT. Apenas informações que devem ser verificadas/confiadas.

Por exemplo: Não é uma boa idéia passar uma senha por JWT, mas enviar o ID do usuário APÓS o login, vai permitir que ele faça requisições autenticadas :D, saca só:

Fluxo de uso de JWT para um client

  1. Quero acessar uma rota protegida, tipo /users

  2. Faço o login, enviando (de forma segura, por https) usuário e senha.

  3. Login com sucesso, recebo o ‘TOKEN’. Guardo pra usar depois ;)

  4. Faço a requisição que eu queria na /users porém, com o http Header x-access-token : meutokenaqui

  5. Com o token enviado, o servidor me identifica e autoriza (ou não) meu acesso, retornando a resposta à minha requisição.

Fluxo de uso de JWT para o SERVER

  1. Se tentarem acessar uma rota protegida sem token, não autoriza.

  2. Se tentar acessar com um token, verifica se é válido. Se for, consegue identificar o usuário pelo TOKEN e libera o acesso.

  3. Quando um usuário faz login, geramos um token, para ele poder fazer requisições autenticadas.

Nossa, Rômulo, que legal, cara! Agora me diz, como eu faço isso?


Como fazer a autenticação de uma API REST com Json Web Token em NodeJs?

Bom, pra explicar legal mesmo a gente vai começar junto, e de mãos dadas, iremos até o fim :P (aperta o cinto, porque vai ser longa a jornada)

O resultado de nossa empreitada esta neste repositório do github

À partir de agora, serei seu guia pelo lindo mundo das APIs com NodeJS, e faremos:

  1. Um servidorzinho

  2. Algumas rotas: POST E GET de usuarios, POST de login

  3. Conexão com o MongoDB do Mongolabs

  4. Criar usuários

  5. Encriptar senhas

  6. Implementar JWT durante o login

  7. Passar middleware que tornará a rota passível de autenticação. E vai conferir o JWT

Vamos lá?

0) Requisitos:

NodeJs
POSTMAN, para testar a API e métodos GET e POST
MongoLabs um db gratuito resolve

1) Servidor em NodeJs.

Já com o Node instalado, vamos criar uma nova pasta.
Nela, crie um arquivo chamado package.json, insira:

[package.json]
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"name": "teste-api-jwt",
"version": "1.0.0",
"main": "server.js",
"dependencies": {
"express": "^4.11.2",
"body-parser": "^1.11.0",
"mongoose": "^3.8.23",
"bcrypt-nodejs": "0.0.3",
"jwt-simple": "^0.2.0",
"moment": "^2.9.0",
}
}

Já pode acessar a pasta pelo terminal, e instalar as dependências, com o comando:

[com o terminal, na mesma pasta do package.json, execute o comando:]
1
$ npm install

Enquanto você espera instalar, veja o que cada item faz:
O express é o framework para App. Vai gerenciar nossas rotas e middlewares.
O body-parser, antigamente incluso no express, vai parsear nossas requisições, permitindo que usemos urls complexas e trabalhar com formulários.
O mongoose é o ORM que vai interagir com o MongoDb pra gente.
O bcrypt-nodejs vai encriptar nossa senha. NUNCA salve a senha sem encriptar.
O jwt-simple vai gerar e conferir nossos JSON Web Token (ou você achou que ia fazer todo aquele processo acima na unha? hehe)
O moment vai nos ajudar a trabalhar com data de expiração do JWT.

Agora, criemos o server.js

[server.js][crie na mesma pasta do package.json]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var express = require('express')
, app = express()
, bodyParser = require('body-parser')
, mongoose = require('mongoose')
, jwt = require('jwt-simple');
var db = '';//coloque a url do db aqui
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
var port = process.env.PORT || 8080;
var router = express.Router();
app.use('/api', router);
/*Aqui criaremos as rotas*/
mongoose.connect(db);
app.listen(port);
console.log('conectado a porta ' + port);

Note que a var db é a url do nosso banco de dados. Vc pode criar uma conta grátis no MongoLab, acessá-la, clicar em create database nomeie teste-jwt deixando com a opção single node (gratis).
Após criada, clique nela, aí em create user e crie um usuário para o nosso DB.
A url deve ser algo como mongodb://usuario:senha@ds053668.mongolab.com:53668/teste-jwt

Após colocar a url na var db, salve o arquivo, acesse a pasta com o terminal e rode:

1
2
$ node server
conectado a porta 8080

Sinal que deu boa ^_^

2) Criando algumas rotas

Logo abaixo de /*Aqui criaremos as rotas*/, insira:

[server.js][insira abaixo do comentário]
1
2
3
4
5
6
7
8
9
10
11
(...)
/*Aqui criaremos as rotas*/
var rotas = require('./rotas')
router.route('/usuarios')
.get(rotas.getUsuarios)
.post(rotas.postUsuarios);
router.route('/login')
.post(rotas.login);

mongoose.connect(db);
(...)

Crie um arquivo chamado rotas.js na mesma pasta.

[rotas.js][crie o arquivo na mesma pasta]
1
2
3
4
5
6
7
8
9
10
11
module.exports = {
getUsuarios: function(req, res){
res.json({message: "rota para GET do /usuarios"})
},
postUsuarios: function(req, res){
res.json({message: "rota para POST do /usuarios"})
},
login: function(req, res){
res.json({message; 'rota de login'})
}
}

Salvando tudo, você pode executar node server novamente, fazer um get para http://localhost:8080/api/usuarios e depois um post para conferir se funcionou
(Pra isso, use algo como o postman (clique para baixar)) ou faz no curl se for macho(a).

3) Criando o Model usuário

Crie o arquivo ModelUsuario.js, na mesma pasta.

[ModelUsuario.js] [Crie o arquivo]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
var mongoose = require('mongoose');
var bcrypt = require('bcrypt-nodejs');
//1
var UsuarioSchema = new mongoose.Schema({
username: {
type: String,
unique: true,
required: true
},
password: {
type: String,
required: true
}
});
//2
UsuarioSchema.pre('save', function(next) {
var user = this;
if (!user.isModified('password')) return next();
bcrypt.genSalt(5, function(err, salt) {
if (err) return next(err);
bcrypt.hash(user.password, salt, null, function(err, hash) {
if (err) return next(err);
user.password = hash;
next();
});
});
});
//3
UsuarioSchema.methods.verificaSenha = function(password, next) {
bcrypt.compare(password, this.password, function(err, isMatch) {
if (err) return next(err);
next(isMatch);
});
};
module.exports = mongoose.model('Usuario', UsuarioSchema);

…Vish.

Acima temos 3 coisas acontecendo. Nesse arquivo, usamos o mongoose e o bcrypt.

  1. var UsuarioSchema
    Primeiro, var UsuarioSchema definimos a estrutura, com os campo username e password.

  2. UsuarioSchema.pre(‘save’….
    Depois, setamos uma função .pre('save') nessa var que criamos: UsuarioSchema.pre('save', function(.......
    Toda vez que formos salvar, essa função .pre() verifica:

    • SE(if) o password não foi alterado. Se ele não foi alterado, retorna next(), ou seja: continua o fluxo do save().
      Se acontecer outra coisa com o password (criado, alterado..) ele gera o salt com bcrypt.genSalt(). Se der boa, ele usa o bcrypt.hash() e encripta nosso password com o salt q ele gerou. Se der erro, retorna next(err). Se não, chama o next()
  3. UsuarioSchema.methods.verificaSenha = function(senha, next){}
    Aí estamos criando um metodo chamado verificaSenha(), que recebe a senha, usa o bcrypt.compare(). Retorna next(isMatch) se der certo.

E por último module.exports exporta o model com o nome ‘Usuario’.

Pra facilitar estamos colocando tudo na mesma pasta. O ideal, seria ter uma pasta /model, mas não vamos fazer isso nesse artigo.

Agora já temos nosso model, precisamos criar a funçaõ que cria um novo usuário… aff tá cansando né?

O controller criaUsuario()

Esse cara vai criar nossos usuários. Basicamente ele pega os campos username e password, que receberemos na rota, e insere no banco de dados.

Crie o arquivo controllerCriaUsuario.js

[controllerCriaUsuario.js][Crie, na mesma pasta]
1
2
3
4
5
6
7
8
9
10
11
12
var Model = require('./ModelUsuario');
module.exports = function(req, res){
var data = new Model({
username: req.body.username,
password: req.body.password
});
data.save(function(err) {
if (err)
res.send(err);
res.json({ message: 'Novo Usuário', data: data });
});
};

Maneiro. Aí estamos apenas gerando um new Model e depois dando um .save() nele. Se der tudo certo, recebemos as informações do novo usuario.

Vamos rapidinho alí no rotas.js pra usar esse controller na postUsuarios!

[rotas.js][altere apenas a postUsuarios]
1
2
3
(...),
postUsuarios: require('./controllerCriaUsuario'),
(...)

Agora podemos criar nosso usuário com senha encriptada

rode node server.

Usando o postman, ou outro, faça um POST para http://localhost:8080/api/usuarios envie username e password devidamente preenchidos:
criando-usuario-nodejs-mongodb-express-api-rest

Aeeee conseguimos! Muito legal né? Um servidor express com rotas, criando usuário e salvando no DB com a senha encriptada!

Precisamos agora, fazer o login. E, ao fazer o login, criar o TOKEN

4) O Controller de Login com gerador de Token

Crie o arquivo controllerLogin.js:

[controllerLogin.js][Crie o arquivo]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
var Usuario = require('./ModelUsuario');
, jwt = require('jwt-simple')
, moment = require('moment')
, segredo = 'seusegredodetoken'
module.exports = function(req, res) {
var username = req.body.username || '';
var password = req.body.password || '';
if (username == '' || password == '') {
return res.send(401);
}
//1
Usuario.findOne({username: username}, function (err, user) {
if (err) {
return res.end(401)
}
//2
user.verificaSenha(password, function(isMatch) {
if (!isMatch) {
return res.send(401);
}
//3
var expires = moment().add(7,'days').valueOf();
var token = jwt.encode({
iss: user.id,
exp: expires
}, segredo);
//4
return res.json({
token : token,
expires: expires,
user: user.toJSON()
});
});
});
};

Vamos pedir que a nossa rota (/login) rode-o:

[rotas.js][altere apenas a login]
1
2
3
(...),
login: require('./controllerLogin'),
(...)

Reinicie o servidor com

1
$ node server

Agora mude a url para http://localhost:8080/api/login e post seu username e password.

Viu? Que massa? Autenticamos e recebemos o token! Dividi o código acima em 3 partes principais.

Antes, da //1, setamos as dependencias que usaremos neste arquivo com os require. Depois usamos um SE o req.body.username ou o req.body.passwordforem vazios, res.send(401). Se tiverem preenchidos, prosseguimos para:

  1. Usuario.findOne()
    Buscamos por usuários com os dados informados. Se der erro, res.end(401). Se não der erro, continua, retornando o resultado como user.

  2. user.verificaSenha lembra do método que criamos no ModelUsuario? pois é. Se ele não retornar o parâmetro isMatch, res.end(401) neles!

  3. var expires: usamos o moment pra dizer ‘daqui a 7 dias’ e var token, onde criamos o token com jwt.encode(). Note a variável segredo definida no começo do arquivo. Vamos precisar dela mais pra frente :P

5) ULTIMA PARTE: O saudoso Middleware para conferir o token e fazer a segurança das rotas

Vamos criar um middleware de nível route para interceptar as requisições nas rotas protegidas e conferir o TOKEN. Basicamente, ele vai testar o token, se der certo, chama next(), senão: res.end(401) neles!

Crie o arquivo validarJWT.js

[validarJWT.js][Crie o arquivo]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var model = require('./ModelUsuario')
, jwt = require('jwt-simple')
, segredo = 'seusegredodetoken';
module.exports = function(req, res, next) {
var token = (req.body && req.body.access_token) || (req.query && req.query.access_token) || req.headers['x-access-token'];
//1
if (token) {
try {
var decoded = jwt.decode(token, segredo);
console.log('decodando ' + decoded);
//2
if (decoded.exp <= Date.now()) {
res.json(400, {error: 'Acesso Expirado, faça login novamente'});
}
//3
model.findOne({ _id: decoded.iss }, function(err, user) {
if(err)
res.status(500).json({message: "erro ao procurar usuario do token."})
req.user = user;
console.log('achei usuario ' + req.user)
return next();
});
//4
} catch (err) {
return res.status(401).json({message: 'Erro: Seu token é inválido'});
}
} else {
res.json(401, {message: 'Token não encontrado ou informado'})
}
};

No //1 SE existir um token, rodamos a funçaõ jwt.decode().

No //2 , usamos o resultado do jwt.decode() para verificar se o token expirou.

no //3 pesquisamos no MongoDB pelo _id que veio no JWT

no //4 ficamos de olho por erros, caso aconteça, 401 neles.

Agora iremos inserir o validarJWT nas rotas à proteger isso será feito em nosso server.js. Iremos proteger apenas a rota .get() do /usuarios:

[server.js][Insira o middleware validarJWT.js nas rotas protegidas]
1
2
3
4
5
(...)
router.route('/usuarios')
.get(require('./validarJWT'), rotas.getUsuarios) //alteramos esta
.post(rotas.postUsuarios);
(...)

Novamente não é boa prática usar o require como parâmetro de função. Pelo bem do fluxo do tutorial e arquivo final, estou fazendo, mas o ideal seria criar var validarJWT = require('./validarJWT')….

Anyway, reinicie o server, faça um GET para http://localhost:8080/api/usuarios . Viu: Ele disse “TOKEN NAO ENCONTRADO”

Tudo pronto!!! Vamos usar nosso token :D

Dessa vez não precisa nem reiniciar o server. Faça um POST para http://localhost:8080/api/login com seu username e password.

[resposta do login]
1
2
3
4
5
6
7
8
9
10
{
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiI1NGRjM2RkODU5ZTQwNmZjMWE3YWJiOGIiLCJleHAiOjE0MjQzMjgxMzI0NzN9._RcLvHkjttELVpeNtYuCdn3ZkojwSKLA42V6ChZLi4s",
"expires": 1424328132473,
"user": {
"_id": "54dc3dd859e406fc1a7abb8b",
"username": "rcdev",
"password": "$2a$05$mXgWbp14wuvDmj3pX0NFBOgIeSyIACAmEOmYgr/41gUManjIVYJ9.",
"__v": 0
}
}

Vamos copiar o token (sim, ele inteiro) e adicionar como header x-access-tokenem nossa próxima requisição.

Faça um GET para /api/usuarios novamente, dessa vez com o token recebido como header chamada x-access-token:

token-sucesso

Parabéns e obrigado à você que acompanhou até aqui :P

Espero poder ter ajudado. Se tiver dicas, dúvidas ou observações, por favor comente :)

O resultado de nossa empreitada esta neste repositório do github