node.jsで安全なREST APIを実装する方法


204

node.js、express、mongodbを使用してREST APIの計画を開始します。APIは、ウェブサイト(パブリックエリアとプライベートエリア)のデータを提供し、後でモバイルアプリを提供することもあります。フロントエンドはAngularJSで開発されます。

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

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

  • 認証および承認されたユーザーのみがプライベートエリアのデータを取得できます(ユーザーが権限を付与したデータのみ)

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

誰かがベストプラクティスや経験を提供できますか?「アーキテクチャ」に不足はありますか?


2
私はあなたが提供するフロントエンドからのみAPIが使用されると思いますか?その場合、セッションを使用してユーザーが有効であることを確認することは、良い解決策のようです。権限については、node-rolesをご覧ください
robertklep 2013年

2
これのために最終的に何をしましたか?共有できるボイラープレートコード(サーバー/モバイルアプリクライアント)はありますか?
Morteza Shahriari Nia 2014

回答:


175

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

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

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

ユーザーが認証されると(username + password + apitoken)、セッションの別のトークン(別名accesstoken)が生成されます。ここでも、node-uuidを使用します。ユーザーにaccesstokenとユーザーIDを送信します。userid(キー)とaccesstoken(値)は、1時間などの有効期限付きのredisに格納されます。

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

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

WebもこのAPIを使用しますが、アピトケンを使用する必要はありません。ExpressをRedisストアで使用することも、上記と同じ手法を使用することもできますが、apitokenチェックをバイパスして、ユーザーID +アクセストークンをCookieでユーザーに返します。

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

概要:

シーケンス図

apitokenを使用しない別の方法は、HTTPSを使用し、Authorizationヘッダーでユーザー名とパスワードを送信し、ユーザー名をredisにキャッシュすることです。


1
私もmongodbを使用していますが、redis(アトミック操作を使用)を使用してセッション(accesstoken)を保存すると、管理が非常に簡単になります。ユーザーがアカウントを作成してユーザーに送り返すと、apitokenがサーバーで生成されます。次に、ユーザーが認証を希望する場合は、ユーザー名+パスワード+ APIトークンを送信する必要があります(http本文に入れます)。HTTPは本文を暗号化しないため、パスワードとapitokenを盗聴できることに注意してください。懸念がある場合はHTTPSを使用してください。
Gabriel Llamas

1
を使用する意味は何apitokenですか?「セカンダリ」パスワードですか?
Salvatorelab

2
@TheBronx apitokenには2つの使用例があります。1)apitokenを使用すると、システムへのユーザーのアクセスを制御でき、各ユーザーの統計を監視および構築できます。2)これは、追加のセキュリティ対策である「セカンダリ」パスワードです。
ガブリエルラマ

1
認証に成功した後、ユーザーIDを何度も送信する必要があるのはなぜですか。トークンは、API呼び出しを実行するために必要な唯一の秘密でなければなりません。
Axel Napolitano 2014年

1
トークンのアイデア-ユーザーアクティビティを追跡するためにトークンを悪用することの他に-理想的には、ユーザーはアプリケーションを使用するためにユーザー名とパスワードを必要としないということです。トークンは一意のアクセスキーです。これにより、ユーザーはいつでも任意のキーをドロップでき、アプリのみに影響し、ユーザーアカウントには影響しません。Webサービスの場合、トークンは非常に便利ではありません。そのため、セッションの最初のログインは、ユーザーがそのトークンを取得する場所です。「通常の」クライアントabの場合、トークンは問題ありません。一度入力すれば、ほとんど完了です。 ;)
Axel Napolitano

22

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

// ------------------------------------------------------
// 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" 

このサンプルのおかげで非常に便利ですが、私はこれを試してみて、接続してログインすると次のように表示されます:curl:(51)SSL:certificate subject name 'xxxx' does not match target host name 'xxx.net'。/ etc / hostsをハードコーディングして、同じマシンでのhttps接続を許可
mastervv


9

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

基本的に、APIキー(不正なユーザーがキーを発見する可能性があるため安全性が最も低い)、アプリキーとトークンの組み合わせ(中)、または完全なOAuth実装(最も安全)のいずれかを選択する必要があります。


私はoauth 1.0とoauth 2.0についてたくさん読みましたが、どちらのバージョンも安全ではないようです。ウィキペディアは、oauth 1.0のセキュリティリークの一部であると書いています。また、コア開発者の1人がチームを離れたため、oauth 2.0は安全ではないという記事も見つけました。
tschiela

12
@tschielaここで引用するものへの参照を追加する必要があります。
mikemaccana 2014

3

アプリケーションを保護する場合は、HTTPではなくHTTPSを使用することから始めてください。これにより、ユーザーとユーザーの間に安全なチャネルが作成され、ユーザーにやり取りされるデータを傍受することを防ぎ、データの保持に役立ちます。機密情報を交換しました。

JWT(JSON Web Tokens)を使用してRESTful APIを保護できます。これには、サーバー側のセッションと比較して多くの利点があります。主な利点は次のとおりです。

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

2- JWTは自己完結型であり、たとえばユーザーロールを定義するクレームを持ちます&アクセスできるもの&日付&有効期限日に発行されます(それ以降はJWTは無効になります)

3- JWTを使用したリクエストがいずれかのサーバーにヒットするたびに、セッションデータを共有したり、セッションを同じサーバーにルーティングするようにサーバーを構成したりする必要がないため、ロードバランサー全体での処理がより簡単になります。 &承認済み

4- DBへのプレッシャーが軽減され、リクエストごとにセッションIDとデータを常に保存および取得する必要がなくなります

5- JWTに署名するために強力な鍵を使用する場合、JWTを改ざんできないため、ユーザーセッションをチェックする必要がなく、リクエストが送信されたJWTのクレームを信頼できます。 、JWTを確認するだけで、このユーザーが誰が何をできるかを知る準備が整います。

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

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

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

このリンクから JWTの詳細を学ぶことができます


1
私はこの古い質問からの最高のアップデートのように見えるあなたの答えが好きです。同じトピックについて別の質問をしましたが、あなたも参考になるかもしれません。=> stackoverflow.com/questions/58076644/...
pbonnefoi

おかげで、私はあなたを助けることができてうれしいです、私はあなたの質問に答えを投稿しています
Ahmed Elkoussy

2

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

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


素敵な記事。ただし、プライベートエリアはユーザー用です。
tschiela

ありがとうございます。そうです。別のソリューションを選択する必要があります。証明書の配布は面倒です。
ExxKA 2013年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.