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
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
E agora vamos importar esse hash nas configs
src/config/index.ts
...
export const auth = {
secret: String(process.env.SECRET),
expires: '1h',
};
Código
Vamos começar criando uma pasta Auth
dentro de apps
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();
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);
};
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();
};
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;
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;
Realizando Login
Criei um usuário com os requests que já existe
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"
}
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
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;
};
}
}
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,
})
);
}
};
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;
};
...
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;
}
...
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);
};
...
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;
Para realizar o teste, vamos alterar a request dentro de requests.http
...
GET http://localhost:3000/api/users HTTP/1.1
Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MDY0YjU1NjBlMTJkZjBiOWVjY2JjZWUiLCJkb2N1bWVudCI6IjQyNzgwOTA4ODkwIiwibmFtZSI6IlZpdG9yIiwiaWF0IjoxNjE3MjE2NTE1LCJleHAiOjE2MTcyMjAxMTV9.oZSom3PhiuLp554A_R4VajBV67T1Sb3DbCEGkNwMCEE
...
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.
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);
});
});
...
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,
},
],
});
});
});
});
E o resultado do coverage fica assim
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,
};
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,
};
Top comments (5)
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
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;
No arquivo src/apps/User/UserService.ts
faltou importar...
import { dbConnections,IUserRequest } from '@config /index';
No tutorial, src/middlewares/authorize
está faltando .ts no arquivo
Vitor, no src/routes.ts, os imports corretos são:
import * as controller from './apps/Auth/AuthController';
import { validateAuthPayload } from './apps/Auth/validator';