Artigo original: Learn how to handle authentication with Node using Passport.js

Escrito por: Antonio Erdeljac

Ajude o autor lendo o texto no original: ORIGINAL (em inglês)

Neste artigo, você aprenderá a lidar com a autenticação para um servidor do Node usando o Passport.js. O artigo não tratará da autenticação no front-end. Use-o para configurar a autenticação no back-end (geração de token para cada usuário e proteção das rotas).

Lembre-se de que, se tiver problemas em alguma das etapas, é possível consultar a qualquer momento o repositório do GitHub.

Neste artigo, ensinarei o seguinte:

  • Tratamento de rotas protegidas
  • Tratamento de tokens do JWT
  • Tratamento de respostas não autorizadas
  • Criação de uma API básica
  • Criação de modelos e esquemas

Introdução

O que é o Passport.js?

O Passport é um middleware de autenticação para o Node.js. Por ser extremamente flexível e modular, o Passport pode ser colocado sem problemas em qualquer aplicação para a web baseada no Express. Um conjunto extenso de estratégias dá suporte à autenticação usando um nome de usuário e senha, Facebook, Twitter e muito mais. Saiba mais sobre o Passport aqui (documentação em inglês).

Tutorial

Criação do servidor do Node desde o início

Crie um diretório com este arquivo "app.js" dentro dele:

const express = require('express');
const path = require('path');
const bodyParser = require('body-parser');
const session = require('express-session');
const cors = require('cors');
const mongoose = require('mongoose');
const errorHandler = require('errorhandler');

//Configure mongoose's promise to global promise
mongoose.promise = global.Promise;

//Configure isProduction variable
const isProduction = process.env.NODE_ENV === 'production';

//Initiate our app
const app = express();

//Configure our app
app.use(cors());
app.use(require('morgan')('dev'));
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use(express.static(path.join(__dirname, 'public')));
app.use(session({ secret: 'passport-tutorial', cookie: { maxAge: 60000 }, resave: false, saveUninitialized: false }));

if(!isProduction) {
  app.use(errorHandler());
}

//Configure Mongoose
mongoose.connect('mongodb://localhost/passport-tutorial');
mongoose.set('debug', true);

//Error handlers & middlewares
if(!isProduction) {
  app.use((err, req, res) => {
    res.status(err.status || 500);

    res.json({
      errors: {
        message: err.message,
        error: err,
      },
    });
  });
}

app.use((err, req, res) => {
  res.status(err.status || 500);

  res.json({
    errors: {
      message: err.message,
      error: {},
    },
  });
});

app.listen(8000, () => console.log('Server running on http://localhost:8000/'));
app.js

Instalaremos o nodemon para facilitar o desenvolvimento.

npm install -g nodemon

Em seguida, vamos executar nosso "app.js" com ele.

$ nodemon app.js
1_6kdVzksHWBymrCL20pNyzA
Resultado esperado após executar o comando acima

Criação do modelo de usuário

Crie uma pasta chamada "models" e um arquivo "Users.js" dentro dessa pasta. É aí que definiremos nosso "UsersSchema", ou esquema de usuário. Usaremos JWT e Crypto para gerar o hash e o salt da string password recebida. Isso será usado mais tarde para a validação do usuário.

const mongoose = require('mongoose');
const crypto = require('crypto');
const jwt = require('jsonwebtoken');

const { Schema } = mongoose;

const UsersSchema = new Schema({
  email: String,
  hash: String,
  salt: String,
});

UsersSchema.methods.setPassword = function(password) {
  this.salt = crypto.randomBytes(16).toString('hex');
  this.hash = crypto.pbkdf2Sync(password, this.salt, 10000, 512, 'sha512').toString('hex');
};

UsersSchema.methods.validatePassword = function(password) {
  const hash = crypto.pbkdf2Sync(password, this.salt, 10000, 512, 'sha512').toString('hex');
  return this.hash === hash;
};

UsersSchema.methods.generateJWT = function() {
  const today = new Date();
  const expirationDate = new Date(today);
  expirationDate.setDate(today.getDate() + 60);

  return jwt.sign({
    email: this.email,
    id: this._id,
    exp: parseInt(expirationDate.getTime() / 1000, 10),
  }, 'secret');
}

UsersSchema.methods.toAuthJSON = function() {
  return {
    _id: this._id,
    email: this.email,
    token: this.generateJWT(),
  };
};

mongoose.model('Users', UsersSchema);
Users.js
1_updLloBs1oJyVGplMGG4lQ
Esta deve ser a estrutura de momento

Vamos adicionar nosso modelo recém-criado ao "app.js".

Adicione a linha seguinte ao "app.js" após configurar o Mongoose:

require('./models/Users');
1_YDxu9Xcr1SqDjQLzTVI9MA

Configuração do Passport

Crie uma pasta "config" com o arquivo "passport.js" dentro dela:

const mongoose = require('mongoose');
const passport = require('passport');
const LocalStrategy = require('passport-local');

const Users = mongoose.model('Users');

passport.use(new LocalStrategy({
  usernameField: 'user[email]',
  passwordField: 'user[password]',
}, (email, password, done) => {
  Users.findOne({ email })
    .then((user) => {
      if(!user || !user.validatePassword(password)) {
        return done(null, false, { errors: { 'email or password': 'is invalid' } });
      }

      return done(null, user);
    }).catch(done);
}));
passport.js

Nesse arquivo, usamos o método validatePassword que definimos no modelo de usuário. Com base no resultado, retornamos um resultado diferente a partir da LocalStrategy (estratégia local) do Passport.

1_TxTXHZEmeZoEff1TeW9hDA
A estrutura agora deve ser esta

Vamos conectar o "passport.js" ao arquivo "app.js". Adicione a linha a seguir abaixo de todos os models:

require('./config/passport');
1__Uem3m6YuPSnhx9DZsJ5sA
A linha do require do Passport deve estar abaixo de todos os modelos

Roas e opções de autenticação

Crie uma pasta chamada "routes" com o arquivo "auth.js" dentro dela.

Nesse arquivo, usaremos a função getTokenFromHeaders para obter um token do JWT que será enviado do lado do client nos cabeçalhos (headers) da solicitação. Também criaremos um objeto auth com as propriedades optional e required. Nós as usaremos mais tarde em nossas rotas.

const jwt = require('express-jwt');

const getTokenFromHeaders = (req) => {
  const { headers: { authorization } } = req;

  if(authorization && authorization.split(' ')[0] === 'Token') {
    return authorization.split(' ')[1];
  }
  return null;
};

const auth = {
  required: jwt({
    secret: 'secret',
    userProperty: 'payload',
    getToken: getTokenFromHeaders,
  }),
  optional: jwt({
    secret: 'secret',
    userProperty: 'payload',
    getToken: getTokenFromHeaders,
    credentialsRequired: false,
  }),
};

module.exports = auth;
auth.js

Na mesma pasta "routes", criamos um arquivo "index.js":

const express = require('express');
const router = express.Router();

router.use('/api', require('./api'));

module.exports = router;
index.js da pasta routes

Agora, precisamos de uma pasta "api" dentro da pasta "routes", com outro arquivo "index.js" dentro dela.

const express = require('express');
const router = express.Router();

router.use('/users', require('./users'));

module.exports = router;
index.js da pasta api
1_xT-bMD4RPNbS0trhqltHQQ
Esta deve ser a estrutura de momento

Agora, criaremos o arquivo "users.js" que solicitamos em "api/index.js".

Primeiro, criaremos uma rota de autorização opcional "/", que será usada para a criação de um modelo (registro).

router.post('/', auth.optional, (req, res, next) ...

Depois disso, criaremos outra rota de autorização opcional "/login". Ela será usada para ativar nossa configuração do Passport e validar uma senha recebida com e-mail.

router.post('/login', auth.optional, (req, res, next) ...

Por fim, criaremos uma rota de autorização obrigatória, que será usada para retornar o usuário que está logado atualmente. Somente usuários logados (usuários com o token enviado com sucesso por meio dos cabeçalhos de solicitação) têm acesso a essa rota.

router.get('/current', auth.required, (req, res, next) ...
const mongoose = require('mongoose');
const passport = require('passport');
const router = require('express').Router();
const auth = require('../auth');
const Users = mongoose.model('Users');

//POST new user route (optional, everyone has access)
router.post('/', auth.optional, (req, res, next) => {
  const { body: { user } } = req;

  if(!user.email) {
    return res.status(422).json({
      errors: {
        email: 'is required',
      },
    });
  }

  if(!user.password) {
    return res.status(422).json({
      errors: {
        password: 'is required',
      },
    });
  }

  const finalUser = new Users(user);

  finalUser.setPassword(user.password);

  return finalUser.save()
    .then(() => res.json({ user: finalUser.toAuthJSON() }));
});

//POST login route (optional, everyone has access)
router.post('/login', auth.optional, (req, res, next) => {
  const { body: { user } } = req;

  if(!user.email) {
    return res.status(422).json({
      errors: {
        email: 'is required',
      },
    });
  }

  if(!user.password) {
    return res.status(422).json({
      errors: {
        password: 'is required',
      },
    });
  }

  return passport.authenticate('local', { session: false }, (err, passportUser, info) => {
    if(err) {
      return next(err);
    }

    if(passportUser) {
      const user = passportUser;
      user.token = passportUser.generateJWT();

      return res.json({ user: user.toAuthJSON() });
    }

    return status(400).info;
  })(req, res, next);
});

//GET current route (required, only authenticated users have access)
router.get('/current', auth.required, (req, res, next) => {
  const { payload: { id } } = req;

  return Users.findById(id)
    .then((user) => {
      if(!user) {
        return res.sendStatus(400);
      }

      return res.json({ user: user.toAuthJSON() });
    });
});

module.exports = router;
users.js
1_FhlHO36q_NTY73Qhw2Vfiw
Esta deve ser a estrutura atual

Vamos adicionar nossa pasta "routes" ao "app.js". Adicione esta linha abaixo do require do Passport:

app.use(require('./routes'));
1_B44dNEd8f0Ii5GDkNJ0fLQ

Teste das rotas

Usaremos o Postman para enviar solicitações ao nosso servidor.

Nosso servidor aceita o body (corpo da solicitação) seguinte:

{
  "user": {
    "email": String,
    "password": String
  }
}

Criação de uma solicitação de POST para a criação de um usuário

body do teste:

1_e_U1SfVcGty_8XAZ8gWuSQ

Resposta:

{
    "user": {
        "_id": "5b0f38772c46910f16a058c5",
        "email": "erdeljac.antonio@gmail.com",
        "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImVyZGVsamFjLmFudG9uaW9AZ21haWwuY29tIiwiaWQiOiI1YjBmMzg3NzJjNDY5MTBmMTZhMDU4YzUiLCJleHAiOjE1MzI5MDgxNTEsImlhdCI6MTUyNzcyNDE1MX0.4TWc1TzY6zToHx_O1Dl2I9Hf9krFTqPkNLHI5U9rn8c"
    }
}

Agora, usaremos este token e o adicionaremos aos nossos "Headers" na configuração do Postman.

1_3TqFAWgy1bULj-ECJRyKKw

Vamos, agora, testar nossa rota acessível somente por autorização.

Criação de uma solicitação de GET para retornar o usuário logado atualmente

URL de solicitação:

GET http://localhost:8000/api/users/current

Resposta:

{
    "user": {
        "_id": "5b0f38772c46910f16a058c5",
        "email": "erdeljac.antonio@gmail.com",
        "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImVyZGVsamFjLmFudG9uaW9AZ21haWwuY29tIiwiaWQiOiI1YjBmMzg3NzJjNDY5MTBmMTZhMDU4YzUiLCJleHAiOjE1MzI5MDgzMTgsImlhdCI6MTUyNzcyNDMxOH0.5UnA2mpS-_puPwwxZEb4VxRGFHX6qJ_Fn3pytgGaJT0"
    }
}

Vamos tentar fazer o mesmo sem o token nos "Headers".

Resposta:

1_ggNxOl_DMzg6dklIJAKG3g

Conclusão

Agradeço a leitura deste tutorial. Se perceber erros, informe-os ao autor. Se tiver problemas em qualquer uma das etapas, consulte este repositório do GitHub.

Você também pode entrar em contato com o autor por: