Desenvolvendo REST APIs com NodeJS – Parte II

Se caiu diretamente nesta página e não sabe o que está acontecendo, de uma olhada antes nesse link: Desenvolvendo REST APIs com NodeJS – Parte I.

No link acima, desenvolvi com Node.JS, MongoDB e Express uma REST API mínima de um blog, com usuários e suas postagens, explicando detalhadamente cada passo. O código está disponível no github: https://github.com/codinomedeveloper/node-api.

Segurança

Agora vamos nos preocupara com a proteção dos nossos serviços utilizando o middlewere Passport e algumas de suas estratégias para fazer a autenticação e a autorização na nossa API.

Autorização e Autenticação

Vamos instalar algumas dependências:

$ npm install passport passport-jwt passport-local jsonwebtoken --save

A seguir o código fonte do arquivo /src/auth/passport.js. Neste arquivo vamos configurar as estratégias de login e autorização via JWT.

var passport = require('passport')
var LocalStrategy = require('passport-local').Strategy
var passportJWT = require('passport-jwt')
var JWTStrategy = passportJWT.Strategy
var ExtractJwt = passportJWT.ExtractJwt
var User = require('../model/user')

passport.use(new LocalStrategy({
    usernameField: 'username',
    passwordField: 'password'
},
    async function (username, password, cb) {
        try {
            var user = await User.findOne({ username, password }).exec()
            if (!user) {
                return cb(null, false, { message: 'Nome de usuário ou senha incorreta.' })
            }
            return cb(null, user._id.toString(), { message: 'Atenticado com sucesso!' })
        }
        catch (err) {
            return cb(err)
        }
    }
))


passport.use(new JWTStrategy(
    {
        jwtFromRequest: ExtractJwt.fromAuthHeaderWithScheme('JWT'),
        secretOrKey: 'your_jwt_secret'
    },
    async (jwt_payload, done) => {
        try {
            var user = await User.findById(jwt_payload.id)
            if (user) {
                return done(null, user)
            } else {
                return done(null, false)
            }
        } catch (err) {
            return done(err, false)
        }
    }
))

module.exports = passport

No código anterior, utilizamos a LocalStrategy para fazer o login em nossa API. Caso o resultado da consulta por usuário e senha confirme a autenticidade do usuário devolvemos uma mensagem de sucesso.

No segundo trecho do código acima, configuramos o JWTStrategy, que servirá para validar o JSON Web Token. Veremos a seguir o serviço de login, cujo métodos deve ser acrescentado na classe de usuário. Neste serviço veremos a geração do JWT:

async login(req, res, next) {
    passport.authenticate('local', { session: false }, (err, user, info) => {
        if (err || !user) {
            return res.status(400).json({
                message: 'Alguma coisa está errada!',
                user: user
             });
        }
        req.login(user, { session: false }, (err) => {
            if (err) {
                res.send(err);
            }
            const token = jwt.sign({ id: user }, 'your_jwt_secret');
            return res.json({ user, token });
        });
    })(req, res);
}

Agora para que tudo isso funcione, devemos alterar nossas rotas, acrescentando o serviço de login e também a chamada do middleware para validação do JWT nos serviços a serem protegidos.

var UserController = require('./controller/UserController')
var PostController = require('./controller/PostController')
var passport = require('./auth/passport')

module.exports = function (app) {

    var jwtMid = passport.authenticate('jwt', { session: false })
    var findPostMid = PostController.findPostMiddleware
    var findUserMid = UserController.findUserMiddleware
    //USER
    app.post('/login', UserController.login);
    app.get('/users', UserController.index)
    app.post('/user', UserController.save)
    app.get('/user/:userId', findUserMid, UserController.get)
    app.put('/user/:userId', jwtMid, findUserMid, UserController.update)
    app.delete('/user/:userId', jwtMid, UserController.delete)

    //POST
    app.get('/user/:userId/posts', PostController.index)
    app.post('/user/:userId/post', jwtMid, PostController.save)
    app.get('/user/:userId/post/:id', findPostMid, PostController.get)
    app.put('/user/:userId/post/:id', jwtMid, findPostMid, PostController.update)
    app.delete('/user/:userId/post/:id', jwtMid, findPostMid, PostController.delete)
}

Agora você pode utilizar o Postman para testar todos os serviços.

Capturar0
Testando o login. Veja o token na resposta quando o login efetuado com sucesso.
Capturar
Veja que para utilizar um serviço que reques autorização é preciso informar o token no header da requisição.

Autorização dobre os dados

Temos dois cuidados que ainda devem ser tomados, o primeiro diz respeito em averiguar se os dados do usuário do JWT são condizentes com os dados que este usuário deseja acessar. Por exemplo, um usuário não pode alterar a postagem de outro usuário. Vamos resolver isso fazendo uma alteração nos middlewares:

/src/controller/UserController.findUserMiddleware

async findUserMiddleware(req, res, next) {
    if (req.user && req.user._id != req.params.userId) {
        return res.status(403).json({
            'message': 'Usuário não tem permissão para acessar estes dados.' 
        })
    }
    var user = await User.findOne({ _id: req.params.userId }).exec()
    if (!user) {
       return res.status(404).json({ 'message': 'Usuário não encontrado' })
    }
    req.user = user
    next()
}

/src/controller/PostController.findPostMiddleware

async findPostMiddleware(req, res, next) {
    if (req.user && req.user._id != req.params.userId) {
        return res.status(403).json({ 'message': 'Usuário não tem permissão para acessar estes dados.' })
    }
    var post = await Post.findOne({
        _id: req.params.id,
        user: req.params.userId
    }).populate('user').exec()
    if (!post) {
       return res.status(404).json({ 'message': 'Post não encontrado' })
    }
    req.post = post
    next(null)
}

Cripitografia

O segundo ponto são os cuidados com as credenciais do usuário: criptografia das senhas, unicidade do usuário e proteção da senha nos JSON dos serviços dos usuários. Primeiro passo instalar o crypto-js:

npm install crypto-js --save

Esse modulo serve para criptografar e validar o password. Vamos utilizar isso no model de usuário criando um middleware  para criptografar a senha:

/src/model/user.js:

var sha256 = require('crypto-js/sha256')

...

userScheme.pre('save', function(next) {
    this.password = sha256(this.password)
    next();
});

Próximo passo é alterar o LocalStrategy, onde é feita a consulta do usuário no login.

/src/auth/passport.js

...
var sha256 = require('crypto-js/sha256')

passport.use(new LocalStrategy({
    usernameField: 'username',
    passwordField: 'password'
},
    async function (username, password, cb) {
        try {
            password = sha256(password).toString()
            var user = await User.findOne({ username }).exec()
            if (!user || password !== user.password) {
                return cb(null, false, { message: 'Nome de usuário ou senha incorreta.' })
            }
            return cb(null, user._id.toString(), { message: 'Atenticado com sucesso!' })
        }
        catch (err) {
            return cb(err)
        }
    }
))
...

Vemos no código acima a alteração da consulta que busca o usuário somente pelo username, isso nos leva ao último ponto a ser atacado, a unicidade do nome do usuário.

Unicidade do username

Precisamos de um plugin do Mongoose que trata index de unicidade:

npm install --save mongoose-unique-validator

Voltaremos ao arquivo que define o modelo de dados do usuário.

/src/model/user.js

var mongoose = require('mongoose')
var sha256 = require('crypto-js/sha256')
var uniqueValidator = require('mongoose-unique-validator');

var userScheme = new mongoose.Schema({
    username: { type: String, require: true, unique: true },
    password: { type: String, require: true },
});

userScheme.plugin(uniqueValidator);

userScheme.pre('save', function(next) {
    this.password = sha256(this.password)
    next();
});

exports = module.exports = mongoose.model('User', userScheme);

Conclusão

Desta forma temos uma REST API completa com controle de usuários e suas postagens. Esta aplicação pode servir de esqueleto para aplicações maiores e ficará disponível no github para que também possa ser feitas sugestões e correções.

Caso queira saber como implantar uma aplicação dessas veja este outro tutorial aqui do blog:

Tutorial: Como implantar seu aplicativo Node.js na DigitalOcean com SSL

3 comentários em “Desenvolvendo REST APIs com NodeJS – Parte II

  1. Bom dia excelente artigo. Está me ajudando bastante. Mas fiquei com uma dúvida: Digamos que esse serviço que você criou seja um microservice e que você tenha mais outro microservice parecido com esse e os dois sejam consumidos por uma aplicação. Nesse caso não faria sentido o usuário se logar em cada uma das APIs. Pesquisando descobri que nesse caso deveríamos utilizar uma API Gateway, mas se eu aplicar a segurança só na API Gateway, os serviços não ficariam desprotegidos? Digamos que eu queira publicar os dois serviços na DigitalOcean, como aplicar uma segurança como essa que você implementou direto na api gateway e manter os dois serviços seguros também?

    Curtir

    1. Considere utilizar uma API Gateway como: https://www.express-gateway.io/. Desta forma toda a questão de segurança deve ficar por conta do Gateway escolhido. Por meio de firewall, por exemplo, você deixa seus serviços apenas acessíveis para o API Gateway. Cada solução de gateway pode te oferecer soluções diversas nesse sentido, cabe você escolher um gateway que mais se adeque a sua solução .

      Curtir

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Foto do Google

Você está comentando utilizando sua conta Google. Sair /  Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s

Este site utiliza o Akismet para reduzir spam. Saiba como seus dados em comentários são processados.