DEV Community

Cover image for Autenticação com JWT
Vitor Silva Delfino
Vitor Silva Delfino

Posted on

Autenticação com JWT

Neste post, vamos escrever um middleware de autenticação e um módulo de login.

O serviço de login vai receber um payload com usuário e senha, após tudo ser validado na base, vamos gerar um token do tipo JWT e devolver pro client.

Para a geração do token, vamos utilizar a lib jsonwebtoken

Todas as outras requests, precisarão desse token no cabeçalho, assim garantimos que é uma request feita por um usuário previamente autenticado, e tudo que antes necessitava do ID do usuário, agora podemos pegar de dentro do token.

Instalações

 yarn add jsonwebtoken && yarn add -D @types/jsonwebtoken 
Enter fullscreen mode Exit fullscreen mode

Configurações

Após a instalação da lib, vamos criar uma variável de ambiente que vai servir como uma chave secreta. Ela vai ser utilizada no momento da geração do token

.env.dev

 PORT=3000 DATABASE_MONGO_CONN=mongodb://localhost:27017/example SECRET=0917B13A9091915D54B6336F45909539CCE452B3661B21F386418A257883B30A 
Enter fullscreen mode Exit fullscreen mode

E agora vamos importar esse hash nas configs
src/config/index.ts

 ... export const auth = { secret: String(process.env.SECRET), expires: '1h', }; 
Enter fullscreen mode Exit fullscreen mode

Código

Vamos começar criando uma pasta Auth dentro de apps

Alt Text

E vamos criar um service.

Responsabilidade da service:

1 - Vamos buscar o usuário na base
2 - Caso o usuário não exista devolvemos um erro
3 - Caso o usuário exista, geramos um token e devolvemos
4 - Se acontecer algum outro erro, devolvemos uma falha interna

src/apps/Auth/AuthService.ts

 /* eslint-disable no-underscore-dangle */ import { CustomError } from 'express-handler-errors'; import { sign } from 'jsonwebtoken'; import { MongoRepository, getConnection } from 'typeorm'; import { dbConnections, auth } from '@config/index'; import { Users } from '@apps/Users/Users.entity'; import logger from '@middlewares/logger'; class AuthService { private readonly repository: MongoRepository<Users>; constructor() { this.repository = getConnection( dbConnections.mongo.name ).getMongoRepository(Users); } async auth(data: { document: string; password: string; }): Promise<{ token: string }> { const { document, password } = data; logger.info(`AuthService::auth::`, data); try { // Buscando usuário const user = await this.repository.findOne({ document, password }); // Validando existência if (!user) { throw new CustomError({ code: 'USER_NOT_FOUND', message: 'Usuário não encontrado', status: 404, }); } // Gerando token const token = await sign( { _id: user._id, document: user.document, name: user.name, }, auth.secret, { expiresIn: auth.expires, } ); return { token, }; } catch (e) { if (e instanceof CustomError) throw e; logger.error(`AuthService::auth::${e.message}`); throw new CustomError({ code: 'ERROR_AUTHENTICATE', message: 'Erro ao autenticar', status: 500, }); } } } export default new AuthService(); 
Enter fullscreen mode Exit fullscreen mode

E então criamos o controller, um validator e a rota

src/apps/Auth/AuthController.ts

 import { Request, Response } from 'express'; import AuthService from './AuthService'; export const auth = async (req: Request, res: Response): Promise<Response> => { const { document, password } = req.body; const response = await AuthService.auth({ document, password }); return res.json(response); }; 
Enter fullscreen mode Exit fullscreen mode

src/apps/Auth/validator.ts

 import { NextFunction, Request, Response } from 'express'; import yup from '@config/yup'; export const validateAuthPayload = async ( req: Request, _: Response, next: NextFunction ): Promise<void> => { await yup .object() .shape({ document: yup.string().length(11).required(), password: yup.string().min(6).max(10).required(), }) .validateSync(req.body, { abortEarly: false }); return next(); }; 
Enter fullscreen mode Exit fullscreen mode

src/apps/Auth/routes.ts

 import { Router } from 'express'; import * as controller from './AuthController'; import { validateAuthPayload } from './validator'; const routes = Router(); routes.post('/', validateAuthPayload, controller.auth); export default routes; 
Enter fullscreen mode Exit fullscreen mode

E vamos adicionar o path '/auth' no arquivo de rotas raiz.

src/routes.ts

 import { Router } from 'express'; import * as controller from './AuthController'; import { validateAuthPayload } from './validator'; import 'express-async-errors'; const routes = Router(); routes.post('/', validateAuthPayload, controller.auth); export default routes; 
Enter fullscreen mode Exit fullscreen mode

Realizando Login

Criei um usuário com os requests que já existe

Alt Text

Agora vou atualizar o arquivo de requests com o endpoint de login

requests.http

 ... POST http://localhost:3000/api/auth HTTP/1.1 Content-Type: application/json { "document": "42780908890", "password": "123456" } 
Enter fullscreen mode Exit fullscreen mode

Alt Text

Podemos ver o token, na resposta da autenticação

Se colarmos esse token no site https://jwt.io, podemos ver a informação armazenada dentro dele, mas só com a secret conseguimos fazer a validação.

Então, nunca devemos gravar informações sensíveis dentro do token

Alt Text

Middleware

Antes de escrever o middleware, vamos modificar a interface do express.

No primeiro tutorial, adicionamos o campo id dentro da request.
Agora vamos adicionar o campo user com os tipos do payload do nosso token.

src/@types/express/index.d.ts

 declare namespace Express { interface Request { id: string; user: { _id: string; document: string; name: string; }; } } 
Enter fullscreen mode Exit fullscreen mode

Alt Text

Agora, vamos escrever um middleware que vai receber esse token e fazer a validação

src/middlewares/authorize

 import { Request, Response, NextFunction } from 'express'; import { CustomError } from 'express-handler-errors'; import { verify } from 'jsonwebtoken'; import { auth } from '@config/index'; import logger from '@middlewares/logger'; export const authorize = ( req: Request, _: Response, next: NextFunction ): void => { // coletamos o token do header da requisição const token = req.headers.authorization; logger.info(`Authorize::validate token::${token}`); // se não existir o token, devolvemos 401, que é o HTTP code para não autorizado if (!token) return next( new CustomError({ code: 'UNAUTHORIZED', message: 'Token não enviado', status: 401, }) ); try { // Aqui fazemos a validação do token const decoded = verify(token, auth.secret) as any; req.user = decoded; logger.info(`Authorize::user authorized::`); // No sucesso da validação a request segue em frente ... return next(); } catch (e) { // Se der erro na validação, devolvemos 401 novamente logger.error(`Authorize::error decode token::${e.message}`); return next( new CustomError({ code: 'UNAUTHORIZED', message: 'Token inválido', status: 401, }) ); } }; 
Enter fullscreen mode Exit fullscreen mode

Para utilizar o middleware, vamos alterar o método findOne do módulo User

src/config/index.ts

 ... export type IUserRequest = { _id: string; document: string; name: string; }; ... 
Enter fullscreen mode Exit fullscreen mode

src/apps/User/UserService.ts

 ... async findOne(userAuthenticated: IUserRequest): Promise<Users> { const user = await this.repository.findOne(userAuthenticated._id); if (!user) throw new CustomError({ code: 'USER_NOT_FOUND', message: 'Usuário não encontrado', status: 404, }); return user; } ... 
Enter fullscreen mode Exit fullscreen mode

E passar o userAuthenticated no controller

src/apps/User/UserController.ts

 ... export const findOne = async ( req: Request, res: Response ): Promise<Response> => { const response = await UserService.findOne(req.user); return res.json(response); }; ... 
Enter fullscreen mode Exit fullscreen mode

Agora, passamos o middleware na rota e podemos realizar o teste

src/apps/User/routes.ts

 import { Router } from 'express'; import * as controller from './UserController'; import { validateUserPayload } from './validator'; import 'express-async-errors'; import { authorize } from '@middlewares/authorize'; const route = Router(); route.post('/', validateUserPayload, controller.create); route.get('/', authorize, controller.findOne); route.put('/:id', controller.update); route.delete('/:id', controller.deleteOne); export default route; 
Enter fullscreen mode Exit fullscreen mode

Para realizar o teste, vamos alterar a request dentro de requests.http

Alt Text

 ... GET http://localhost:3000/api/users HTTP/1.1 Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MDY0YjU1NjBlMTJkZjBiOWVjY2JjZWUiLCJkb2N1bWVudCI6IjQyNzgwOTA4ODkwIiwibmFtZSI6IlZpdG9yIiwiaWF0IjoxNjE3MjE2NTE1LCJleHAiOjE2MTcyMjAxMTV9.oZSom3PhiuLp554A_R4VajBV67T1Sb3DbCEGkNwMCEE ... 
Enter fullscreen mode Exit fullscreen mode

Alt Text

Estamos utilizando a informação de dentro do token, para resgatar um usuário da nossa base.

Tests

E como ficam os testes unitários que escrevemos ????

Como alteramos o serviço, os testes agora estão quebrando.

Alt Text

Vamos refatorar o teste existente.

Nós vamos precisar escrever um novo escopo em nossa switch de testes.

Como o token, tem uma expiração de 1h, fica inviável ter que gerar sempre um novo token para rodar os testes.

Nesses casos, vamos usar a função afterEach, para limpar o mock feito para o middleware de autenticação.

tests/User/user.test.ts

 import { MockProxy } from 'jest-mock-extended'; import jwt from 'jsonwebtoken'; import request from 'supertest'; import { MongoRepository } from 'typeorm'; ... describe('## GET ##', () => { // Aqui estamos restaurando o mock afterEach(() => { jest.resetAllMocks(); }); test('should return error when user does not exists', async () => { /** * Vamos espionar a função verify, * a mesma utilizada no middleware e modificar o seu comportamento * é um outro jeito de mocar funções com jest * */ const spy = jest.spyOn(jwt, 'verify'); spy.mockReturnValue({ _id: '6064b5560e12df0b9eccbcee', document: '42780908890', name: 'Vitor', } as any); repository.findOne.mockResolvedValue(null); await request(app) .get('/api/users') .set('Authorization', 'token') .expect(404, { errors: [ { code: 'USER_NOT_FOUND', message: 'Usuário não encontrado', status: 404, }, ], }); }); test('should return an user', async () => { const spy = jest.spyOn(jwt, 'verify'); spy.mockReturnValue({ _id: '6064b5560e12df0b9eccbcee', document: '42780908890', name: 'Vitor', } as any); const user = { _id: '6064b5560e12df0b9eccbcee', name: 'Teste', password: '1234', }; repository.findOne.mockResolvedValue(user); await request(app) .get('/api/users') .set('Authorization', 'token') .expect(200, user); }); }); ... 
Enter fullscreen mode Exit fullscreen mode

Vamos escrever os testes do login

tests/Auth/auth.test.ts

 import { MockProxy } from 'jest-mock-extended'; import jwt from 'jsonwebtoken'; import request from 'supertest'; import { MongoRepository } from 'typeorm'; jest.mock('typeorm'); jest.mock('../../src/middlewares/logger'); describe('## Auth Module ##', () => { const { app } = require('../../src/app').default; const repository = require('typeorm').mongoRepositoryMock as MockProxy< MongoRepository<any> >; describe('## Login ##', () => { afterEach(() => { jest.resetAllMocks(); }); test('should return error when user does not exists', async () => { repository.findOne.mockResolvedValue(null); await request(app) .post('/api/auth') .send({ document: '42780908890', password: '123456' }) .expect(404, { errors: [ { code: 'USER_NOT_FOUND', message: 'Usuário não encontrado', status: 404, }, ], }); }); test('should return an token', async () => { repository.findOne.mockResolvedValue({ _id: '6064b5560e12df0b9eccbcee', document: '42780908890', name: 'Vitor', }); const spy = jest.spyOn(jwt, 'sign'); const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MDY0YjU1NjBlMTJkZjBiOWVjY2JjZWUiLCJkb2N1bWVudCI6IjQyNzgwOTA4ODkwIiwibmFtZSI6IlZpdG9yIiwiaWF0IjoxNjE3MjE2NTE1LCJleHAiOjE2MTcyMjAxMTV9.oZSom3PhiuLp554A_R4VajBV67T1Sb3DbCEGkNwMCEE'; spy.mockReturnValue(token as any); await request(app) .post('/api/auth') .send({ document: '42780908890', password: '123456' }) .expect(200, { token, }); }); test('should return error when generate token', async () => { repository.findOne.mockResolvedValue({ _id: '6064b5560e12df0b9eccbcee', document: '42780908890', name: 'Vitor', }); const spy = jest.spyOn(jwt, 'sign'); spy.mockImplementation(() => { throw new Error('Error to generate token'); }); await request(app) .post('/api/auth') .send({ document: '42780908890', password: '123456' }) .expect(500, { errors: [ { code: 'ERROR_AUTHENTICATE', message: 'Erro ao autenticar', status: 500, }, ], }); }); }); }); 
Enter fullscreen mode Exit fullscreen mode

E o resultado do coverage fica assim

Alt Text

Considerações finais

Para finalizar, vamos atualizar o swagger

No get do usuário, vamos remover o parâmetro id

src/apps/User/swagger.ts

 const paths = { '/users/{id}': { ... }, '/users': { get: { tags: ['User'], summary: 'User', description: 'Get user by Id', security: [ { Bearer: [], }, ], parameters: [ { in: 'path', name: 'id', required: true, schema: { type: 'string', }, description: 'uuid', }, ], responses: { 200: { description: 'OK', schema: { $ref: '#/definitions/User', }, }, 404: { description: 'Not Found', schema: { $ref: '#/definitions/ErrorResponse', }, }, 500: { description: 'Internal Server Error', schema: { $ref: '#/definitions/ErrorResponse', }, }, }, }, ... }, }, }; const definitions = { User: { type: 'object', properties: { _id: { type: 'string' }, name: { type: 'string' }, document: { type: 'string' }, password: { type: 'string' }, createdAt: { type: 'date' }, updatedAt: { type: 'date' }, }, }, UserPayload: { type: 'object', properties: { name: { type: 'string' }, document: { type: 'string' }, password: { type: 'string' }, }, }, }; export default { paths, definitions, }; 
Enter fullscreen mode Exit fullscreen mode

E vamos escrever o swagger do módulo Auth

src/apps/Auth/swagger.ts

 const paths = { '/auth': { post: { tags: ['Auth'], summary: 'Auth', description: 'Authenticate User', security: [ { Bearer: [], }, ], parameters: [ { in: 'body', name: 'update', required: true, schema: { $ref: '#/definitions/AuthPayload', }, }, ], responses: { 200: { description: 'OK', schema: { $ref: '#/definitions/AuthResponse', }, }, 404: { description: 'Not Found', schema: { $ref: '#/definitions/ErrorResponse', }, }, 500: { description: 'Internal Server Error', schema: { $ref: '#/definitions/ErrorResponse', }, }, }, }, }, }; const definitions = { AuthResponse: { type: 'object', properties: { token: { type: 'string' }, }, }, AuthPayload: { type: 'object', properties: { document: { type: 'string' }, password: { type: 'string' }, }, }, }; export default { paths, definitions, }; 
Enter fullscreen mode Exit fullscreen mode

Alt Text

Top comments (5)

Collapse
 
vitorcalvi profile image
Vitor Calvi

Segue o arquivo src/routes.ts corrigido
import { Router } from 'express';

import * as controller from './apps/Auth/AuthController';
import { validateAuthPayload } from './apps/Auth/validator';

import UserRoutes from '@apps/Users/routes';
import 'express-async-errors';

const route = Router();

route.use('/users', UserRoutes);
route.post('/auth', validateAuthPayload, controller.auth);
export default route;

Collapse
 
vitorcalvi profile image
Vitor Calvi

Na requisição do Token JWT, dá erro:

info: AuthService::auth:: {"document":"42780908890","password":"123456"}
error: AuthService::auth::Cannot read property 'prototype' of undefined
error: ErrorHandle::handle::Error::Erro ao autenticar

Collapse
 
vitorcalvi profile image
Vitor Calvi

Vitor, no src/routes.ts, os imports corretos são:
import * as controller from './apps/Auth/AuthController';
import { validateAuthPayload } from './apps/Auth/validator';

Collapse
 
vitorcalvi profile image
Vitor Calvi

No arquivo src/apps/User/UserService.ts
faltou importar...

import { dbConnections,IUserRequest } from '@config /index';

Collapse
 
vitorcalvi profile image
Vitor Calvi

No tutorial, src/middlewares/authorize
está faltando .ts no arquivo