目次


アカウント承認を Node.js Bluemix アプリケーションで管理する

Comments

B2C (企業対一般消費者) アプリケーションなどの多くの環境では、ユーザーに自分のことを登録させた後、管理者がそのアカウント、または特定のアカウントの権限を承認または却下するのが最善の方法となります。この記事では、そのようなシステムを実装する方法を説明します。

まず始めに、このシステムを説明するための非常に単純なアプリケーションを紹介します。ただし、このアプリケーションには多数のセキュリティー問題があるので、これらのセキュリティー問題を 1 つひとつ調べて、修正する方法を示します。重要なセクションについては、皆さんが理解してその知識を自分のアプリケーションに適用できるよう、1 行ずつ詳しく見ていきます。

アプリケーションを作成するために必要となるもの

アプリケーションのデモ版とセキュア版

このアプリケーションには、デモ版とセキュア版の 2 つを用意してあります。このチュートリアルの前半では、デモ版アプリケーションを取り上げ、その後でセキュリティーについて説明する際に、セキュア版アプリケーションを取り上げます。これら 2 つののアプリケーションには、以下のボタンでアクセスすることができます。

非セキュアなアプリを実行するコードを入手する

セキュアなアプリを実行するコードを入手する

このチュートリアルでは、ユーザーが自分の情報を登録すると、管理者がそのアカウントを承認または却下できるアプリケーションを作成する方法を紹介します。また、一般的なセキュリティーの脆弱性とそのような脆弱性を防ぐ方法についても見ていきます。

デモ版アプリケーション

デモ版アプリケーションを開くと、「Login (ログイン)」、「Request an account (アカウントのリクエスト)」、「Approve accounts (アカウントの承認)」という 3 つのボタンが表示されます。

メイン画面のスクリーン・キャプチャー

ユーザー・インターフェース

ユーザーが「Login (ログイン)」ボタンをクリックすると、ログイン画面が表示されます。この画面で、ユーザーは e-メール・アドレスとパスワードを入力します。

ユーザー・インターフェースのスクリーン・キャプチャー
ユーザー・インターフェースのスクリーン・キャプチャー

ユーザーがまだ登録されていない場合は、「Login (ログイン)」ボタンではなく「Request an account (アカウントのリクエスト)」ボタンをクリックします。このボタンをクリックすると、「Account Request (アカウントのリクエスト)」フォームが表示されます。ユーザーはそこに自分の名前、パスワード、e-メール・アドレス、電話番号、理由を入力した後、「Submit (送信)」をクリックします。

アカウントのリクエスト・フォームのスクリーン・キャプチャー
アカウントのリクエスト・フォームのスクリーン・キャプチャー

メイン画面にある 3 つ目のボタンは、「Approve accounts (アカウントの承認)」ボタンです。このボタンは実際には管理者専用ですが、このアプリケーションはデモ用なので、ボタンを有効なままにしておきました。管理者はこのボタンをクリックして現在のアカウントのリクエストをすべて表示し、それぞれのアカウントを承認または却下することができます。

承認リクエストの一覧のスクリーン・キャプチャー
承認リクエストの一覧のスクリーン・キャプチャー

アカウントのリクエストが承認された後、ユーザーはアプリケーションを再試行してログインすることができます。

アカウントのリクエスト

メイン画面の「Request an Account (アカウントのリクエスト)」ボタンをクリックすると、ユーザーは acct_request.html にリダイレクトされます。これは HTML による単純な POST フォームで、このフォームで興味深い唯一の点は、Bootstrap テーマを使用していることです (アプリケーションの残りすべての HTML ファイルも同じです)。

このフォームを POST 送信する先となっている /acct_req は、app.js 内の以下の関数で処理されます。

// Deal with new account requests
app.post("/acct_req", function(req, res) {
  acctReqs[req.body.email] = req.body;
  res.send("Request received, thank you " + req.body.name + ".");
});

上記の関数は極めて単純です。この関数で行っている処理のほとんどが、POST リクエストの本体を構文解析する作業になっています。環境にこの構文解析処理を行わせるには、body-parser パッケージをインポートして使用する必要があります。それには、次の 2 つのアクションが必要です。

  1. package.json 内で、依存関係リストに body-parser を追加することで (4 行目)、このパッケージが必要であることを宣言します。
    	"dependencies": {
    		"express": "4.12.x",
    		"cfenv": "1.0.x",
    		"body-parser": "*"
    	},
  2. body-parser パッケージ用のオブジェクトを作成し、このオブジェクトを使用するようアプリに指示します。アプリ内のハンドラーには、ターミナル・ハンドラーまたはミドル・ハンドラーのいずれかを使用することができます。ターミナル・ハンドラーはリクエストに応答し、ミドル・ハンドラーは (例えば、POST リクエストの本体を構文解析することによって) リクエストを変更します。
	// Use body-parser to receive POST form requests
	var bodyParser = require("body-parser");
	app.use(bodyParser.urlencoded({extended:true}));

フォームのフィールドはすべて、req.body という連想配列 (別名「ハッシュ・テーブル」) に格納されます。アカウントのリクエストを保管するために必要なことは、このリクエスト配列にリクエストを格納することだけです。

  acctReqs[req.body.email] = req.body;

このアプリケーションでは、e-メール・アドレスを一意の ID として使用しています。e-メール・アドレスは、アカウントのリクエスト配列 acctReqs のインデックスとして使用されます。

acctReqs の初期値

acctReqs を初期化するコードを以下に示します。

	var acctReqs = {"hacker@evil.com": {
                  email: "hacker@evil.com",
                  name: "Bad Guy",
                  justification: "I want to break your stuff.",
                  password: "Object00"},
             "niceguy@good.com": {
                  email: "niceguy@good.com",
                  name: "Good Guy",
                  justification: "You can trust me",
                  password: "Object00"}
                };

ご覧のように、コードにはすでに、hacker@evil.comniceguy@good.com という 2 つのエントリーが含まれています。これらのエントリーが提供するリクエストは、アプリケーションが起動されると、承認または却下することができます。

管理者による承認

権限を与えられた管理者が「Approve accounts (アカウントの承認)」ボタンをクリックすると、ブラウザーに acct_approval.html が開きます。このファイルはデータのモデルとユーザー・インターフェースのビューの間のコントローラーとして Angular (MEAN スタックの「A」の部分) を使用しているため、複雑になっています。

サーバーからリクエストを受け取る

管理者に対してリクエストを表示するには、Web ページがそれらのリクエストの内容を認識している必要があります。しかし、acctReqs はサーバー上に保管されています。acctReqs 変数を含め、acct_approval.html ファイル全体を直接 app.js 内に直接生成することもできますが、そうすると、理解しにくいコードになってしまいます。それよりも、公開ディレクトリー内に HTML ファイルを置いたほうが遥かに簡単です。

以下のソリューションでは、変数を別個のスクリプトに格納した上で、そのスクリプトを acct_approval.html に読み込みます。

<script src="acctReqs.js"></script>

変数を格納したスクリプトは、JSON (JavaScript Object Notation) はオブジェクトの JavaScript 表現であるという事実に基づき、app.js によって動的に生成されます。

// Request for the list of accounts
app.get("/acctReqs.js", function(req, res) {
  res.send("var acctReqs = " + JSON.stringify(acctReqs) + ";");
});

http://approval-req.mybluemix.net/acctReqs.js を調べると、現在のリクエストがわかります。

Angular をセットアップする

Angular が使用するエンティティーには、アプリケーションと (1 つまたは複数の) コントローラーの 2 つがあります。いずれも HTML タグに関連付けて、そのスコープを設定しなければなりません。この例では、これらのコントローラーは両方とも最上位レベルのタグ html に関連付けています。

<html ng-app="myApp" ng-controller="myCtrl">
<!-- All the ng.. attributes are directives to the Angular library, which is
     used as the data model →

また、script タグ内に Angular をセットアップすることも必要です。このコードは最初にアプリケーション myApp を作成し、次にコントローラー myCtrl を作成します。myCtrl を作成するための呼び出しに設定するパラメーターのうちの 1 つは、構造体 $scope を初期化する関数です。この構造体は、コントローラーの「スコープ」に含まれる以下の変数と関数からなります (つまり、コントローラーがこれらの変数と関数を管理します)。

  • acctReqs: この変数には、サーバー上での値と同じ値が格納されます。
  • approve(email)decline(email): これらの関数は、リクエストが承認されたか却下されたかをサーバーに伝える URL へとブラウザーをリダイレクトします。
<script>
// The data model

var myApp = angular.module("myApp", []);

myApp.controller("myCtrl", function($scope) {
       // The account requests come in a separate script we get from the server
      $scope.acctReqs = acctReqs;


      $scope.approve = function(email) {
        window.location = "approve/" + escape(email);
      }

      $scope.decline = function(email) {
        window.location = "decline/" + escape(email);
      }
});

</script>

Angular を使用する

Angular は、HTML では 2 通りの方法で使用されます。第 1 に、HTML 属性に ng- で始まるタグがある場合、そのタグは Angular 呼び出しです。第 2 に、HTML に含まれる式が、{{expression}} のように二重波括弧で囲まれている場合、その式はコントローラー・スコープのコンテキストで JavaScript として評価されます。

以下のコードで最初に Angular が使われているところは、ng-repeat 属性です。この属性により、配列に含まれる項目ごとにタグ (この例では <tr>) が繰り返されます (フィルターを追加して、項目の一部だけを使用することも可能です)。この構文は、acctReqs 連想配列に含まれる各項目に対して、表の行を作成します。現在の行の値は、一時スコープ変数 req に格納されます。

次に Angular が使われているところは、ng-click 属性です。通常の onClick と同様に、ボタンがクリックされたときにどの JavaScript コードが実行されるかが、この属性によって決まります。ただし、この例では、JavaScript コードが Anglar スコープのコンテキストで実行されます。つまり、このスコープで宣言されている関数と変数しか使用することができません。そのため、ng-click 内で approve(email)decline(email) のそれぞれに 1 行使うのではなく、approve(email) と decline(email) を Anglar スコープ内で宣言しなければなりませんでした。

最後の {{req.name}}{{req.email}}、および {{req.justification}} は、管理者がリクエストを承認または却下するかを決定できるようにリクエスト・パラメーターを表示します。

    <table class="table table-condensed table-striped">
      <tr><th></th><th>Name</th><th>E-mail</th><th>Justification</th></tr>
      <tr ng-repeat="req in acctReqs">
        <td>
          <button class="btn btn-sm btn-success"
            ng-click="approve(req.email);">
            <span class="glyphicon glyphicon-ok" />
          </button>
          <button class="btn btn-sm btn-danger"
            ng-click="decline(req.email);">
            <span class="glyphicon glyphicon-remove" />
          </button>
        </td>
        <td>{{req.name}}</td>
        <td>{{req.email}}</td>
        <td>{{req.justification}}</td>
      </tr>
    </table>

サーバーによるレスポンスの処理

サーバーによるレスポンスの処理には、2 つの役割があります。1 つ目として、サーバーは acctReqs を変更してリクエストを削除する必要があります (リクエストが承認された場合は、accts も変更して新しいアカウントを追加します)。そして 2 つ目として、サーバーはブラウザーをアカウントの承認ページにリダイレクトする必要があります。

パスに :<urlVar> を使用した app.<HTML 動詞> 関数によって URL が処理されると、この変数が req.params.<urlVar> に格納されます。この場合、変数は e-メール・アドレスであり、ハッシュ・テーブルのキーとして使用されます。このキーに応じて、ハッシュ・テーブルが適切に変更されることになります。

最後の res.redirect(<url>) は、アカウントの承認ページにリダイレクトすることによって、ブラウザーに応答します。このレスポンスの「ページ」は、ブラウザーの観点ではサブディレクトリーにあるように見えるため、アプリケーションのメイン・ディレクトリーに再びリダイレクトされます。

// Deal with approved accounts
app.get("/approve/:email", function(req, res) {
    // Move the account from request to account list
    accts[req.params.email] = acctReqs[req.params.email];
    delete acctReqs[req.params.email];

    // Redirect to the account approval page
    res.redirect("../acct_approval.html");
});

// Deal with accounts that are not approved
app.get("/decline/:email", function(req, res) {
    // Delete the request
    delete acctReqs[req.params.email];

    // Redirect to the account approval page
    res.redirect("../acct_approval.html");
});

ログイン

ユーザーが「Login (ログイン)」ボタンをクリックした場合のリダイレクト先は、login.html です。これも単純な HTML による POST フォームで、Bootstrap を使っています。

app.js 内でログインを処理する関数も、同じく極めて単純なものです。この関数はまず、当該 e-メール・アドレスが設定された実際のアカウントの有無を調べます。このアカウントが存在する場合は、パスワードが一致するかどうかを調べます。その結果に応じて、適切なレスポンスを送信します。このアカウントが存在しなければ、それと同じアカウントについてのリクエストがあるかどうかを調べます。

// Deal with logins
app.post("/login_attempt", function(req, res) {
  if (accts[req.body.email] != undefined) { // The account exists
    if (accts[req.body.email].password == req.body.password) {
      res.send("Login successful, welcome " + accts[req.body.email].name);
    } else {
      res.send("Wrong password, " + accts[req.body.email].name);
    }
  } else {  // Account does not exists
    if (acctReqs[req.body.email] != undefined) { // There is a request
      res.send("Request not processed yet, " + acctReqs[req.body.email].name);
    } else {
      res.send("I don't know you.");
    }
  }
});

アプリケーションのセキュリティー

このアプリケーションは、恐ろしく非セキュアなものとして作成されています。現時点では自称ハッカーが、(例えば https://approval-req.mybluemix.net/approve/hacker@evil.com にアクセスすることで) 自分のアカウントを承認できてしまいます。また、https://approval-req.mybluemix.net/acctReqs.js にアクセスすることで、パスワードを含むリクエストのリストを取得することも可能です。しかも、パスワードに誤りがあった場合のレスポンスと、e-メール・アドレスが不明だった場合のレスポンスとの違いを判別することで、登録済みユーザーの e-メール・アドレスを特定することもできます。

従って、次のステップではこれらのセキュリティー・ホールを修正して、よりセキュアなアプリケーションに仕上げます。そのセキュア版アプリケーションには、approval-req-sec.mybluemix.net でアクセスすることができます。

パスワードの漏洩

acctReqs.js にアクセスしてもパスワードが明らかにならないようにする方法は 2 つあります。それは、パスワードを提供する app.get() の呼び出しを修正するか、acctReqs 内に使用できる形でパスワードを含めない (パスワードを分けて保管するか、暗号化またはハッシュ化します) かのいずれかです。この記事では、両方の方法を説明します。

パスワードをハッシュ化するには、password-hash パッケージを使用します。

  1. まず、以下の行を package.json 内の依存関係に追加します (4 行目)。
    "password-hash": "*",
    	"dependencies": {
    		"express": "4.12.x",
    		"cfenv": "1.0.x",
    		"password-hash": "*",
    		"body-parser": "*"
    	},
  2. 次に、パッケージを使用するための require 呼び出しを追加し (1 行目)、この呼び出しを使用するように初期 acctReqs を変更します (6 行目と 11 行目)。
    pwdHash = require("password-hash");	
    	var acctReqs = {"hacker@evil.com": {
                      email: "hacker@evil.com",
                      name: "Bad Guy",
                      justification: "I want to break your stuff.",
                      password: pwdHash.generate("Object00")},
                 "niceguy@good.com": {
                      email: "niceguy@good.com",
                      name: "Good Guy",
                      justification: "You can trust me",
                      password: pwdHash.generate("Object00")}
                    };
  3. また、/login_attempt リクエストに応答する関数も、ハッシュ化されたパスワードを使用するように変更する必要があります (4 行目)。
    // Deal with logins
    app.post("/login_attempt", function(req, res) {
      if (accts[req.body.email] != undefined) { // The account exists
        if (pwdHash.verify(req.body.password, accts[req.body.email].password)) {
          res.send("Login successful, welcome " + accts[req.body.email].name);
        } else {
          res.send("Wrong password, " + accts[req.body.email].name);
        }
      } else {  // Account does not exists
        if (acctReqs[req.body.email] != undefined) { // There is a request
          res.send("Request not processed yet, " + acctReqs[req.body.email].name);
        } else {
          res.send("I don't know you.");
        }
      }
    });

パスワードをハッシュ化したとしても、パスワードが漏洩するような事態は、可能な限り回避しなければなりません。パスワードが漏洩しないようにするには、パスワード・データを送信する app.get() の呼び出しを変更します。JSON.stringify() は 2 番目の引数として関数を取る場合、すべてのキー/値のペアに対してその関数を呼び出して、値を変更できるようにします。この関数には至極もっともな名前が付けられており、「置き換え」関数と呼ばれています。

// Request for the list of accounts
app.get("/acctReqs.js", function(req, res) {
  res.send("var acctReqs = " + JSON.stringify(acctReqs,
	function(key, val) {
		if (key == "password")
			return undefined;
		else
			return val;
	})
	+ ";");
});

管理インターフェースの安全性

次に対処する問題は、ハッカーが自身のリクエストを承認できてしまうという問題です。ハッカーはアプリケーションの URL に「approve/<e-メール・アドレス>」を追加するだけで、自身のリクエストを承認できてしまいます。この攻撃を防ぐ 1 つの方法は、クッキーを使用することです。管理者として、acct_approval.html を読み取る際にブラウザーのクッキーにランダムな値を設定します。こうすれば、アプリケーションは管理者によるリクエストの承認または却下の決定を受け取るときに、ブラウザーのクッキーに正しい値が設定されているかどうかをチェックすることができます。この方法では、正規の管理者だけがリクエストを承認または却下することができます (ただし、このアプリケーションをトレーニングの目的で作成したときのように、acct_approval.html が公開される場合はその限りではありません)。

クッキーの値を生成するために使用するのは、node-uuid パッケージです。他のパッケージと同じように、このパッケージを package.json に追加してから、変数を初期化する以下のコードを app.js の先頭近くに追加します。

var cookieName = "approval-req";
var cookieVal = require("node-uuid").v4();

注: 簡単にするために、上記のコードは静的な値を生成します。本番環境では、定期的にクッキーの値を変更するとしても、管理者がリクエストの承認または却下を検討している最中である場合に備え、数分間は古い値を使用できるようにしておいてください。

私たちは、acct_approval.html をブラウザーに返す関数を制御することはできないため、この関数にクッキーの変更を追加することはできません。public/ ディレクトリー内のファイルは express パッケージに含まれる express.static オブジェクトによって処理されるので、このオブジェクトを呼び出すコード行の前に別のミドルウェアを追加し、そのコード行に到達する前にクッキーを変更します。このミドルウェア/acct_approval.html のみに適用され、リクエストを処理する際の最後のステップではないことを意味する next() の呼び出しで終了します。

app.use("/acct_approval.html", function(req, res, next) {
	res.cookie(cookieName, cookieVal);
	next();
});

// serve the files out of ./public as our main files
app.use(express.static(__dirname + '/public'));

返されたクッキーの値を受け取るには、cookie-parser パッケージを package.json に追加し、このパッケージを使用するために以下の行を追加します。

app.use(require("cookie-parser")());

最後に、レスポンスを受け入れる 2 つの app.get() 呼び出しの中で、クッキーの有無をチェックします。

// Deal with approved accounts
app.get("/approve/:email", function(req, res) {
  if(req.cookies[cookieName] == cookieVal) {
    // Move the account from request to account list
    accts[req.params.email] = acctReqs[req.params.email];
    delete acctReqs[req.params.email];

    // Redirect to the account approval page
    res.redirect("../acct_approval.html");
  } else
    res.send("Nice try");
});

// Deal with accounts that are not approved
app.get("/decline/:email", function(req, res) {
  if(req.cookies[cookieName] == cookieVal) {
    // Delete the request
    delete acctReqs[req.params.email];

    // Redirect to the account approval page
    res.redirect("../acct_approval.html");
  } else
    res.send("Nice try");
});

アカウントの存在の露見

アカウントの存在が露見するのを回避するには、/login_attempt を処理する app.post() で、すべての障害に同じエラー・メッセージを使用するだけで対処することができます (6 行目から 9 行目)。これは、リクエストが存在するかどうかを気にかけないということを意味しています。

// Deal with logins
app.post("/login_attempt", function(req, res) {
  if (accts[req.body.email] != undefined) { // The account exists
    if (pwdHash.verify(req.body.password, accts[req.body.email].password))
      res.send("Login successful, welcome " + accts[req.body.email].name);
    else
      res.send("Bad user or password");
  } else
      res.send("Bad user or password");
});

アカウントの承認に重点を置くために無視しましたが、本番システムには以下の機能も含まれることになります。

  • 永続化。アプリケーションを再起動すると、アプリケーションの状態が完全に失われるため、すべてのアカウントとアカウントのリクエストは削除されます。そのため、本番システムでは acctsacctReqs の両方を MongoDB データベースに保管することになるでしょう。その方法は、「Bluemix と MEAN スタックを使用して自動投稿 Facebook アプリケーションを作成する: パート 2 ユーザー情報をサーバーに保管する」で説明しています。
  • パスワードの検証。通常、ユーザーはアカウントを作成するときに、パスワードを 2 回入力するよう求められます。入力ミスによって使用できないアカウントにならないようにするためです。パスワードの検証を実施するには Angular を使用し、パスワードが一致しない場合は ng-if を使ってエラーを表示するという方法を用います。詳細については、Angular Web サイトの ngIf の項目を参照してください。
  • e-メールの検証、パスワードのリセット、アカウントの通知。これらの機能にはいずれも、リンクに含めたランダム・トークンまたは何らかのテキストを記載した e-メールを送信することが必要になります。e-メールを送信するには、Bluemix SendGrid サービスを利用することができます。

まとめ

このチュートリアルを完了した今、皆さんはかなりセキュアでシンプルなアカウント承認システムを作成し、複雑なシナリオに合わせて変更できるようになっているはずです。極めて複雑なシナリオには、IBM Security Identity Manager (ISIM) を使用することができます。ISIM には、一般的に使用されているほとんどすべてのシステムでアカウントを制御するためのアダプターが用意されており、複合承認ワークフローにより、必要に応じて複雑なビジネス・プロセスを実装することができます。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=セキュリティ
ArticleID=1025495
ArticleTitle=アカウント承認を Node.js Bluemix アプリケーションで管理する
publish-date=01212016