次の方法で共有



June 2016

Volume 31 Number 6

働くプログラマ - MEAN あれこれ: Passport

Ted Neward | June 2016

Ted Neward「MEANers」の皆さん、お帰りなさい。

これまで、サーバー側の仕事をたくさんこなしてきました。そろそろ、クライアント側の仕事に移りましょう。とは言え、仕事を完全に移してしまう前に取り上げておかなければならないことがもう 1 つあります。それは「ユーザーをサポートできるようにする」ことです。(現時点では、すべてではありませんが) ほとんどのアプリケーションには、ユーザーの ID を確立するためになんらかのユーザー認証メカニズムが必要です。これは通常、ユーザーに表示するデータや、ユーザーがシステム内で実行できるオプションを制限するためです。

自作するという選択肢にはいつも心ひかれますが、Node コミュニティにおいては時代遅れです。このようなジレンマの最適解は「常に npm を使用すること」です。今回の場合、認証システムに広く採用されていて、最も優れているのが「Passport」という Node.js ライブラリです。

Passport

Passport ライブラリを使用するにあたっての最初の手順はもうおわかりでしょう。npm パッケージ名を調べて「npm install」を実行します。npm パッケージ名は、オンラインの npm レジストリを検索するか、Passport のホームページを参照して見つけます。初めて PassportJS.org にアクセスするとき気付くことが 2 つあります。1 つは、これまで作成されてきたあらゆる Node.js パッケージのホームページとは対照的に「npm install」コマンドがフロント ページにないこと、もう 1 つは Passport には「strategy」という重要な概念があることです。

strategy が存在する理由は単純で、「ここでユーザーの資格情報を認証しましょう」という宣言はあいまいだからです。認証に使用する資格情報はたくさん存在するだけでなく、サーバーのように、ユーザーが認証する資格情報ストアも 1,000 種類を超えます。Passport の目的は、あらゆる資格情報ストア (Facebook、LinkedIn、Google、独自のローカル データベースなど) に、どのような認証を行う場合でも機能するソリューションとなることで、JSON Web Token を通じた username/password から、HTTP Bearer ヘッダーまで、考えうるすべての資格情報を使用します。

つまり、Passport は単なる 1 つのパッケージではありません。passport コア パッケージと、Passport が実際の認証を行う方式を指定する strategy があります (本稿執筆時点で 307 種類に上ります)。どの strategy (後ほど扱いますが、複数の strategy でもかまいません) を選択するかによって、実際に必要なパッケージが決まるので、結果的にインストールするパッケージも決まります (とは言え、Passport は他の関係する strategy が使用する「passport」コア パッケージを定義するので、「npm install --save passport」を実行すれば、strategy の詳細を知る必要はありません)。

Hello, Local

(特に、内部のユーザー データベース/資格情報ストア向けに構築されているシステムにとって) ごく一般的な strategy は「local」 strategy です。これは「クライアントが送信したユーザー名とパスワードを、格納されているさまざまなユーザー名とパスワードと比較する」という古典的な機能を実行します。他にもっとセキュリティの高い strategy は間違いなくありますが、出発点としては優れています。

現時点では、シリーズを通して作成してきたコードに、いかなる認証も存在しません。したがって、ユーザー名とパスワードを固定でハードコーディングして、わかりやすくすることにします。データベースとの照合を行うコードが比較を実行する場所は、Passport のしくみを知ってしまえばかなりわかりやすくなるので、ここでは扱いません。

passport-local strategy を使用することにしたので、Passport の必要な要素を含めるために「npm install --save passport-local」から開始します (前にも出てきた「--save」引数は、正式な依存関係として自動的に追跡されるよう、passport-local strategy をパッケージ マニフェスト ファイルに格納します)。

インストールが完了したら、3 つのことを実行する必要があります。まず、特定の strategy を使用するよう Passport を構成します。次に、ユーザーが認証要求を送信する HTTP URL ルートを確立します。そして、ユーザーが当該の HTTP URL に実際にアクセスするには認証が必要となるように Express ミドルウェアをセットアップします。

構成

まず、最初に Passport がアプリケーションに読み込まれるようにします。passport と passport-local が既にインストールされているとして、いつもの require を使ってそれを app.js スクリプトに読み込みます。

var express = require('express'),
  bodyParser = require('body-parser'),
  // ...
  passport = require('passport'),
  LocalStrategy = require('passport-local').Strategy;

LocalStrategy の設定がわずかに異なっています。前に出てきた MongoClient のように、require 呼び出しで返されるオブジェクトから「Strategy」フィールドにアクセスした結果を、LocalStrategy に代入しています。これは Node.js では一般的ではありませんが珍しいことでもありません。今回の場合の LocalStrategy は、インスタンスを作成するクラスの一種 (または、通常 JavaScript に可能な限り近いクラスの一種) として機能します。

また、以下のように、Passport が機能していることを Express 環境に通知することも必要です。

var app = express();
app.use(bodyParser.json());
app.use(passport.initialize());

initialize 呼び出しはだいたい見てのとおりで、着信する要求を Passport が受け取れるよう準備します。ユーザーごとのセッションをセットアップするために、ASP.NET で見られるような passport.session の呼び出しに似たものが使用されることがよくあります。ですが、ここで構築しているような HTTP API において、これはあまり必要でも、適切でもありません (後ほど説明します)。

課題

次に、Passport が認証要求を受け取ったときに呼び出すコールバックを確立する必要があります。このコールバックは、ユーザーを探し、渡されるパスワードを検証します (より実際的なシナリオでは、ユーザーを探して、salt 化されたパスワード ハッシュが、現在データベースに格納されている salt 化されたパスワード ハッシュと同一かどうかを検証しますが、これは明らかに Passport の範囲を超えています)。このため、passport.use を呼び出して、使用する Strategy のインスタンスをコールバックが埋め込まれた状態で渡します (図 1 参照)。

図 1 コールバックの確立

passport.use(new LocalStrategy(
  function(username, password, done) {
    debug("Authenticating ",username,",",password);
    if ((username === "sa") && (password == "nopassword")) {
      var user = {
        username : "ted",
        firstName : "Ted",
        lastName : "Neward",
        id : 1
      };
      return done(null, user);
    }
    else {
      return done(null, false, { message: "DENIED"} );
    }
  }
));

ここで何が行われているかを説明します。まず、コールバックが呼び出されるときには Passport は既に、着信する要求を解析し、このコールバックに渡すユーザー名とパスワードを抽出しています。LocalStrategy については、Passport はこれらの値がそれぞれ username と password というパラメーターを通じて渡されていると仮定します (もしこれが許容できない場合は、LocalStrategy の構築呼び出しで構成することが可能です)。

また、実際の検証のメカニズムは完全に Passport の管轄外になっています。Passport は strategy が検証を実行すると見なし、今回は「local」strategy がアプリケーション コードにそれを完全に委ねます。この例ではハードコーディングされた値を照合するだけですが、渡されたものと username が一致するユーザーに対して Mongo ルックアップを実行しパスワードを照合するというのが通常のやり方です。

そして、ここでは通常の Node.js ミドルウェア方式を踏襲し、done 関数によって success または failure をシグナル状態にして、それと同時に success と failure のどちらが発生したか示すパラメーターを渡しています。success が意味するのは、2 つ目のパラメーターが Express 要求オブジェクト内に配置されるユーザー オブジェクトということで、このオブジェクトはさらにパイプラインに渡されます。また failure は、Express が 401 応答 (「未承認」) を返すよう Passport に通知し、UI に対する「flash」メッセージに通常使用される failure メッセージを含むことも可能です (flash メッセージが使用されていない場合、メッセージは事実上捨てられます)。

結果

今や残すところは、以下のように、認証が実行されるルートを構成するだけになりました。

app.post('/login',
  passport.authenticate('local', { session: false }),
  function(req, res) {
    debug("user ", req.user.firstName, " authenticated against the system");
    res.redirect("/persons");
  });

Passport は、実際に認証を実行する URL パターンに特別敏感ではありません。/login は単なる規則で、/signin、/user/auth、またはその他多数のパターンもまったく問題なく機能します。鍵は、このルートを解決する際の最初の手順です。それは、「使用する strategy (local)」、「ユーザーごとのセッション Cookie を使用するかどうか」(既に説明したとおり、これは API にはあまり適していません)、および「認証が成功したときに呼び出す実際の関数」を渡して、passport 認証関数を呼び出すことです。この場合、その関数はデバッグするメッセージを単に記録して、データベースに格納されている Persons の一覧にユーザーをリダイレクトします。

フォーム ポスト コンテンツを渡すか JSON コンテンツを送信することでテストが可能になりました。これは API なので、次のように JSON パケットで送信する方がおそらく適切かつ簡単です。

{ "username" : "sa" , "password" : "nopassword" }

username と password が一致したら、success と、/persons への 302 リダイレクトが返されます。一致しない場合は 401 応答が返されます。テストの結果、機能することがわかりました。

トラフィックのリダイレクト

事実、成功した認証がユーザーを特定のルートに導く、または失敗によってユーザーを新しいページに導くというのは (Express を使用して従来のサーバー側の Web アプリケーションを構築する際に) 一般的なパターンです。このため、以下のように Passport では、認証のコールバックにさらにシンプルなアプローチを取ることができます。

app.post('/login',
  passport.authenticate('local', { successRedirect: '/',
                                   failureRedirect: '/login',
                                   failureFlash: true })
);

ここでは、成功したとき Passport は自動的に「/」 URL にリダイレクトし、失敗したときは「/login」 URL に戻ります。また、(この場合) ユーザーが正常にサインインできなかったことを示すフラッシュ メッセージも表示されます。

API の場合、より一般的なのは、表示や編集のためにクライアントにユーザー オブジェクトの JSON 表現を返すことです。ただし、セキュリティ関連の情報や機密情報 (特にパスワード) を含めて返すしてはいけません。ブラウザーは、クライアント側のデバッグ ユーティリティを提供するのにはたいへん便利です。ところがそれゆえに、攻撃者はいとも簡単に、ブラウザーのメモリに格納されているユーザー オブジェクトにアクセスして心ゆくまで編集することができてしまい、問題になります (これらの JSON オブジェクトも、多くの Node.js ベースの API システムが HTTP でなく HTTPS 経由で実行されるよう、転送中に改ざんされることが可能です。さいわい多くの場合、HTTP ではなく HTTPS で実行するよう Express を構成するのは、プログラム上の変更というよりはクラウド構成の操作になります)。このため、パスワードはサーバーから決して離れてはならず、「役割」(役割ベースの承認システムの場合) は、request が渡したユーザー オブジェクトではなく、常にデータベースから確認する必要があります。

ただし、先ほど述べたように、現時点では API クライアントは「/login」ルートにヒットしたとき必ず認証資格情報を渡す必要があり、資格情報はその他のルートでは確認されません。すべてのルートに認証チェックを配置することも確かに可能ですが、あらゆるメソッド呼び出しで資格情報を渡すのはあまり気が進みません。

代替策

Passport では、この問題に「確実に」対処していると主張されています。

まず、戻ってセッションを有効にすることはいつでも可能です。セッションが有効になると、Passport は一意識別子を作成し、HTTP 応答で Cookie としてそれを返します。クライアントは、その Cookie を、後に続く各要求の一部として返すよう求められます。この時点における、サーバー側での主な要求は、Passport ライブラリがユーザー オブジェクトと識別子を相互に変換する方法を把握していることです (これは、ユーザーの「シリアル化」および「シリアル化解除」といいます)。また以下のように、それらの Passport のエンドポイント 2 つのメソッド コールバックをセットアップすることも要求されます。

passport.serializeUser(function(user, done) {
  done(null, user.id);
});
passport.deserializeUser(function(id, done) {
  User.findById(id, function(err, user) {
    done(err, user);
  });
});

serializeUser 関数は Passport にユーザーの一意識別子を提供するようになっているので、一意識別子を user.id フィールドから取り出しています。また deserializeUser 関数はその逆を実行するので、渡される id を、ユーザー オブジェクト全体を探すデータベース照合の主キーとして使用しています。

strategy のセッションは、すべてとはいかないもののほとんどを有効にできます。ただし通常は、ブラウザーが直接解釈する必要がある HTML をサーバーが生成するときに機能します。API は、Cookie とあまり適切に連動しない傾向があります。特に、API はブラウザーベースのクライアントと同じくらい頻繁に (またはそれよりも多く) ネイティブのモバイル アプリ クライアントからアクセスされるためです。

また、2 つ目のアプローチでは、クライアントとサーバー両方の「既知のシークレット」に依存する、別の strategy を使用します。このシークレットはさまざまな方法で渡すことができます。場合によっては、発行された「API キー」の既知のセットをシステムが保持していて、そのキーを各要求の一部として提供する必要があります。これは、多くのサードパーティの REST サービスで非常に一般的ですが、キーを取得できた攻撃者は、クライアントがキーをリセットするまでクライアントを装うことができるという弱点があります。Passport は、このための strategy を提供します。実行するのは「npm install --save passport-localapikey」というコマンドです。これは、local strategy とほぼ同じように動作しますが、strategy 認証メソッドがデータベースから username と password ではなく API キーを探します。

同様のアプローチとして、JSON Web Token (JWT) を活用するものもあります。これはより安全ですが、説明するにはスペースが足りません。このアプローチをプロジェクトに導入するには「npm install --save passport-jwt」というコマンドを実行します。JWT は、さまざまなデータ要素がパッケージ化されたもので、そのうち 1 つは (API キーやパスワードのように) 共有シークレットにすることができますが、特定の発行者や対象ユーザーなどに対する検証を実行可能です。

または、どのような資格情報も格納せず、サードパーティのシステム (Facebook、Google、Twitter、LinkedIn など、その他数百個の人気サイト) に認証を頼ってもかまいません。Passport はこのシナリオもサポートしていて、これらのサイト 1 つ 1 つに特定の strategy が用意されています。また、標準化された OAuth 2.0 (および、OpenID を使用するサイト向けの OpenID) の strategy もあります。

つまり、認証システムを実装するならば、Passport に定義済みの strategy があるということです。「npm install」を実行して、構成をセットアップし、Express ルートに承認呼び出しを配置すれば完了です。

ちなみに、これらのあらゆる認証の問題に単一アクセス制御点を提供するサービスがインターネット上に存在するので、ぜひ知っておいてください。このようなサービスは「Authentication-as-a-Service」(サービスとしての認証) といい、人々が定期的に使用するサイトが普及し、管理の手間がさらに大きくなっているため、広がりを見せています。個人的なお気に入りの 1 つ「Auth0」 (実は、元マイクロソフト技術者が数人在籍しています) は、Passport プロジェクトのスポンサーで、そのアイコンとロゴは Passport のサイトのあちこちに目立たないように配置されています。プロジェクトでどのような認証を実行するか (従来のシステムを使用したり、Facebook や Dropbox と統合したりするなど) がまだ決まっていない場合、こちらをぜひチェックしてみてください。

まとめ

Passport は、言語やプラットフォームを問わず、史上最も成功した認証プロジェクトと言えます。必要な認証の「フック」を提供するだけでなく、必要に応じて実際の認証方式を制御できます。また、何かを加えることも、複雑な処理を実行することも可能です。strategy のアプローチが意味するのは、無限に拡張可能で、20 年後に出現するかもしれないなんらかの新しい認証スキームにも対応できます (本気です。この JavaScript は、20 年後も生きているでしょう。注目していてください)。

ですが Passport は、実際には実行しない機能を持っていると見なされています。実のところ Passport は、役割ベースの承認というアイデアに完全に賭け、どのような暗号化も実行しません。Passport は単に資格情報を確認するだけのもので、そうすると名前の由来がずっとわかりやすくなります。たとえば私がヨーロッパに旅行するとして、米国国民であることを証明するためにパスポートを見せる必要があるように、Passport はユーザーがシステム内の優良な「市民」ということを証明してもらうために資格情報の表示を求めます。

また、スペースと時間が尽きてしまいました。ひとまずは、コーディングを楽しんでください。


Ted Neward は、シアトルを拠点に活躍している、ポリテクノロジーに関するコンサルタント、講演者、および指導者です。これまでに 100 本を超える記事を執筆している Ted は、F# MVP で、さまざまな書籍を執筆および共同執筆しています。仕事への協力を依頼する場合、連絡先は ted@tedneward.com (英語のみ) です。また、blogs.tedneward.com (英語) でブログを公開しています。

この記事のレビューに協力してくれた技術スタッフの Shawn Wildermuth に心より感謝いたします。