Jak zaimplementować bezpieczny interfejs API REST za pomocą node.js


204

Zaczynam planować interfejs API REST za pomocą node.js, express i mongodb. Interfejs API zapewnia dane dla strony internetowej (obszar publiczny i prywatny), a może później aplikacji mobilnej. Frontend zostanie opracowany w AngularJS.

Przez kilka dni dużo czytałem o zabezpieczaniu interfejsów API REST, ale nie docieram do ostatecznego rozwiązania. O ile rozumiem, to używanie HTTPS w celu zapewnienia podstawowego bezpieczeństwa. Ale w jaki sposób mogę chronić interfejs API w takich przypadkach użycia:

  • Tylko odwiedzający / użytkownicy strony / aplikacji mogą uzyskiwać dane dla publicznego obszaru strony / aplikacji

  • Tylko uwierzytelnieni i autoryzowani użytkownicy mogą uzyskiwać dane dla obszaru prywatnego (i tylko te dane, w których użytkownik udzielił uprawnień)

W tej chwili myślę o zezwoleniu na używanie interfejsu API tylko użytkownikom z aktywną sesją. Aby autoryzować użytkowników, użyję paszportu i aby uzyskać zgodę, muszę coś dla siebie zaimplementować. Wszystko na szczycie HTTPS.

Czy ktoś może zapewnić najlepsze praktyki lub doświadczenia? Czy brakuje mojej „architektury”?


2
Zgaduję, że interfejsu API można używać tylko z interfejsu użytkownika, który udostępniasz? W takim przypadku użycie sesji do upewnienia się, że użytkownik jest poprawny, wydaje się dobrym rozwiązaniem. Aby uzyskać uprawnienia, możesz rzucić okiem na role węzłów .
robertklep

2
Co w końcu zrobiłeś dla tego? Dowolny kod płyty kotła (serwer / klient aplikacji mobilnej), którą możesz udostępnić?
Morteza Shahriari Nia,

Odpowiedzi:


175

Miałem ten sam problem, który opisujesz. Dostęp do strony internetowej, którą tworzę, można uzyskać z telefonu komórkowego i przeglądarki, więc potrzebuję interfejsu API, aby umożliwić użytkownikom rejestrację, logowanie i wykonywanie określonych zadań. Ponadto muszę wspierać skalowalność, ten sam kod działający na różnych procesach / maszynach.

Ponieważ użytkownicy mogą TWORZYĆ zasoby (czyli akcje POST / PUT), musisz zabezpieczyć interfejs API. Możesz użyć oauth lub zbudować własne rozwiązanie, ale pamiętaj, że wszystkie rozwiązania mogą zostać zepsute, jeśli hasło naprawdę łatwo jest znaleźć. Podstawową ideą jest uwierzytelnianie użytkowników przy użyciu nazwy użytkownika, hasła i tokena, zwanego także apitokenem. Ten apitoken można wygenerować za pomocą UUID węzła, a hasło można zaszyfrować za pomocą pbkdf2

Następnie musisz gdzieś zapisać sesję. Jeśli zapiszesz go w pamięci jako zwykły obiekt, jeśli zabijesz serwer i uruchomisz go ponownie, sesja zostanie zniszczona. Ponadto nie jest to skalowalne. Jeśli używasz haproxy do ładowania równowagi między maszynami lub po prostu używasz pracowników, ten stan sesji zostanie zapisany w jednym procesie, więc jeśli ten sam użytkownik zostanie przekierowany do innego procesu / maszyny, będzie musiał ponownie się uwierzytelnić. Dlatego musisz przechowywać sesję we wspólnym miejscu. Zazwyczaj odbywa się to za pomocą redis.

Gdy użytkownik zostanie uwierzytelniony (nazwa użytkownika + hasło + apitoken), wygeneruj kolejny token dla sesji, czyli accesstoken. Znowu z uuid węzła. Wyślij do użytkownika klucz dostępu i identyfikator użytkownika. Identyfikator użytkownika (klucz) i accesstoken (wartość) są przechowywane w pamięci podręcznej z czasem wygaśnięcia, np. 1h.

Teraz za każdym razem, gdy użytkownik wykona dowolną operację przy użyciu interfejsu API reszty, będzie musiał wysłać identyfikator użytkownika i klucz dostępu.

Jeśli zezwolisz użytkownikom na rejestrację przy użyciu pozostałego interfejsu API, musisz utworzyć konto administratora z administracyjnym apitokenem i przechowywać je w aplikacji mobilnej (szyfruj nazwę użytkownika + hasło + apitoken), ponieważ nowi użytkownicy nie będą mieli apitokenu, gdy rejestrują się.

Sieć korzysta również z tego interfejsu API, ale nie trzeba używać apitokens. Możesz użyć express w sklepie redis lub skorzystać z tej samej techniki opisanej powyżej, ale pomijając sprawdzanie apitoken i zwracając użytkownikowi identyfikator użytkownika + dostęp do pliku cookie.

Jeśli masz obszary prywatne, porównaj nazwę użytkownika z dozwolonymi użytkownikami podczas ich uwierzytelniania. Możesz także zastosować role do użytkowników.

Podsumowanie:

diagram sekwencyjny

Alternatywą bez apitokenu byłoby użycie HTTPS i wysłanie nazwy użytkownika i hasła w nagłówku autoryzacji oraz buforowanie nazwy użytkownika w trybie redis.


1
Używam również mongodb, ale zarządzanie nim jest dość łatwe, jeśli zapiszesz sesję (accesstoken) za pomocą redis (użyj operacji atomowych). Apitoken jest generowany na serwerze, gdy użytkownik utworzy konto i odeśle je z powrotem do użytkownika. Następnie, gdy użytkownik chce się uwierzytelnić, musi wysłać nazwę użytkownika + hasło + apitoken (umieścić je w treści http). Pamiętaj, że HTTP nie szyfruje treści, więc hasło i apitoken mogą być wąchane. Użyj HTTPS, jeśli dotyczy to Ciebie.
Gabriel Llamas

1
jaki jest sens używania apitoken? czy to jest hasło „wtórne”?
Salvatorelab

2
@TheBronx Apitoken ma 2 przypadki użycia: 1) za pomocą apitoken można kontrolować dostęp użytkowników do systemu oraz monitorować i budować statystyki każdego użytkownika. 2) Jest to dodatkowy środek bezpieczeństwa, „wtórne” hasło.
Gabriel Llamas

1
Dlaczego powinieneś wysyłać identyfikator użytkownika wielokrotnie po udanym uwierzytelnieniu. Token powinien być jedynym sekretem potrzebnym do wykonywania wywołań interfejsu API.
Axel Napolitano

1
Idea tokena - oprócz nadużywania go do śledzenia aktywności użytkownika - polega na tym, że użytkownik idealnie nie potrzebuje nazwy użytkownika ani hasła do korzystania z aplikacji: token jest unikalnym kluczem dostępu. Pozwala to użytkownikom na upuszczenie dowolnego klucza w dowolnym momencie, wpływając tylko na aplikację, ale nie na konto użytkownika. W przypadku usługi internetowej token jest dość niewygodny - dlatego początkowe logowanie do sesji jest miejscem, w którym użytkownik otrzymuje ten token - dla „zwykłego” klienta ab token nie stanowi problemu: wprowadź go raz, a już prawie gotowe ;)
Axel Napolitano

22

Chciałbym przekazać ten kod jako rozwiązanie strukturalne dla postawionego pytania, zgodnie (mam nadzieję) z przyjętą odpowiedzią. (Możesz to bardzo łatwo dostosować).

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

Ten serwer można przetestować za pomocą 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" 

Dzięki za ten przykład jest bardzo pomocny, jednak staram się to śledzić, a kiedy loguję się, loguję się: curl: (51) SSL: nazwa podmiotu certyfikatu „xxxx” nie pasuje do nazwy hosta docelowego „xxx.net”. Na stałe zapisałem / etc / hosts, aby umożliwić https łączenie się na tym samym komputerze
mastervv


9

Istnieje wiele pytań dotyczących wzorców uwierzytelniania REST tutaj na SO. Oto najbardziej odpowiednie dla twojego pytania:

Zasadniczo musisz wybierać między użyciem kluczy API (najmniej bezpieczny, ponieważ klucz może zostać odkryty przez nieautoryzowanego użytkownika), kombinacji aplikacji i tokena (średni) lub pełnej implementacji OAuth (najbezpieczniejszy).


Dużo czytałem o oauth 1.0 i oauth 2.0 i obie wersje wydają się niezbyt bezpieczne. Wikipedia napisała, że ​​są pewne wycieki bezpieczeństwa w Oauth 1.0. Znalazłem również artykuł, że około jednego z głównych programistów opuścił zespół, ponieważ oauth 2.0 jest niezabezpieczony.
tschiela

12
@tschiela Powinieneś dodać odniesienia do wszystkiego, co tu cytujesz.
mikemaccana

3

Jeśli chcesz zabezpieczyć swoją aplikację, zdecydowanie powinieneś zacząć od HTTPS zamiast HTTP , to zapewnia stworzenie bezpiecznego kanału między tobą a użytkownikami, który zapobiegnie wąchaniu danych przesyłanych tam i z powrotem do użytkowników i pomoże zachować dane wymieniane poufne.

Możesz użyć JWT (JSON Web Tokeny) do zabezpieczenia interfejsów API RESTful , ma to wiele zalet w porównaniu do sesji po stronie serwera, korzyści to głównie:

1- Bardziej skalowalne, ponieważ serwery API nie będą musiały utrzymywać sesji dla każdego użytkownika (co może być dużym obciążeniem, gdy masz wiele sesji)

2- JWT są samodzielne i mają roszczenia, które określają na przykład rolę użytkownika oraz to, do czego może uzyskać dostęp i wydane w dniu i dacie wygaśnięcia (po którym JWT nie będzie ważne)

3- Łatwiejsza obsługa w modułach równoważenia obciążenia, a jeśli masz wiele serwerów API, ponieważ nie będziesz musiał udostępniać danych sesji ani konfigurować serwera tak, aby kierował sesję do tego samego serwera, za każdym razem, gdy żądanie z JWT trafi na dowolny serwer, może zostać uwierzytelnione i autoryzowane

4 - Mniejsza presja na twoją bazę danych, jak również nie będziesz musiał stale przechowywać i pobierać identyfikatora sesji i danych dla każdego żądania

5- Nie można manipulować JWT, jeśli użyjesz silnego klucza do podpisania JWT, więc możesz ufać roszczeniom w JWT wysyłanym z żądaniem bez konieczności sprawdzania sesji użytkownika i czy jest on autoryzowany, czy nie , możesz po prostu sprawdzić JWT, a następnie wszystko jest ustawione, aby wiedzieć, kto i co ten użytkownik może zrobić.

Wiele bibliotek zapewnia łatwe sposoby tworzenia i sprawdzania poprawności JWT w większości języków programowania, na przykład: w node.js jedną z najpopularniejszych jest jsonwebtoken

Ponieważ interfejsy API REST zasadniczo mają na celu utrzymanie bezstanowego serwera, dlatego JWT są bardziej zgodne z tą koncepcją, ponieważ każde żądanie jest wysyłane z tokenem autoryzacji, który jest samowystarczalny (JWT), bez konieczności śledzenia sesji użytkownika przez serwer w porównaniu do sesji, które sprawiają, że serwer stanowy, aby zapamiętał użytkownika i jego rolę, jednak sesje są również powszechnie używane i mają swoje zalety, które możesz wyszukać, jeśli chcesz.

Jedną ważną rzeczą do zapamiętania jest to, że musisz bezpiecznie dostarczyć JWT do klienta za pomocą HTTPS i zapisać go w bezpiecznym miejscu (na przykład w lokalnej pamięci masowej).

Możesz dowiedzieć się więcej o JWT z tego linku


1
Podoba mi się twoja odpowiedź, która wydaje się najlepszą aktualizacją tego starego pytania. Zadałem sobie inne pytanie na ten sam temat i Ty też możesz być pomocny. => stackoverflow.com/questions/58076644/…
pbonnefoi

Dzięki, cieszę się, że mogłem pomóc, zamieszczam odpowiedź na twoje pytanie
Ahmed Elkoussy 24.09.19

2

Jeśli chcesz mieć całkowicie zablokowany obszar aplikacji internetowej, do którego dostęp mają tylko administratorzy z Twojej firmy, autoryzacja SSL może być dla Ciebie. Zapewni to, że nikt nie będzie mógł nawiązać połączenia z instancją serwera, jeśli nie ma zainstalowanego autoryzowanego certyfikatu w przeglądarce. W zeszłym tygodniu napisałem artykuł o konfiguracji serwera: Artykuł

Jest to jedna z najbezpieczniejszych konfiguracji, jaką znajdziesz, ponieważ nie dotyczy to nazwy użytkownika / hasła, więc nikt nie może uzyskać dostępu, chyba że jeden z Twoich użytkowników przekaże pliki kluczy potencjalnemu hakerowi.


fajny artykuł. Ale prywatny obszar jest dla użytkowników.
tschiela

Dzięki - właśnie, powinieneś wybrać inne rozwiązanie, dystrybucja certyfikatów byłaby uciążliwa.
ExxKA
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.