Como implementar uma API REST segura com node.js


204

Começo a planejar uma API REST com node.js, express e mongodb. A API fornece dados para um site (área pública e privada) e talvez mais tarde para um aplicativo móvel. O frontend será desenvolvido com o AngularJS.

Por alguns dias, li muito sobre a proteção de APIs REST, mas não chego a uma solução final. Tanto quanto eu entendo é usar HTTPS para fornecer uma segurança básica. Mas como posso proteger a API nesses casos de uso:

  • Somente visitantes / usuários do site / aplicativo podem obter dados para a área pública do site / aplicativo

  • Somente usuários autenticados e autorizados têm permissão para obter dados para área privada (e somente dados, onde o usuário concedeu permissões)

No momento, penso em permitir apenas que usuários com uma sessão ativa usem a API. Para autorizar os usuários, usarei o passaporte e, para obter permissão, preciso implementar algo para mim. Tudo no topo do HTTPS.

Alguém pode fornecer as melhores práticas ou experiências? Existe uma falta na minha "arquitetura"?


2
Acho que a API deve ser usada apenas no frontend que você fornece? Nesse caso, usar a sessão para garantir a validade do usuário parece uma boa solução. Para obter permissões, você pode dar uma olhada nas funções do nó .
robertklep

2
O que você finalmente fez por isso? Qualquer código da placa da caldeira (servidor / cliente de aplicativo móvel) que você pode compartilhar?
Morteza Shahriari Nia

Respostas:


175

Eu tive o mesmo problema que você descreve. O site que estou construindo pode ser acessado a partir de um telefone celular e do navegador, por isso preciso de uma API para permitir que os usuários se inscrevam, façam login e realizem algumas tarefas específicas. Além disso, preciso oferecer suporte à escalabilidade, o mesmo código em execução em diferentes processos / máquinas.

Como os usuários podem criar recursos (também conhecidos como ações POST / PUT), é necessário proteger sua API. Você pode usar oauth ou criar sua própria solução, mas lembre-se de que todas as soluções podem ser quebradas se a senha for realmente fácil de descobrir. A idéia básica é autenticar usuários usando o nome de usuário, senha e um token, também conhecido como apitoken. Esse apitoken pode ser gerado usando o node-uuid e a senha pode ser hash usando o pbkdf2

Então, você precisa salvar a sessão em algum lugar. Se você salvá-lo na memória em um objeto comum, se você matar o servidor e reiniciá-lo novamente, a sessão será destruída. Além disso, isso não é escalável. Se você usar haproxy para carregar o equilíbrio entre máquinas ou simplesmente usar trabalhadores, esse estado da sessão será armazenado em um único processo; portanto, se o mesmo usuário for redirecionado para outro processo / máquina, será necessário autenticar novamente. Portanto, você precisa armazenar a sessão em um local comum. Isso geralmente é feito usando redis.

Quando o usuário é autenticado (nome de usuário + senha + apitoken), gera outro token para a sessão, também conhecido como accesstoken. Novamente, com node-uuid. Envie ao usuário o accesstoken e o userid. O ID do usuário (chave) e o acesso (valor) são armazenados em redis com tempo de expiração, por exemplo, 1h.

Agora, toda vez que o usuário fizer alguma operação usando a API restante, será necessário enviar o ID do usuário e o token de acesso.

Se você permitir que os usuários se inscrevam usando a API restante, será necessário criar uma conta de administrador com uma apitoken de administrador e armazená-los no aplicativo móvel (criptografar nome de usuário + senha + apitoken) porque novos usuários não terão uma apitoken quando eles se inscrevem.

A web também usa essa API, mas você não precisa usar apitokens. Você pode usar express com uma loja redis ou usar a mesma técnica descrita acima, mas ignorando a verificação de apitoken e retornando ao usuário o ID do usuário + accesstoken em um cookie.

Se você tiver áreas privadas, compare o nome de usuário com os usuários permitidos quando eles se autenticarem. Você também pode aplicar funções aos usuários.

Resumo:

diagrama de sequência

Uma alternativa sem apitoken seria usar HTTPS e enviar o nome de usuário e a senha no cabeçalho da Autorização e armazenar em cache o nome de usuário em redis.


1
Eu também uso o mongodb, mas é muito fácil de gerenciar se você salvar a sessão (accesstoken) usando redis (use operações atômicas). O apitoken é gerado no servidor quando o usuário cria uma conta e a envia de volta ao usuário. Em seguida, quando o usuário desejar se autenticar, deverá enviar o nome de usuário + senha + apitoken (coloque-os no corpo http). Lembre-se de que o HTTP não criptografa o corpo para que a senha e o apitoken sejam detectados. Use HTTPS se isso for um problema para você.
Gabriel Llamas

1
para que usar um apitoken? é uma senha "secundária"?
Salvatorelab

2
@TheBronx O apitoken possui 2 casos de uso: 1) com um apitoken, você pode controlar o acesso dos usuários ao seu sistema e monitorar e criar estatísticas de cada usuário. 2) É uma medida de segurança adicional, uma senha "secundária".
Gabriel Llamas

1
Por que você deve enviar o ID do usuário repetidamente após a autenticação bem-sucedida. O token deve ser o único segredo necessário para executar chamadas de API.
Axel Napolitano

1
A idéia do token - além de abusar dele para rastrear a atividade do usuário - é que, idealmente, um usuário não precisa de nenhum nome de usuário e senha para usar um aplicativo: O token é a chave de acesso exclusiva. Isso permite que os usuários largem qualquer chave a qualquer momento, afetando apenas o aplicativo, mas não a conta do usuário. Para um serviço da Web, um token não é fácil - é por isso que um login inicial de uma sessão é o local onde o usuário obtém esse token - para um cliente "regular" ab, um token não é problema: insira-o uma vez e você está quase pronto ;)
Axel Napolitano

22

Eu gostaria de contribuir com este código como uma solução estrutural para a questão colocada, de acordo (espero que sim) com a resposta aceita. (Você pode personalizá-lo com muita facilidade).

// ------------------------------------------------------
// server.js 

// .......................................................
// requires
var fs = require('fs');
var express = require('express'); 
var myBusinessLogic = require('../businessLogic/businessLogic.js');

// .......................................................
// security options

/*
1. Generate a self-signed certificate-key pair
openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -keyout key.pem -out certificate.pem

2. Import them to a keystore (some programs use a keystore)
keytool -importcert -file certificate.pem -keystore my.keystore
*/

var securityOptions = {
    key: fs.readFileSync('key.pem'),
    cert: fs.readFileSync('certificate.pem'),
    requestCert: true
};

// .......................................................
// create the secure server (HTTPS)

var app = express();
var secureServer = require('https').createServer(securityOptions, app);

// ------------------------------------------------------
// helper functions for auth

// .............................................
// true if req == GET /login 

function isGETLogin (req) {
    if (req.path != "/login") { return false; }
    if ( req.method != "GET" ) { return false; }
    return true;
} // ()

// .............................................
// your auth policy  here:
// true if req does have permissions
// (you may check here permissions and roles 
//  allowed to access the REST action depending
//  on the URI being accessed)

function reqHasPermission (req) {
    // decode req.accessToken, extract 
    // supposed fields there: userId:roleId:expiryTime
    // and check them

    // for the moment we do a very rigorous check
    if (req.headers.accessToken != "you-are-welcome") {
        return false;
    }
    return true;
} // ()

// ------------------------------------------------------
// install a function to transparently perform the auth check
// of incoming request, BEFORE they are actually invoked

app.use (function(req, res, next) {
    if (! isGETLogin (req) ) {
        if (! reqHasPermission (req) ){
            res.writeHead(401);  // unauthorized
            res.end();
            return; // don't call next()
        }
    } else {
        console.log (" * is a login request ");
    }
    next(); // continue processing the request
});

// ------------------------------------------------------
// copy everything in the req body to req.body

app.use (function(req, res, next) {
    var data='';
    req.setEncoding('utf8');
    req.on('data', function(chunk) { 
       data += chunk;
    });
    req.on('end', function() {
        req.body = data;
        next(); 
    });
});

// ------------------------------------------------------
// REST requests
// ------------------------------------------------------

// .......................................................
// authenticating method
// GET /login?user=xxx&password=yyy

app.get('/login', function(req, res){
    var user = req.query.user;
    var password = req.query.password;

    // rigorous auth check of user-passwrod
    if (user != "foobar" || password != "1234") {
        res.writeHead(403);  // forbidden
    } else {
        // OK: create an access token with fields user, role and expiry time, hash it
        // and put it on a response header field
        res.setHeader ('accessToken', "you-are-welcome");
        res.writeHead(200); 
    }
    res.end();
});

// .......................................................
// "regular" methods (just an example)
// newBook()
// PUT /book

app.put('/book', function (req,res){
    var bookData = JSON.parse (req.body);

    myBusinessLogic.newBook(bookData, function (err) {
        if (err) {
            res.writeHead(409);
            res.end();
            return;
        }
        // no error:
        res.writeHead(200);
        res.end();
    });
});

// .......................................................
// "main()"

secureServer.listen (8081);

Este servidor pode ser testado com curl:

echo "----   first: do login "
curl -v "https://localhost:8081/login?user=foobar&password=1234" --cacert certificate.pem

# now, in a real case, you should copy the accessToken received before, in the following request

echo "----  new book"
curl -X POST  -d '{"id": "12341324", "author": "Herman Melville", "title": "Moby-Dick"}' "https://localhost:8081/book" --cacert certificate.pem --header "accessToken: you-are-welcome" 

Obrigado por este exemplo, é muito útil, no entanto, tento seguir isso e, quando me conecto para fazer o login, digo o seguinte: curl: (51) SSL: o nome do assunto do certificado 'xxxx' não corresponde ao nome do host de destino 'xxx.net'. Eu codificado meus / etc / hosts para permitir https conexão na mesma máquina
mastervv


9

Há muitas perguntas sobre os padrões de autenticação REST aqui no SO. Estes são os mais relevantes para sua pergunta:

Basicamente, você precisa escolher entre usar chaves de API (menos seguras, pois a chave pode ser descoberta por um usuário não autorizado), uma combinação de chave e token de aplicativo (média) ou uma implementação completa do OAuth (mais segura).


Eu li muito sobre o outh 1.0 e oauth 2.0 e ambas as versões não parecem muito seguras. A Wikipedia escreveu que existem alguns vazamentos de segurança no oauth 1.0. Também encontrei um artigo que cerca de um dos desenvolvedores principais deixa a equipe porque oauth 2.0 é inseguro.
Tschiela # 19/13

12
@tschiela Você deve adicionar referências a qualquer coisa que você citar aqui.
Mikemaccana

3

Se você deseja proteger seu aplicativo, definitivamente deve começar usando HTTPS em vez de HTTP , isso garante um canal seguro de criação entre você e os usuários, que evitará cheirar os dados enviados aos usuários e ajudará a mantê-los trocados confidenciais.

Você pode usar JWTs (JSON Web Tokens) para proteger APIs RESTful ; isso tem muitos benefícios quando comparado às sessões do servidor, os benefícios são principalmente:

1- Mais escalável, pois seus servidores de API não precisarão manter sessões para cada usuário (o que pode ser um grande fardo quando você tiver muitas sessões)

2- Os JWTs são independentes e possuem as reivindicações que definem a função do usuário, por exemplo, o que ele pode acessar e emitido na data e data de validade (após o qual o JWT não será válido)

3- Mais fácil de lidar com balanceadores de carga e se você tiver vários servidores de API, não precisará compartilhar dados da sessão nem configurar o servidor para rotear a sessão para o mesmo servidor, sempre que uma solicitação com um JWT atingir qualquer servidor, ela pode ser autenticada e autorizado

4- Menos pressão no seu banco de dados, assim como você não precisará armazenar e recuperar constantemente o ID e os dados da sessão para cada solicitação

5- Os JWTs não podem ser adulterados se você usar uma chave forte para assinar o JWT, para que possa confiar nas reivindicações no JWT enviadas com a solicitação sem precisar verificar a sessão do usuário e se ele está autorizado ou não , basta verificar o JWT e pronto para saber quem e o que esse usuário pode fazer.

Muitas bibliotecas fornecem maneiras fáceis de criar e validar JWTs na maioria das linguagens de programação, por exemplo: no node.js, uma das mais populares é jsonwebtoken

Como as APIs REST geralmente têm como objetivo manter o servidor sem estado, as JWTs são mais compatíveis com esse conceito, pois cada solicitação é enviada com o token de autorização independente (JWT), sem que o servidor tenha que acompanhar a sessão do usuário em comparação com as sessões que tornam o com estado do servidor, para que ele se lembre do usuário e de sua função; no entanto, as sessões também são amplamente usadas e têm seus profissionais, que você pode procurar se quiser.

Uma coisa importante a ser observada é que você deve entregar com segurança o JWT ao cliente usando HTTPS e salvá-lo em um local seguro (por exemplo, no armazenamento local).

Você pode aprender mais sobre JWTs neste link


1
Eu gosto da sua resposta que parece a melhor atualização desta pergunta antiga. Fiz outra pergunta sobre o mesmo tópico e você pode ser útil também. => stackoverflow.com/questions/58076644/…
pbonnefoi

Obrigado, feliz que pude ajudar, eu estou postando uma resposta para a sua pergunta
Ahmed Elkoussy

2

Se você deseja ter uma área completamente bloqueada do seu aplicativo da web que só pode ser acessada pelos administradores da sua empresa, a autorização SSL talvez seja para você. Ele garantirá que ninguém possa fazer uma conexão com a instância do servidor, a menos que tenha um certificado autorizado instalado em seu navegador. Na semana passada, escrevi um artigo sobre como configurar o servidor: Artigo

Essa é uma das configurações mais seguras que você encontrará, pois não há nome de usuário / senha envolvidos, para que ninguém possa obter acesso, a menos que um de seus usuários entregue os arquivos principais a um hacker em potencial.


bom artigo. Mas a área privada é para usuários.
Tschiela 19/03/2013

Obrigado - certo, então você deve procurar outra solução, a distribuição de certificados seria uma dor.
ExxKA 19/03/2019
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.