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"?

tschiela
fonte
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.

Gabriel Llamas
fonte
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" 
cibercitizen1
fonte
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
11

Acabei de terminar um aplicativo de amostra que faz isso de uma maneira bastante básica, mas clara. Ele usa o mangusto com o mongodb para armazenar usuários e passaporte para gerenciamento de autenticação.

https://github.com/Khelldar/Angular-Express-Train-Seed

clangager
fonte
7
Você está usando cookies para proteger a API. Eu não acho que está correto.
Vince Yuan
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).

Zim
fonte
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

Ahmed Elkoussy
fonte
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.

ExxKA
fonte
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