Cómo implementar una API REST segura con node.js


Empiezo a planificar una API REST con node.js, express y mongodb. La API proporciona datos para un sitio web (área pública y privada) y tal vez más tarde una aplicación móvil. El frontend se desarrollará con AngularJS.

Durante algunos días he leído mucho sobre la seguridad de las API REST, pero no llego a una solución final. Por lo que entiendo es usar HTTPS para proporcionar una seguridad básica. Pero cómo puedo proteger la API en esos casos de uso:

  • Solo los visitantes / usuarios del sitio web/aplicación son se permite obtener datos para el área pública del sitio web/aplicación

  • Solo los usuarios autenticados y autorizados pueden obtener datos para el área privada (y solo los datos, donde el usuario otorgó permisos)

Por el momento pienso en permitir que solo los usuarios con una sesión activa utilicen la API. Para autorizar a los usuarios voy a utilizar passport y para el permiso necesito implementar algo por mí mismo. Todo en la parte superior de HTTPS.

¿Puede alguien proporcionar algunos mejores prácticas o experiencias? ¿Hay una falta en mi "arquitectura"?

Author: laggingreflex, 2013-03-19

5 answers

He tenido el mismo problema que usted describe. El sitio web que estoy construyendo se puede acceder desde un teléfono móvil y desde el navegador, por lo que necesito una api para permitir a los usuarios registrarse, iniciar sesión y hacer algunas tareas específicas. Además, necesito soportar la escalabilidad, el mismo código ejecutándose en diferentes procesos / máquinas.

Debido a que los usuarios pueden CREAR recursos (también conocidas como acciones POST/PUT), necesita proteger su api. Puede usar oauth o puede construir su propia solución, pero tenga en cuenta que todas las soluciones se puede romper si la contraseña es realmente fácil de descubrir. La idea básica es autenticar a los usuarios usando el nombre de usuario, la contraseña y un token, también conocido como apitoken. Este apitoken se puede generar usando node-uuid y la contraseña se puede hash usando pbkdf2

Luego, debe guardar la sesión en algún lugar. Si lo guarda en memoria en un objeto plano, si mata el servidor y lo reinicia de nuevo, la sesión se destruirá. Además, esto no es escalable. Si usa haproxy para equilibrio de carga entre máquinas o si simplemente usa workers, este estado de sesión se almacenará en un solo proceso, por lo que si el mismo usuario es redirigido a otro proceso/máquina, tendrá que autenticarse nuevamente. Por lo tanto, es necesario almacenar la sesión en un lugar común. Esto se hace típicamente usando redis.

Cuando el usuario se autentica (nombre de usuario+contraseña+apitoken) generar otro token para la sesión, también conocido como accesstoken. De nuevo, con node-uuid. Enviar al usuario el accesstoken y el userid. El id de usuario (clave) y el accesstoken (valor) se almacenan en redis con tiempo de caducidad, por ejemplo, 1h.

Ahora, cada vez que el usuario haga cualquier operación usando la api rest necesitará enviar el userid y el accesstoken.

Si permite que los usuarios se registren utilizando la api rest, deberá crear una cuenta de administrador con un apitoken de administrador y almacenarlos en la aplicación móvil (cifrar nombre de usuario+contraseña+apitoken) porque los nuevos usuarios no tendrán un apitoken cuando firmen hasta.

La web también utiliza esta api, pero no es necesario utilizar apitokens. Puede utilizar express con una tienda redis o utilizar la misma técnica descrita anteriormente pero sin pasar por la comprobación de apitoken y devolviendo al usuario el id de usuario + accesstoken en una cookie.

Si tiene áreas privadas, compare el nombre de usuario con los usuarios permitidos cuando se autentican. También puede aplicar roles a los usuarios.

Resumen:

diagrama de secuencia

Una alternativa sin apitoken sería usar HTTPS y para enviar el nombre de usuario y contraseña en el encabezado de autorización y almacenar en caché el nombre de usuario en redis.

 170
Author: Gabriel Llamas,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-04-25 13:43:16

Me gustaría aportar este código como una solución estructural a la pregunta planteada, de acuerdo (espero que así sea) a la respuesta aceptada. (Se puede personalizar muy fácilmente).

// ------------------------------------------------------
// 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 se puede probar con 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" 
 19
Author: cibercitizen1,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-05-28 10:50:46

Acabo de terminar una aplicación de ejemplo que hace esto de una manera bastante básica, pero clara. Utiliza mongoose con mongodb para almacenar usuarios y passport para la administración de auth.

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

 12
Author: clangager,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-04-02 04:25:10

Hay muchas preguntas sobre los patrones de auth de DESCANSO aquí en SO. Estos son los más relevantes para su pregunta:

Básicamente, debe elegir entre usar claves API (menos seguras, ya que la clave puede ser descubierta por un usuario no autorizado), un combo de clave y token de aplicación (medio) o una implementación completa de OAuth (más segura).

 8
Author: Zim,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:10:38

Si desea tener un área completamente bloqueada de su aplicación web que solo puede ser accedida por los administradores de su empresa, entonces la autorización SSL tal vez para usted. Se asegurará de que nadie pueda hacer una conexión a la instancia del servidor a menos que tenga un certificado autorizado instalado en su navegador. La semana pasada escribí un artículo sobre cómo configurar el servidor: Article

Esta es una de las configuraciones más seguras que encontrará, ya que no hay nombre de usuario / contraseñas involucrado para que nadie pueda acceder a menos que uno de sus usuarios entregue los archivos clave a un potencial hacker.

 2
Author: ExxKA,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-03-19 12:57:01