Comment implémenter une API REST sécurisée avec node.js

204

Je commence à planifier une API REST avec node.js, express et mongodb. L'API fournit des données pour un site Web (espace public et privé) et peut-être plus tard une application mobile. Le frontend sera développé avec AngularJS.

Pendant quelques jours, j'ai lu beaucoup de choses sur la sécurisation des API REST, mais je ne parviens pas à une solution finale. Autant que je sache, c'est d'utiliser HTTPS pour fournir une sécurité de base. Mais comment je peux protéger l'API dans ces cas d'utilisation:

  • Seuls les visiteurs / utilisateurs du site / de l'application sont autorisés à obtenir des données pour la zone publique du site / de l'application

  • Seuls les utilisateurs authentifiés et autorisés sont autorisés à obtenir des données pour un espace privé (et uniquement les données pour lesquelles l'utilisateur a accordé des autorisations)

Pour le moment, je pense à autoriser uniquement les utilisateurs ayant une session active à utiliser l'API. Pour autoriser les utilisateurs, j'utiliserai un passeport et pour obtenir la permission, je dois mettre en œuvre quelque chose pour moi. Tout en haut de HTTPS.

Quelqu'un peut-il fournir quelques bonnes pratiques ou expériences? Y a-t-il un manque dans mon «architecture»?

tschiela
la source
2
Je suppose que l'API ne doit être utilisée que depuis l'interface que vous fournissez? Dans ce cas, l'utilisation de la session pour garantir la validité de l'utilisateur semble être une bonne solution. Pour les autorisations, vous pouvez consulter les rôles de nœud .
robertklep
2
Qu'avez-vous finalement fait pour ça? Tout code de plaque de chaudière (serveur / client d'application mobile) que vous pouvez partager?
Morteza Shahriari Nia

Réponses:

176

J'ai eu le même problème que vous décrivez. Le site Web que je construis est accessible à partir d'un téléphone mobile et du navigateur, j'ai donc besoin d'une API pour permettre aux utilisateurs de s'inscrire, de se connecter et d'effectuer certaines tâches spécifiques. De plus, je dois prendre en charge l'évolutivité, le même code s'exécutant sur différents processus / machines.

Parce que les utilisateurs peuvent CRÉER des ressources (alias actions POST / PUT), vous devez sécuriser votre API. Vous pouvez utiliser oauth ou vous pouvez créer votre propre solution mais gardez à l'esprit que toutes les solutions peuvent être cassées si le mot de passe est vraiment facile à découvrir. L'idée de base est d'authentifier les utilisateurs en utilisant le nom d'utilisateur, le mot de passe et un jeton, alias l'apitoken. Cet apitoken peut être généré en utilisant node-uuid et le mot de passe peut être haché en utilisant pbkdf2

Ensuite, vous devez enregistrer la session quelque part. Si vous l'enregistrez en mémoire dans un objet simple, si vous tuez le serveur et le redémarrez à nouveau, la session sera détruite. De plus, ce n'est pas évolutif. Si vous utilisez haproxy pour équilibrer la charge entre les machines ou si vous utilisez simplement des travailleurs, cet état de session sera stocké dans un seul processus, donc si le même utilisateur est redirigé vers un autre processus / machine, il devra s'authentifier à nouveau. Par conséquent, vous devez stocker la session dans un endroit commun. Cela se fait généralement à l'aide de redis.

Lorsque l'utilisateur est authentifié (nom d'utilisateur + mot de passe + apitoken), générez un autre jeton pour la session, alias accesstoken. Encore une fois, avec node-uuid. Envoyez à l'utilisateur l'accesstoken et l'ID utilisateur. L'ID utilisateur (clé) et le jeton d'accès (valeur) sont stockés dans redis avec et expirent le temps, par exemple 1h.

Maintenant, chaque fois que l'utilisateur effectue une opération à l'aide de l'API de repos, il devra envoyer l'ID utilisateur et le jeton d'accès.

Si vous autorisez les utilisateurs à s'inscrire à l'aide de l'API de repos, vous devrez créer un compte administrateur avec un apitoken administrateur et les stocker dans l'application mobile (crypter nom d'utilisateur + mot de passe + apitoken) car les nouveaux utilisateurs n'auront pas d'apitoken quand ils s'inscrivent.

Le Web utilise également cette API, mais vous n'avez pas besoin d'utiliser des apitokens. Vous pouvez utiliser express avec un magasin redis ou utiliser la même technique que celle décrite ci-dessus, mais en contournant le contrôle apitoken et en renvoyant à l'utilisateur l'identifiant utilisateur + accesstoken dans un cookie.

Si vous disposez d'espaces privés, comparez le nom d'utilisateur avec les utilisateurs autorisés lors de leur authentification. Vous pouvez également appliquer des rôles aux utilisateurs.

Résumé:

diagramme de séquençage

Une alternative sans apitoken serait d'utiliser HTTPS et d'envoyer le nom d'utilisateur et le mot de passe dans l'en-tête d'autorisation et de mettre en cache le nom d'utilisateur dans redis.

Gabriel Llamas
la source
1
J'utilise également mongodb mais c'est assez facile à gérer si vous enregistrez la session (accesstoken) en utilisant redis (utilisez les opérations atomiques). L'apitoken est généré sur le serveur lorsque l'utilisateur crée un compte et le renvoie à l'utilisateur. Ensuite, lorsque l'utilisateur veut s'authentifier, il doit envoyer un nom d'utilisateur + mot de passe + apitoken (les mettre dans le corps http). Gardez à l'esprit que HTTP ne crypte pas le corps afin que le mot de passe et l'apitoken puissent être reniflés. Utilisez HTTPS si cela vous inquiète.
Gabriel Llamas
1
quel est l'intérêt d'utiliser un apitoken? est-ce un mot de passe "secondaire"?
Salvatorelab
2
@TheBronx L'apitoken a 2 cas d'utilisation: 1) avec un apitoken vous pouvez contrôler l'accès des utilisateurs à votre système et vous pouvez surveiller et construire des statistiques de chaque utilisateur. 2) C'est une mesure de sécurité supplémentaire, un mot de passe "secondaire".
Gabriel Llamas
1
Pourquoi devriez-vous envoyer l'ID utilisateur encore et encore après une authentification réussie. Le jeton doit être le seul secret dont vous avez besoin pour effectuer des appels API.
Axel Napolitano
1
L'idée du jeton - en plus d'en abuser pour suivre l'activité des utilisateurs - est qu'un utilisateur n'a idéalement pas besoin de nom d'utilisateur et de mot de passe pour utiliser une application: le jeton est la clé d'accès unique. Cela permet aux utilisateurs de supprimer n'importe quelle clé à tout moment affectant uniquement l'application, mais pas le compte d'utilisateur. Pour un service Web, un jeton est assez difficile à utiliser - c'est pourquoi une connexion initiale pour une session est l'endroit où l'utilisateur obtient ce jeton - pour un ab client "normal", un jeton n'est pas un problème: entrez-le une fois et vous avez presque terminé ;)
Axel Napolitano
22

Je voudrais apporter ce code comme solution structurelle à la question posée, en fonction (je l'espère) de la réponse acceptée. (Vous pouvez très facilement le personnaliser).

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

Ce serveur peut être testé avec 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
la source
Merci pour cet exemple, c'est très utile, mais j'essaie de suivre cela, et lorsque je me connecte pour le connecter, il dit ceci: curl: (51) SSL: le nom du sujet du certificat 'xxxx' ne correspond pas au nom d'hôte cible 'xxx.net'. J'ai codé en dur mes / etc / hosts pour permettre la connexion https sur la même machine
mastervv
11

Je viens de terminer un exemple d'application qui le fait de manière assez basique mais claire. Il utilise mangouste avec mongodb pour stocker les utilisateurs et le passeport pour la gestion de l'authentification.

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

clangager
la source
7
Vous utilisez des cookies pour sécuriser l'API. Je ne pense pas que ce soit correct.
Vince Yuan,
9

Il y a beaucoup de questions sur les modèles d'authentification REST ici sur SO. Ce sont les plus pertinents pour votre question:

Fondamentalement, vous devez choisir entre l'utilisation de clés API (moins sécurisées car la clé peut être découverte par un utilisateur non autorisé), une combinaison de clés et de jetons d'application (moyenne) ou une implémentation complète d'OAuth (la plus sécurisée).

Zim
la source
J'ai beaucoup lu sur oauth 1.0 et oauth 2.0 et les deux versions ne semblent pas très sécurisées. Wikipedia a écrit qu'il y avait des fuites de sécurité dans oauth 1.0. J'ai également trouvé un article selon lequel l'un des développeurs principaux quitte l'équipe parce que 2.0 est non sécurisé.
tschiela
12
@tschiela Vous devez ajouter des références à tout ce que vous citez ici.
mikemaccana
3

Si vous souhaitez sécuriser votre application, vous devez certainement commencer par utiliser HTTPS au lieu de HTTP , cela garantit une création de canal sécurisé entre vous et les utilisateurs qui empêchera de renifler les données envoyées d'avant en arrière aux utilisateurs et aidera à conserver les données échangé confidentiel.

Vous pouvez utiliser des JWT (JSON Web Tokens) pour sécuriser les API RESTful , cela présente de nombreux avantages par rapport aux sessions côté serveur, les avantages sont principalement:

1- Plus évolutif, car vos serveurs API n'auront pas à maintenir de sessions pour chaque utilisateur (ce qui peut être un gros fardeau lorsque vous avez plusieurs sessions)

2- Les JWT sont autonomes et ont les revendications qui définissent le rôle d'utilisateur par exemple et ce à quoi il peut accéder et émis à la date et à la date d'expiration (après quoi le JWT ne sera pas valide)

3- Plus facile à gérer entre les équilibreurs de charge et si vous avez plusieurs serveurs API car vous n'aurez pas à partager les données de session ni à configurer le serveur pour acheminer la session vers le même serveur, chaque fois qu'une demande avec un JWT frappe n'importe quel serveur, elle peut être authentifiée & autorisé

4- Moins de pression sur votre base de données et vous n'aurez pas à stocker et récupérer en permanence l'identifiant et les données de session pour chaque demande

5- Les JWT ne peuvent pas être falsifiés si vous utilisez une clé forte pour signer le JWT, vous pouvez donc faire confiance aux revendications dans le JWT envoyé avec la demande sans avoir à vérifier la session utilisateur et s'il est autorisé ou non , vous pouvez simplement vérifier le JWT et vous êtes prêt à savoir qui et ce que cet utilisateur peut faire.

De nombreuses bibliothèques offrent des moyens simples de créer et de valider des JWT dans la plupart des langages de programmation, par exemple: dans node.js, l'un des plus populaires est jsonwebtoken

Étant donné que les API REST visent généralement à garder le serveur sans état, les JWT sont donc plus compatibles avec ce concept car chaque demande est envoyée avec un jeton d'autorisation qui est autonome (JWT) sans que le serveur doive garder une trace de la session utilisateur par rapport aux sessions qui font la serveur avec état afin qu'il se souvienne de l'utilisateur et de son rôle, cependant, les sessions sont également largement utilisées et ont leurs avantages, que vous pouvez rechercher si vous le souhaitez.

Une chose importante à noter est que vous devez livrer le JWT en toute sécurité au client en utilisant HTTPS et l'enregistrer dans un endroit sûr (par exemple dans le stockage local).

Vous pouvez en savoir plus sur les JWT à partir de ce lien

Ahmed Elkoussy
la source
1
J'aime votre réponse qui semble être la meilleure mise à jour de cette ancienne question. Je me suis posé une autre question sur le même sujet et vous pourriez aussi être utile. => stackoverflow.com/questions/58076644/…
pbonnefoi
Merci, heureux d'avoir pu aider, je poste une réponse à votre question
Ahmed Elkoussy
2

Si vous souhaitez avoir une zone complètement verrouillée de votre application Web à laquelle seuls les administrateurs de votre entreprise peuvent accéder, alors l'autorisation SSL peut-être pour vous. Cela garantira que personne ne peut se connecter à l'instance de serveur à moins qu'un certificat autorisé ne soit installé dans son navigateur. La semaine dernière, j'ai écrit un article sur la configuration du serveur: Article

Il s'agit de l'une des configurations les plus sécurisées que vous trouverez car aucun nom d'utilisateur / mot de passe n'est impliqué, donc personne ne peut y accéder à moins qu'un de vos utilisateurs ne remette les fichiers clés à un pirate potentiel.

ExxKA
la source
bel article. Mais l'espace privé est réservé aux utilisateurs.
tschiela
Merci - à droite, alors vous devriez opter pour une autre solution, la distribution de certificats serait une douleur.
ExxKA