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

204

Comienzo 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 para una aplicación móvil. La interfaz se desarrollará con AngularJS.

Durante algunos días leí mucho sobre cómo proteger las API REST, pero no llego a una solución final. Por lo que yo 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 pueden 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 datos, donde el usuario otorgó permisos)

Por el momento, pienso permitir que solo los usuarios con una sesión activa utilicen la API. Para autorizar a los usuarios, usaré el pasaporte y, para obtener el permiso, debo implementar algo por mí mismo. Todo en la cima de HTTPS.

¿Alguien puede proporcionar algunas mejores prácticas o experiencias? ¿Hay una falta en mi "arquitectura"?

tschiela
fuente
2
¿Supongo que la API solo se debe usar desde la interfaz que usted proporciona? En ese caso, usar la sesión para garantizar que el usuario sea válido parece una buena solución. Para obtener permisos, puede echar un vistazo a los roles de nodo .
robertklep
2
¿Qué hiciste finalmente por esto? ¿Algún código de placa de caldera (servidor / cliente de aplicación móvil) que pueda compartir?
Morteza Shahriari Nia

Respuestas:

176

He tenido el mismo problema que usted describe. Se puede acceder al sitio web que estoy construyendo desde un teléfono móvil y desde el navegador, por lo que necesito una API que permita a los usuarios registrarse, iniciar sesión y realizar algunas tareas específicas. Además, necesito admitir la escalabilidad, el mismo código que se ejecuta en diferentes procesos / máquinas.

Debido a que los usuarios pueden CREAR recursos (también conocidos como acciones POST / PUT), debe proteger su API. Puede usar oauth o puede crear su propia solución, pero tenga en cuenta que todas las soluciones pueden romperse si la contraseña es realmente fácil de descubrir. La idea básica es autenticar a los usuarios utilizando 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 cifrar usando pbkdf2

Luego, debe guardar la sesión en algún lugar. Si lo guarda en la memoria en un objeto plano, si mata el servidor y lo reinicia nuevamente, la sesión se destruirá. Además, esto no es escalable. Si usa haproxy para equilibrar la carga entre máquinas o si simplemente usa trabajadores, 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, deberá autenticarse nuevamente. Por lo tanto, debe 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) genera otro token para la sesión, también conocido como accesstoken. De nuevo, con nodo-uuid. Enviar al usuario el token de acceso y el ID de usuario. El ID de usuario (clave) y el token de acceso (valor) se almacenan en redis con el tiempo de caducidad, por ejemplo, 1 h.

Ahora, cada vez que el usuario realice cualquier operación utilizando la API de descanso, deberá enviar el ID de usuario y el token de acceso.

Si permite que los usuarios se registren utilizando la API de resto, 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 Se registran.

La web también usa esta api pero no es necesario que uses apitokens. Puede usar express con una tienda de redis o usar la misma técnica descrita anteriormente pero omitiendo el cheque de apitoken y devolviendo al usuario el userid + accesstoken en una cookie.

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

Resumen:

diagrama de secuencia

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

Gabriel Llamas
fuente
1
También uso mongodb pero es bastante fácil de administrar si guardas la sesión (accesstoken) usando redis (usa operaciones atómicas). El apitoken se genera en el servidor cuando el usuario crea una cuenta y la envía de vuelta al usuario. Luego, cuando el usuario quiera autenticarse, debe enviar un nombre de usuario + contraseña + apitoken (ponerlos en el cuerpo http). Tenga en cuenta que HTTP no cifra el cuerpo, por lo que la contraseña y el apitoken se pueden rastrear. Use HTTPS si esto le preocupa.
Gabriel Llamas
1
¿Cuál es el punto de usar un apitoken? ¿Es una contraseña "secundaria"?
Salvatorelab
2
@TheBronx El apitoken tiene 2 casos de uso: 1) con un apitoken puede controlar el acceso de los usuarios a su sistema y puede monitorear y construir estadísticas de cada usuario. 2) Es una medida de seguridad adicional, una contraseña "secundaria".
Gabriel Llamas
1
¿Por qué debería enviar la identificación de usuario una y otra vez después de una autenticación exitosa? El token debería ser el único secreto que necesita para realizar llamadas API.
Axel Napolitano
1
La idea del token, además de abusar de él para rastrear la actividad del usuario, es que un usuario idealmente no necesita ningún nombre de usuario y contraseña para usar una aplicación: el token es la clave de acceso única. Esto permite a los usuarios soltar cualquier clave en cualquier momento que afecte solo a la aplicación pero no a la cuenta de usuario. Para un servicio web, un token es bastante inestable, es por eso que un inicio de sesión inicial para una sesión es el lugar donde el usuario obtiene ese token, para un cliente "regular" ab, un token no es un problema: ingréselo una vez y ya casi ha terminado ;)
Axel Napolitano
22

Me gustaría contribuir con este código como una solución estructural para la pregunta planteada, de acuerdo (espero) con la respuesta aceptada. (Puedes personalizarlo 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" 
cibercitizen1
fuente
Gracias por esta muestra, es muy útil, sin embargo, trato de seguir esto, y cuando me conecto para iniciar sesión, digo esto: curl: (51) SSL: el nombre del sujeto del certificado 'xxxx' no coincide con el nombre de host de destino 'xxx.net'. He codificado mi / etc / hosts para permitir la conexión https en la misma máquina
mastervv
11

Acabo de terminar una aplicación de muestra que hace esto de una manera bastante básica, pero clara. Utiliza mangosta con mongodb para almacenar usuarios y pasaportes para la gestión de autenticación.

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

clangager
fuente
77
Estás utilizando cookies para asegurar la API. No creo que sea correcto.
Vince Yuan
9

Hay muchas preguntas sobre los patrones de autenticación REST aquí en SO. Estos son los más relevantes para su pregunta:

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

Zim
fuente
Leí mucho sobre oauth 1.0 y oauth 2.0 y ambas versiones no parecen muy seguras. Wikipedia escribió que hay algunas fugas de seguridad en oauth 1.0. También encontré un artículo que dice que uno de los desarrolladores principales dejó el equipo porque oauth 2.0 no es seguro.
tschiela
12
@tschiela Deberías agregar referencias a cualquier cosa que cites aquí.
mikemaccana
3

Si desea proteger su aplicación, entonces definitivamente debe comenzar usando HTTPS en lugar de HTTP , esto asegura una creación de un canal seguro entre usted y los usuarios que evitará rastrear los datos enviados de un lado a otro a los usuarios y ayudará a mantener los datos Intercambio confidencial.

Puede usar JWT (JSON Web Tokens) para asegurar API RESTful , esto tiene muchos beneficios en comparación con las sesiones del lado del servidor, los beneficios son principalmente:

1- Más escalable, ya que sus servidores API no tendrán que mantener sesiones para cada usuario (lo que puede ser una gran carga cuando tiene muchas sesiones)

2- Los JWT son independientes y tienen los reclamos que definen el rol del usuario, por ejemplo, y a qué puede acceder y emitido en la fecha y fecha de vencimiento (después de lo cual JWT no será válido)

3- Más fácil de manejar a través de equilibradores de carga y si tiene múltiples servidores API ya que no tendrá que compartir datos de sesión ni configurar el servidor para enrutar la sesión al mismo servidor, siempre que una solicitud con un JWT llegue a cualquier servidor, se puede autenticar y autorizado

4- Menos presión en su base de datos y no tendrá que almacenar y recuperar constantemente la identificación y los datos de la sesión para cada solicitud

5- Los JWT no pueden ser manipulados si usa una clave segura para firmar el JWT, por lo que puede confiar en los reclamos en el JWT que se envía con la solicitud sin tener que verificar la sesión del usuario y si está autorizado o no , solo puede verificar el JWT y luego estará listo para saber quién y qué puede hacer este usuario.

Muchas bibliotecas proporcionan formas fáciles de crear y validar JWT en la mayoría de los lenguajes de programación, por ejemplo: en node.js uno de los más populares es jsonwebtoken

Dado que las API REST generalmente tienen como objetivo mantener el servidor sin estado, los JWT son más compatibles con ese concepto ya que cada solicitud se envía con token de autorización que es autónomo (JWT) sin que el servidor tenga que realizar un seguimiento de la sesión del usuario en comparación con las sesiones que hacen que el servidor servidor con estado para que recuerde al usuario y su función, sin embargo, las sesiones también se usan ampliamente y tienen sus ventajas, que puede buscar si lo desea.

Una cosa importante a tener en cuenta es que debe entregar de forma segura el JWT al cliente utilizando HTTPS y guardarlo en un lugar seguro (por ejemplo, en el almacenamiento local).

Puede obtener más información sobre los JWT en este enlace.

Ahmed Elkoussy
fuente
1
Me gusta su respuesta que parece la mejor actualización de esta vieja pregunta. Me he hecho otra pregunta sobre el mismo tema y usted también podría ser útil. => stackoverflow.com/questions/58076644/…
pbonnefoi
Gracias, me alegro de poder ayudar, estoy publicando una respuesta a su pregunta
Ahmed Elkoussy
2

Si desea tener un área completamente bloqueada de su aplicación web a la que solo puedan acceder los administradores de su empresa, entonces la autorización SSL puede ser para usted. Asegurará que nadie pueda conectarse 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: Artículo

Esta es una de las configuraciones más seguras que encontrará, ya que no hay nombres de usuario / contraseñas involucrados para que nadie pueda acceder a menos que uno de sus usuarios entregue los archivos clave a un posible pirata informático.

ExxKA
fuente
Buen articulo. Pero el área privada es para usuarios.
tschiela
Gracias - bien, entonces deberías buscar otra solución, distribuir certificados sería una molestia.
ExxKA