Como podemos autenticar nossos usuários afim de que algumas páginas sejam restritas e outras sejam públicas? Uma solução para tal problema pode ser a autenticação JWT!
- JSON
- WEB
- TOKEN
O funcionamento é bastante simples:
- O usuário envia no corpo da requisição o email e senha.
- O servidor fará uma pesquisa no banco procurando pelo email informado.
- Caso o email informado não seja encontrado, será retornado algum erro.
- Caso o email informado seja encontrado, então as senhas serão comparadas.
- Caso as senhas coincidam, será gerado um token de autenticação contendo algumas informações não senssíveis do usuário.
- Esse token será devolvido ao front-end, que por sua vez o armazenará no localStorage.
- A partir de agora, o token será enviado no header de toda requisição feita ao servidor.
- O servidor por sua vez terá o papel de verificar se aquele token é valido e decidir se permitirá que o usuário continue navegando ou não.
✔️ Vale lembrar que o token em questão pode ou não ter uma data para expirar.
🔓 O token em si não é criptografado, você pode visualizar as informações contidas nele acessando: https://jwt.io/
🔑 Mas existe uma chave de assinatura contida no servidor, essa chave serve para assinar o token, de modo que se alguém tentar enviar um token falsificado, o servidor terá como verificar que aquele token não é verídico.
- Passport
- O Passport é um middleware de autenticação para o Node . Ele foi projetado para servir a um propósito único: autenticar solicitações.
- Passport-local
- Estratégia de passaporte para autenticação com nome de usuário e senha.
- Passport-jwt
- Uma estratégia do Passport para autenticação com um JSON Web Token.
- Jsonwebtoken
- Biblioteca utilizada para gerar, verificar, decodificar o token.
- Bcrypt
- Biblioteca utilizada para criptografar as senhas do banco
Execute as migrations:
$ npx sequelize db:migrate
Execute as seeds:
$ npx sequelize db:seed:all
Instale as dependências:
$ npm i
Inicie o projeto:
$ npm start
O projeto está dividido em uma sequencia lógica de três modulos:
- SignUp (Registro de usuário)
- SignIn (Login de usuário)
- Dashboard (Disponível apenas a usuários autenticados)
Tudo começa com o registro de um usuário:
Depois disso, podemos logar com usuário e senha:
Como podemos observar, o usuário dispara uma requisição para o servidor enviando usuário e senha, antes desses dados serem entregues ao controller, eles passam por um middlware:
router.post('/', secureLocal, controller.signIn);
O secureLocal se refere a isso, que está sendo exportado de Auth:
const secureLocal = passport.authenticate('local', { session: false });
Ou seja, estamos falando da estratégia de autenticação local do passport:
// LOCAL STRATEGY
passport.use(
new LocalStrategy(async (username, password, done) => {
// Find the user given the email
let userModel;
try {
userModel = await user.findOne({
where: { username, isActive: true },
attributes: ['id', 'username', 'password'],
});
} catch (err) {
return done(null, err);
}
// If not, handle it
if (!userModel) {
return done(null, false);
}
// Check if the password is correct
const userPassword = userModel.get('password');
const userObj = userModel.get({ plain: true });
if (!(await bcrypt.compare(password, userPassword))) {
return done(null, false);
}
delete userObj.password;
return done(null, userObj);
}),
);
};
Observando esse código, podemos facilmente notar que a unica coisa que ele está fazendo é verificar no banco se existe algum usuário igual ao informado, se sim, então ele utiliza o metodo compare do bcrypt para comparar as senhas, em caso de false a seguir vem a instrução de retorno do passport.
No caso do usuário e senha realmente baterem, então simplesmente excluímos a senha do objeto usuário e o retornamos:
delete userObj.password;
return done(null, userObj);
Após isso, seguindo para o controller, o token será gerado com base no usuário retornado.
signIn: (req, res) => {
const { user } = req;
// Generate token
const token = JWT.sign(
{
user,
},
config.get('authentication.jwtSecret'), { expiresIn: '1h' } // Expires in 1h
);
res.json({ token: `bearer ${token}`, username: user.username });
},
Aqui podemos definir algumas coisas interessantes: Tempo de duração do token:
{ expiresIn: '1h' } // Expires in 1h
SECRET_KEY:
config.get('authentication.jwtSecret')
Aqui vale uma breve explicação sobre o SECRET_KEY, que é a chave de assinatura do token conforme já falamos no início deste arquivo, nesse caso armazenamos o SECRET_KEY dentro do arquivo do convict: src/config.js.
authentication: {
jwtSecret: {
doc: 'This is a key to dealing with tokens',
format: String,
default: null,
env: 'JWT_SECRET',
},
},
Fizemos isso pois não é legal que algo tão senssível esteja no meio do código, senão qualquer um que tenha acesso ao código poderia ter acesso ao SECRET_KEY.
Vale lembrar também, que o SECRET_KEY deve ser chamado no arquivo .env para que funcione:
NODE_ENV=development
JWT_PORT=3000
JWT_DB_NAME=databaseTest
JWT_DB_USER=userTest
JWT_DB_PASSWORD=#Teste34524519
JWT_DB_DIALECT=mysql
JWT_DB_HOST=127.0.0.1
JWT_DB_PORT=3306
JWT_DB_POOL_MAX=25
JWT_SECRET=d65sf4-6sd5f4-6vs1v-s65s4x
Bearer: É necessário que o token seja concatenado com o prefixo Bearer antes de ser enviado ao front-end, pois o Bearer indica que está sendo trafegado um token e não um usuário e senha, que é o caso da flag Basic.
Com o token em mãos, o front-end o enviará nos headers das próximas requisições:
O objetivo de enviar o token na requisição é verificar se aquele usuário é autentico e se tem permissão para acessar tal rota.
Novamente tudo começa no arquivo de rotas:
router.get('/', secureJwt, controller.test);
Dessa vez utilizamos a estratégica JWT:
// JSON WEB TOKENS STRATEGIES
passport.use(
new JwtStrategy(
{
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken('authorization'),
secretOrKey: config.get('authentication.jwtSecret'),
},
async (payload, done) => {
// Find the user specified in token
let userModel;
try {
userModel = await user.findOne({
where: { id: payload.id },
});
} catch (err) {
return done(null, err);
}
// If user doesn't exists, handle it
if (!userModel) {
return done(null, false);
}
// Otherwise, return the user
return done(null, user);
},
),
);
Em caso de sucesso, estamos autenticados e podemos seguir para o controller, ao infinito e além!
test: async (req, res) => {
console.log('It works!');
res.json({ message: 'It works!' });
},
Julguei importante demonstrar alguns testes básicos referentes a autenticação JWT neste repositório, afinal de contas, se você está procurando aprender sobre JWT, uma hora ou outra terá que testar tal funcionalidade.
Para tal, utilizamos as bibliotecas Jest e SuperTest Inicie o projeto:
$ npm install jest supertest --save-dev
Caso você nunca tenha feito nada com testes, recomendo que acesse este repositório: https://github.com/Spinkers/javascript-jest Ele também é autoexplicativo.
Para rodar os testes use:
$ npm test
O teste de cadastro é simples, não há muito o que demonstrar, basicamente enviamos o usuário e senha via post e aguardamos uma mensagem de sucesso.
it('should responds with json and statusCode 201', async () => {
const response = {
message: 'Success!'
};
const user = {
username: 'USUARIO',
password: '12345678',
};
db.user.create.mockReturnValue({
get: jest.fn().mockReturnValue(response),
});
const res = await superTeste(app)
.post('/signup')
.send(user);
expect(res.statusCode).toStrictEqual(201);
expect(res.body).toStrictEqual(response);
});
O teste de login é mais interessante para quem está interessado em JWT, primeiro fazemos o mock da resposta da função findOne, que é constituída essencialmente por username e password, depois disso enviamos os parametros requeridos no corpo da requisição (que também é usuário e senha) desta forma:
const res = await superTeste(app).post('/signin').send({
username: 'Lucas',
password: '12345678',
});
No fim deste processo esperamos que o objeto de resposta contenha um token e um username, neste caso não podemos passar um objeto com propriedades fixas, pois a cada vez o token retornado seria diferente.
it('should return an json with token and username', async () => {
const mockUser = {
id: 1,
username: 'Lucas',
password: '$2b$12$nT/8GO9Ei1dPo0ylr6FD6e/rj.aQTheVl3/1AH3AZwyjz4hmvmDZC',
};
db.user.findOne.mockReturnValue({
get: jest.fn().mockReturnValue(mockUser),
});
const res = await superTeste(app).post('/signin').send({
username: 'Lucas',
password: '12345678',
});
const mockBodyResponse = {
token: res.body.token,
username: res.body.username,
}
expect(res.statusCode).toStrictEqual(200);
expect(res.body).toStrictEqual(mockBodyResponse);
});
O teste de autenticação, apesar de simples, é super interessante, neste caso simplesmente enviamos o token no header da requisição desta forma:
const res = await superTeste(app).get('/dashboard').set('Authorization', token);
Código completo:
it('should responds with json and statusCode 200', async () => {
const token =
'bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7ImlkIjoxLCJ1c2VybmFtZSI6ImFkbWluIn0sImlhdCI6MTU4MDc0MzM3OX0.kWqyYtYosb4wMaiaGjn_4TurMqcyZkGZz3lZQM__ClM';
const response = {
message: 'It works!'
}
db.user.findOne.mockReturnValue({
get: jest.fn().mockReturnValue(response),
});
const res = await superTeste(app)
.get('/dashboard')
.set('Authorization', token);
expect(res.statusCode).toStrictEqual(200);
expect(res.body).toStrictEqual(response);
});
I hope you enjoy!