web-dev-qa-db-ja.com

node.jsを使用してセキュアなREST AP​​Iを実装する方法

Node.js、express、およびmongodbを使用してREST AP​​Iの計画を開始します。 APIは、Webサイト(パブリックおよびプライベートエリア)、そして後にモバイルアプリケーション用のデータを提供します。フロントエンドはAngularJSで開発されます。

数日の間、REST AP​​Iのセキュリティ保護についてはよく読んでいますが、最終的な解決策には至りません。私が理解している限りでは、基本的なセキュリティを提供するためにHTTPSを使用することです。しかし、どのように私はそのユースケースでAPIを保護することができます:

  • ウェブサイト/アプリの訪問者/ユーザーのみがウェブサイト/アプリのパブリックエリアのデータを取得することができます

  • 認証され許可されたユーザーだけがプライベートエリアのデータを取得することを許可されます(ユーザーが許可を与えたデータのみ)

現時点では、アクティブセッションを持つユーザーにのみAPIの使用を許可することを検討しています。ユーザーを認証するにはパスポートを使用し、許可を得るために自分で何かを実装する必要があります。 HTTPSの上にすべて。

誰かがベストプラクティスや経験を提供できますか?私の「建築」に欠けているものはありますか?

197
tschiela

私はあなたが説明するのと同じ問題を抱えています。私が構築しているWebサイトには携帯電話からもブラウザからもアクセスできるので、ユーザーがサインアップ、ログイン、そして特定の作業を行えるようにするにはAPIが必要です。さらに、私はスケーラビリティ、同じコードが異なるプロセス/マシン上で実行されることをサポートする必要があります。

ユーザーはリソース(POST/PUTアクション)を作成することができるので、APIを保護する必要があります。あなたはoauthを使うこともできますし、あなた自身の解決策を構築することもできますが、パスワードを発見するのが本当に簡単であるなら、すべての解決策が破られる可能性があることを覚えておいてください。基本的な考え方は、ユーザー名、パスワード、トークン(別名apitoken)を使用してユーザーを認証することです。このapitokenは node-uuid を使用して生成でき、パスワードは pbkdf2 を使用してハッシュできます。

その後、セッションをどこかに保存する必要があります。普通のオブジェクトとしてメモリに保存した場合、サーバを終了して再起動するとセッションが破壊されます。また、これはスケーラブルではありません。マシン間の負荷分散にhaproxyを使用する場合、または単にワーカーを使用する場合は、このセッション状態は単一のプロセスに格納されるため、同じユーザーが別のプロセス/マシンにリダイレクトされると、再度認証が必要になります。したがって、セッションを共通の場所に保存する必要があります。これは通常redisを使って行われます。

ユーザーが認証されると(ユーザー名+パスワード+ apitoken)、セッション用に別のトークン(別名accessstoken)が生成されます。繰り返しますが、node-uuidを使用します。アクセストークンとユーザーIDをユーザーに送信します。ユーザID(キー)およびアクセストークン(値)は、期限切れの時間とともに期限切れで再格納される。 1時間.

現在、ユーザーが残りのAPIを使用して操作を行うたびに、ユーザーIDとアクセストークンを送信する必要があります。

ユーザーが残りのapiを使用してサインアップできるようにする場合は、新しいユーザーにはapitokenがないため、admin apitokenを使用してadminアカウントを作成し、モバイルアプリに保存する必要があります。彼らはサインアップします。

WebもこのAPIを使用していますが、apitokensを使用する必要はありません。 redisストアでexpressを使用することも、上記と同じテクニックを使用することもできますが、apitokenチェックを迂回してユーザーID + cookieにアクセスしたユーザーに戻ります。

プライベートエリアがある場合は、認証時にユーザー名と許可ユーザーを比較します。ユーザーにロールを適用することもできます。

概要:

sequence diagram

Apitokenなしの代替手段は、HTTPSを使用してAuthorizationヘッダーにユーザー名とパスワードを送信し、ユーザー名をredisにキャッシュすることです。

170
Gabriel Llamas

私は、受け入れられた答えに従って(私はそう願っています)、提起された質問に対する構造的解決策としてこのコードを貢献したいと思います。 (あなたは非常に簡単にそれをカスタマイズすることができます)。

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

このサーバーは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" 
20
cibercitizen1

私はこれをかなり基本的な、しかし明確な方法で行うサンプルアプリを完成させたところです。認証管理のためにユーザとパスポートを保存するためにmongooseとmongodbを使用します。

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

12
clangager

ここでSOのREST authパターンについて多くの質問があります。これらはあなたの質問に最も関連があります:

基本的には、APIキー(不正なユーザーによってキーが発見される可能性があるため最も安全性が低い)、アプリケーションキーとトークンコンボ(中程度)、または完全なOAuth実装(最も安全性が高い)のいずれかを選択する必要があります。

8
Zim

会社の管理者しかアクセスできないWebアプリケーションの領域を完全にロックしたい場合は、SSL認証が必要になります。承認された証明書がブラウザにインストールされていない限り、だれもサーバーインスタンスに接続できないことが保証されます。先週、私はサーバーの設定方法についての記事を書きました。 Article

ユーザー名/パスワードが含まれていないため、これは最も安全な設定の1つです。ユーザーの1人がキーファイルを潜在的なハッカーに渡さない限り、誰もアクセスできません。

2
ExxKA

あなたがあなたのアプリケーションを安全にしたいなら、それからあなたは間違いなくHTTPの代わりにHTTPSを使うことから始めるべきですユーザーとの間でやり取りされるデータの盗聴を防ぐことができ、データ交換の機密性を保つのに役立ちます。

RESTful APIを保護するためにJWT(JSON Web Tokens)を使用することができます。これはサーバーサイドセッションと比較すると多くの利点があります。利点は主に以下のとおりです。

1 - あなたのAPIサーバーが各ユーザーのためにセッションを維持する必要がないので、よりスケーラブルです(あなたが多くのセッションを持っているとき、これは大きな負担になる可能性があります)

2- JWTは自己完結型であり、ユーザーの役割を定義するクレームを持っています。

3-ロードバランサー間での処理が簡単。複数のAPIサーバーがある場合、セッションデータを共有したり、同じサーバーにセッションをルーティングするようにサーバーを設定したりする必要がないため、JWTによるリクエストが認証可能承認済み

4-あなたのDBへのより少ない圧力と同様にあなたは絶えず各要求のためにセッションIDとデータを保存して取り出す必要はありません

5- JWTに署名するために強力な鍵を使用する場合、JWTは改ざんできません。そのため、ユーザーセッションを確認しなくても、要求と共に送信されたJWT内のクレームを信頼できます。 JWTをチェックするだけで、その後、このユーザーが誰に何をできるのかを知ることができます。

多くのライブラリは、ほとんどのプログラミング言語でJWTを簡単に作成および検証する方法を提供しています。例えば、node.jsで最も一般的なものの1つは jsonwebtoken です。

REST AP​​Iは通常、サーバーをステートレスに保つことを目的としているため、各要求は自己完結型の認証トークン(JWT)を使用して送信されるため、JWTはその概念と互換性があります。サーバーがユーザーとその役割を記憶するようにサーバーをステートフルにするセッションと比較して、サーバーがユーザーセッションを追跡する必要はありませんが、セッションも広く使用されています。

注意すべき重要なことの1つは、HTTPSを使用してJWTをクライアントに安全に配信し、安全な場所(ローカルストレージなど)に保存する必要があることです。

あなたはJWTについてもっと知ることができます このリンクから

0
Ahmed Elkoussy