Come implementare un'API REST sicura con node.js


204

Inizio a pianificare un'API REST con node.js, express e mongodb. L'API fornisce i dati per un sito Web (area pubblica e privata) e forse in seguito un'app mobile. Il frontend sarà sviluppato con AngularJS.

Per alcuni giorni ho letto molto sulla protezione delle API REST, ma non arrivo a una soluzione finale. Per quanto ho capito è usare HTTPS per fornire una sicurezza di base. Ma come posso proteggere l'API in quei casi d'uso:

  • Solo i visitatori / utenti del sito Web / dell'app possono ottenere dati per l'area pubblica del sito Web / dell'app

  • Solo gli utenti autenticati e autorizzati possono ottenere dati per l'area privata (e solo i dati, in cui l'utente ha concesso le autorizzazioni)

Al momento penso di consentire solo agli utenti con una sessione attiva di utilizzare l'API. Per autorizzare gli utenti userò il passaporto e per l'autorizzazione devo implementare qualcosa per me stesso. Tutto in cima a HTTPS.

Qualcuno può fornire alcune migliori pratiche o esperienze? C'è una mancanza nella mia "architettura"?


2
Suppongo che l'API debba essere utilizzata solo dal frontend che fornisci? In tal caso, utilizzare la sessione per assicurarsi che l'utente sia valido sembra una buona soluzione. Per le autorizzazioni, puoi dare un'occhiata ai ruoli dei nodi .
robertklep,

2
Cosa hai fatto finalmente per questo? Qualche codice della piastra della caldaia (server / client dell'app mobile) che puoi condividere?
Morteza Shahriari Nia,

Risposte:


176

Ho avuto lo stesso problema che descrivi. Il sito web che sto costruendo è accessibile da un telefono cellulare e dal browser, quindi ho bisogno di un API per consentire agli utenti di registrarsi, accedere e svolgere alcune attività specifiche. Inoltre, ho bisogno di supportare la scalabilità, lo stesso codice in esecuzione su diversi processi / macchine.

Poiché gli utenti possono CREARE risorse (ovvero azioni POST / PUT), è necessario proteggere le API. Puoi usare oauth o puoi creare la tua soluzione, ma tieni presente che tutte le soluzioni possono essere rotte se la password è davvero facile da scoprire. L'idea di base è autenticare gli utenti usando username, password e un token, ovvero l'apitoken. Questo apitoken può essere generato usando node-uuid e la password può essere hash usando pbkdf2

Quindi, è necessario salvare la sessione da qualche parte. Se lo salvi in ​​memoria in un oggetto semplice, se uccidi il server e lo riavvii di nuovo, la sessione verrà distrutta. Inoltre, questo non è scalabile. Se si utilizza haproxy per il bilanciamento del carico tra macchine o se si utilizzano semplicemente i lavoratori, questo stato della sessione verrà memorizzato in un singolo processo, quindi se lo stesso utente viene reindirizzato a un altro processo / macchina, sarà necessario eseguire nuovamente l'autenticazione. Pertanto è necessario archiviare la sessione in un luogo comune. Questo in genere viene fatto usando redis.

Quando l'utente viene autenticato (nome utente + password + apitoken) genera un altro token per la sessione, noto anche come accesstoken. Ancora una volta, con node-uuid. Invia all'utente accesstoken e userid. Userid (chiave) e accesstoken (valore) vengono memorizzati in redis con e scadono il tempo, ad es. 1h.

Ora, ogni volta che l'utente esegue qualsiasi operazione utilizzando l'API rimanente, dovrà inviare userid e accesstoken.

Se consenti agli utenti di registrarsi utilizzando l'API rimanente, dovrai creare un account amministratore con un amministratore apitoken e memorizzarli nell'app mobile (crittografare nome utente + password + apitoken) perché i nuovi utenti non avranno un apitoken quando si iscrivono.

Anche il web usa questa API ma non è necessario usare apitokens. Puoi usare express con un redis store o usare la stessa tecnica descritta sopra ma aggirando il controllo apitoken e restituendo all'utente userid + accesstoken in un cookie.

Se si dispone di aree private, confrontare il nome utente con gli utenti consentiti durante l'autenticazione. È inoltre possibile applicare ruoli agli utenti.

Sommario:

diagramma di sequenza

Un'alternativa senza apitoken sarebbe quella di utilizzare HTTPS e di inviare il nome utente e la password nell'intestazione dell'autorizzazione e memorizzare nella cache il nome utente in redis.


1
Uso anche mongodb ma è abbastanza facile da gestire se salvi la sessione (accesstoken) usando redis (usa le operazioni atomiche). L'apitoken viene generato nel server quando l'utente crea un account e lo rispedisce all'utente. Quindi, quando l'utente desidera autenticarsi, deve inviare nome utente + password + apitoken (inserendoli nel corpo http). Tieni presente che HTTP non crittografa il corpo in modo da poter annusare la password e l'apitoken. Usa HTTPS se questo è un problema per te.
Gabriel Llamas,

1
che senso ha usare un apitoken? è una password "secondaria"?
Salvatorelab,

2
@TheBronx L'apitoken ha 2 casi d'uso: 1) con un apitoken puoi controllare l'accesso degli utenti al tuo sistema e puoi monitorare e costruire statistiche di ciascun utente. 2) È un'ulteriore misura di sicurezza, una password "secondaria".
Gabriel Llamas,

1
Perché dovresti inviare l'ID utente più volte dopo l'autenticazione corretta. Il token dovrebbe essere l'unico segreto necessario per eseguire chiamate API.
Axel Napolitano,

1
L'idea del token - oltre ad abusarne per il monitoraggio dell'attività dell'utente - è che un utente idealmente non ha bisogno di nome utente e password per utilizzare un'applicazione: il token è la chiave di accesso univoca. Ciò consente agli utenti di rilasciare qualsiasi tasto in qualsiasi momento interessando solo l'app ma non l'account utente. Per un servizio web un token è abbastanza invariato - ecco perché un login iniziale per una sessione è il luogo in cui l'utente ottiene quel token - per un client "normale" ab, un token non è un problema: inseriscilo una volta e hai quasi fatto ;)
Axel Napolitano,

22

Vorrei contribuire con questo codice come soluzione strutturale alla domanda posta, (lo spero) secondo la risposta accettata. (Puoi personalizzarlo molto facilmente).

// ------------------------------------------------------
// 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);

Questo server può essere testato con arricciatura:

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" 

Grazie per questo esempio è molto utile, tuttavia provo a seguire questo, e quando mi collego per accedervi dicendo questo: curl: (51) SSL: il nome del soggetto certificato 'xxxx' non corrisponde al nome host di destinazione 'xxx.net'. Ho codificato il mio / etc / hosts per consentire la connessione https sulla stessa macchina
mastervv


9

Ci sono molte domande sui modelli di autenticazione REST qui su SO. Questi sono i più rilevanti per la tua domanda:

Fondamentalmente è necessario scegliere tra l'uso di chiavi API (meno sicure in quanto la chiave può essere scoperta da un utente non autorizzato), una chiave per app e una combinazione di token (media) o un'implementazione OAuth completa (la più sicura).


Ho letto molto su oauth 1.0 e oauth 2.0 ed entrambe le versioni non sembrano molto sicure. Wikipedia ha scritto che ci sono alcune falle nella sicurezza di oauth 1.0. Inoltre ho trovato un articolo che circa uno degli sviluppatori principali lascia il team perché oauth 2.0 non è sicuro.
Tschiela,

12
@tschiela Dovresti aggiungere riferimenti a tutto ciò che citi qui.
mikemaccana,

3

Se vuoi proteggere la tua applicazione, allora dovresti assolutamente iniziare a usare HTTPS anziché HTTP , questo assicura un canale sicuro tra te e gli utenti che impedirà di annusare i dati inviati avanti e indietro agli utenti e aiuterà a conservare i dati scambiato confidenziale.

È possibile utilizzare JWT (token Web JSON) per proteggere le API RESTful , questo ha molti vantaggi rispetto alle sessioni lato server, i vantaggi sono principalmente:

1- Più scalabile, poiché i server API non dovranno mantenere sessioni per ciascun utente (il che può essere un grosso fardello quando si hanno molte sessioni)

2- I JWT sono autonomi e hanno i reclami che definiscono ad esempio il ruolo dell'utente e ciò a cui può accedere e rilasciare alla data e alla data di scadenza (dopo la quale JWT non sarà valido)

3- Più facile da gestire attraverso i servizi di bilanciamento del carico e se si dispone di più server API in quanto non è necessario condividere i dati della sessione né configurare il server per indirizzare la sessione allo stesso server, ogni volta che una richiesta con un JWT raggiunge qualsiasi server può essere autenticata e autorizzato

4- Meno pressione sul DB e non sarà necessario archiviare e recuperare costantemente ID sessione e dati per ogni richiesta

5- I JWT non possono essere manomessi se si utilizza una chiave avanzata per firmare il JWT, quindi è possibile fidarsi dei reclami nel JWT inviati con la richiesta senza dover controllare la sessione dell'utente e se è autorizzato o meno , puoi semplicemente controllare JWT e sei pronto a sapere chi e cosa può fare questo utente.

Molte librerie offrono semplici modi per creare e validare JWT nella maggior parte dei linguaggi di programmazione, ad esempio: in node.js uno dei più popolari è jsonwebtoken

Poiché le API REST generalmente mirano a mantenere il server senza stato, quindi i JWT sono più compatibili con quel concetto in quanto ogni richiesta viene inviata con token di autorizzazione autonomo (JWT) senza che il server debba tenere traccia della sessione dell'utente rispetto alle sessioni che rendono il server stateful in modo che ricordi l'utente e il suo ruolo, tuttavia, le sessioni sono anche ampiamente utilizzate e hanno i loro pro, che puoi cercare se vuoi.

Una cosa importante da notare è che devi consegnare in sicurezza il JWT al client usando HTTPS e salvarlo in un luogo sicuro (ad esempio nella memoria locale).

Puoi saperne di più sui JWT da questo link


1
Mi piace la tua risposta che sembra il migliore aggiornamento da questa vecchia domanda. Mi sono posto un'altra domanda sullo stesso argomento e potresti anche essere utile. => Stackoverflow.com/questions/58076644/...
pbonnefoi

Grazie, felice di poterti aiutare, sto pubblicando una risposta per la tua domanda
Ahmed Elkoussy,

2

Se vuoi avere un'area completamente bloccata della tua applicazione web alla quale possono accedere solo gli amministratori della tua azienda, allora forse l'autorizzazione SSL fa per te. Assicurerà che nessuno possa stabilire una connessione all'istanza del server a meno che non abbia un certificato autorizzato installato nel proprio browser. La scorsa settimana ho scritto un articolo su come configurare il server: l' articolo

Questa è una delle configurazioni più sicure che troverai in quanto non sono coinvolti nome utente / password, quindi nessuno può accedere a meno che uno dei tuoi utenti non consegni i file chiave a un potenziale hacker.


bell'articolo. Ma l'area privata è riservata agli utenti.
Tschiela,

Grazie - giusto, allora dovresti cercare un'altra soluzione, distribuire certificati sarebbe una seccatura.
ExxKA
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.