TokyoFacebookTwitterHatenaPocket従来のWebアプリケーションなら簡単なGoogleやFacebookのソーシャルログインも、シングルページアプリケーションに実装するのは意外と大変。実装に戸惑った方へ、ちょっとしたコツを教えます。シングルページアーキテクチャーを使って開発されたWebアプリケーションを目にすることが多くなりました。このアーキテクチャーでは、アプリケーション全体がJavaScriptとしてブラウザーに読み込まれ、サーバーとのやりとりはすべてJSONドキュメントを返すHTTPベースのAPIを使って実行されます。したアプリケーションはユーザープロファイルを保存するときをはじめ、特定のユーザーに操作を限定する必要があります。この処理は従来のHTMLベースのアプリケーションでは比較的簡単に実装できましたが、シングルページアプリケーションではすべてのAPIリクエストを認証しなければならないため、難度が上がります。本記事では、さまざまなプロバイダーを利用したソーシャルログインをPassport.jsライブラリーで実装する方法を紹介します。ログイン後のAPIコールで用いるトークンベース認証の実装方法についても説明します。ソースコードはすべてGitHubリポジトリからダウンロードできます。SPAでソーシャルログインを使う理由Webアプリケーションにログイン機能を実装するときに検討する課題があります。認証をユーザーインターフェイスに実装する方法は?ユーザー情報を保存する方法は?ユーザー認証情報のセキュリティを確保する方法は?ログイン画面を書き始める前にこれらの課題を解決する必要がありますが、良い方法があります。ソーシャルネットワークをはじめとする多くのWebサイトが、プラットホームを利用してアプリケーションの認証ができる機能を用意しています。Connectなどがあります。ソーシャルログイン技術でログイン機能を実装する利点をまとめます。認証のためのユーザーインターフェイスの作成が不要ユーザー機密情報の保存やセキュリティを確保する責任から解放されるユーザーは単一のアカウントで複数のWebサイトにログインできるパスワードの漏洩が疑われる場合、ユーザーはパスワードを一度再設定すれば多数のWebサイトに適用できる認証機能を提供するサービスを利用するとほかの情報も入手できることが多い。この情報は、当該Webサイトを一度も利用したことがないユーザーを自動的に登録したり、ユーザーの代わりにプロファイルを更新したりするために利用できるAPIにトークンベース認証を使う理由クライアントがAPIにアクセスしたときに、アクセスを許可するのか判断します。判断する主な方法は以下のとおりです。セッションベース認証クッキーベース認証トークンベース認証セッションベース認証では、APIサービスがセッションとクライアントを結びつけます。簡単にセットアップできますが、APIを複数のサーバーにデプロイする場合は大変です。サーバーのセッション管理メカニズムやセッションの期限切れのコントロールができない場合もあります。クッキーベース認証ではIDをクッキーに保存し、APIリクエストを自動的に識別します。最初にクッキーを設定する仕組みが必要です。クッキーは同じホストに対するすべてのリクエストに自動的に含まれるため、以降のリクエスト発行時にIDが漏洩する危険があります。トークンベース認証はクッキーベース認証の派生形ですが、より多くのコントロールができる方法です。トークンを生成し、リクエストにインクルードします(通常は「Authorization」ヘッダー、または直接URLに含めます)。トークンを思いのままにリクエストに格納できるのです。注記:HTTPヘッダーは「Authorization(認可)」という名前がついていますが、実際はヘッダーを使って「authentication(認証)」をします。クライアントが「なにを」許可されているかを確認するのではなく、クライアントが「誰か」を確認するので認証です。トークンの生成にどのストラテジーを用いるかも重要です。トークンには参照(reference)トークンと完全(complete)トークンがあります。参照トークンはサーバーが情報を検索するために用いるIDに過ぎません。完全トークンには必要な情報すべてが含まれています。参照トークンはセキュリティ面で有利です。ユーザー認証の情報がクライアントに漏れる心配がありません。リクエストのたびにトークンを処理して実際の認証情報を得るためパフォーマンスを犠牲にします。完全トークンは逆で、トークンの内容を理解できる相手にユーザー認証の情報を知られる恐れがあります。トークンに情報がそろっているので認証情報の取得に伴うパフォーマンスの低下はありません。Tokens規格を用いて実装されることが多いです。JWTはトークンを暗号を使って署名するためトークンの改ざんを確実に防止できます。トークンの暗号化のオプションも用意されていて、暗号化キーがなければトークンのデコードさえ不可能です。TokensをNode.jsで使うを確認してください。完全トークンの欠点はデータの多さです。参照トークンは、UUIDで実装すれば36文字です。対して、JWTは数百文字に達します。この記事ではJWTトークンを使います。実際は参照トークンと完全トークンのどちらを使うか、トークンを使うための仕組みを各自検討してください。PassportとはPassportはWebアプリケーションに認証機能を実装するためのNode.js用モジュール群です。Node.jsベースのWebサーバーに簡単に導入でき、モジュール構造でログイン機能を実装するためコード量を最小限に抑えられます。Passportは幅広い認証の要件に対応できる強力なモジュールスイートです。さまざまなエンドポイントの異なる認証要件に合わせてモジュール構成を柔軟に変えられます。Passportはサードパーティのプロバイダーに関する作業をすべて実行するので、認証システムはURL内の特定の値を確認するだけです。この記事ではpassport-google-oauth、passport-facebook、passport-jwtモジュールを利用します。ソーシャルログインとAPIエンドポイント用のJWTトークンベース認証を実装します。passport-jwtモジュールは特定のエンドポイント(今回はアクセスする際に認証が必要なAPIエンドポイント)へのリクエスト内に有効なJWTを格納するために使います。passport-google-oauthモジュールとpassport-facebookモジュールはGoogle認証、Facebook認証のためのエンドポイントを設置し、アプリケーションのほかのエンドポイントにアクセスする際に用いるJWTを生成するために使います。SPAにソーシャルログインを実装する単純なシングルページアプリケーションにソーシャルログインを実装します。アプリケーションはExpressで書かれていて、セキュアなエンドポイントとセキュアでないエンドポイントを1つずつ提供する単純なAPIを持っています。ソースコードはhttps://github.com/sitepoint-editors/social-logins-spaで確認できます。installを実行すればアプリケーションがビルドされ依存オブジェクトがすべてダウンロードされます。src/index.jsを実行すればアプリケーションが起動します。アプリケーションを使うにはGoogleとFacebookに登録してソーシャルログインの認証情報を取得し、アプリケーションで利用できるようにします。完全な手順はデモアプリケーションのREADMEファイルにあります。これらの認証情報を環境変数として利用できるようにします。アプリケーションを実行します。Tokenを使った)トークン認証を追加して、GoogleとFacebookを使ったソーシャルログインを追加し、アプリケーションの以降の処理で使用するトークンを取得できるようにします。一度ソーシャルプロバイダーで認証してから、生成したJWTを使ってアプリケーションのAPIを呼び出します。JWTは情報をすべて内部に格納しつつセキュリティも確保しているため、今回のシナリオに最適です。JWTはJSONペイロードと暗号化された署名で作られています。ペイロードには認証されたユーザー、認証システム、トークンの有効期間についての情報が格納されています。署名鍵でしかトークンを生成できないため、ペイロードが悪意のある第三者がねつ造したものではないことを署名で確認できます。アプリケーションにインクルードされているconfig.jsモジュールをたびたび参照します。このモジュールはアプリケーションのプロパティを格納するためのモジュールで、Node-convictモジュールを外部から読み込み、構造に利用します。今回使用するプロパティは以下のとおりです。http.port:アプリケーションを実行するポート。デフォルトは3000で環境変数「PORT」で上書きするauthentication.google.clientId:Google認証に使うGoogleのクライアントID。環境変数「GOOGLE_CLIENTID」でアプリケーションに渡すauthentication.google.clientSecret:Google認証に使うGoogleのクライアントシークレット。環境変数「GOOGLE_CLIENTSECRET」でアプリケーションに渡すauthentication.facebook.clientId:Facebook認証に使うFacebookのクライアントID。環境変数「FACEBOOK_CLIENTID」でアプリケーションに渡すauthentication.facebook.clientSecret:Facebook認証に使うFacebookのクライアントシークレット。環境変数「FACEBOOK_CLIENTSECRET」でアプリケーションに渡すauthentication.token.secret:認証トークンのJWTを署名するために使うシークレットキー(secret)。デフォルトは「mySuperSecretKey」authentication.token.issuer:JWT内に保存される発行元(issuer)。複数のアプリケーションを1つの認証サービスでまかなうときに、どのサービスがトークンを発行したかを示すauthentication.token.audience:JWT内に保存されるオーディエンス(audience)。複数のアプリケーションを1つの認証サービスでまかなうときに、トークンがどのサービスを対象としているかを示すPassportの組み込みPassportをアプリケーションで使うためのセットアップ作業は、モジュールをインストールし、Expressアプリケーションでミドルウェアを初期化するだけです。この段階で必要なモジュールはpassportです。Expressアプリに追加するすることでミドルウェアをセットアップできます。require('passport');.....app.use(passport.initialize());PassportのWebサイトに記載されている手順はpassport.session()を使ってセッションサポートをセットアップしていますが、今回のアプリケーションではステートレスAPIを実装し、認証をセッション期間中持続させずリクエストごと認証するためセッションサポートは使いません。従ってセットアップは不要です。セキュアなエンドポイントにJWTトークン認証を実装するJWTトークン認証はPassportを使うことで簡単にセットアップできます。passport-jwtモジュールが面倒な処理をすべて実行します。passport-jwtモジュールは値が「JWT」で始まる「Authorization」ヘッダーを探し、ヘッダーの残りの部分をJWTトークンとして認証に使います。JWTをデコードし内部に保存された値を利用(たとえばユーザーの検索)できるようにします。署名が無効だったり、トークンの期限切れだったりといった理由でJWTが無効なら、コードに追加処理を書かない限り、リクエストは認証されません。JWTトークン認証を設定します。done();}));2つの内部モジュールを使っています。config.js:アプリケーション全体で使用するプロパティが格納されている。これらのプロパティにはすでに値が設定済みであり、値はいつでも利用できるusers.js:アプリケーションのユーザーストア。ユーザーの読み込みや作成に使う。ここでは内部IDを使ってユーザーを読み込んでいる既知のsecret、issuer、audienceを使ってJWTデコーダーを設定し、AuthorizationヘッダーからJWTを取得するストラテジーを作成しています。issuerまたはaudienceがJWT内に格納されたものと一致しない場合は認証に失敗します。単純な方法はありますが、これも偽装対策の1つです。トークンのデコード処理は最初のトークン生成時に使った設定値と同じ値を渡すだけでpassport-jwtモジュールが一手に引き受けます。JWTは規格の1つなので、この規格に従うモジュールはすべて互換性を持っています。デコードに成功したトークンは、ペイロードとしてコールバックに渡されます。トークンの「subject」でユーザーを特定します。実際のアプリケーションでは、トークンが無効かの確認などチェック処理を追加する必要があるかもしれません。ユーザーが見つかったらPassportに渡します。Passportはユーザーをreq.userとして処理し、以降のリクエストで利用できるようにします。ユーザーが見つからない場合はPassportにユーザーを渡さないため、認証が失敗したものと判断します。ユーザーとリクエストハンドラを紐づけし、認証が必要なリクエストが成功しました。});3行目はPassportにリクエストを処理させるおまじないで、設定済みの「jwt」ストラテジーを実行します。成功するとリクエストを発行します。src/index.jsでアプリケーションを実行し、リソースにアクセスを試みると、実際の動作を確認できます。});Authorizationヘッダーになにも渡していないので、処理に失敗します。有効なAuthorizationヘッダーを渡せば、正常なレスポンスを受け取ります。{"id":0,"name":"Graham","providers":[]}テストを実施するために、https://www.jsonwebtoken.ioのフォームに入力してJWTを手作業で生成しました。使用した「Payload」は以下のとおりです。Key」はプロパティの「mySuperSecretKey」を使います。トークン生成のサポート有効なトークンでリソースにアクセスできるようになったので、次はトークンを実際に生成します。jsonwebtokenモジュールを使って適切な情報を格納したJWTを作成し、上で使ったものと同じキーで署名します。token;}JWTを生成するときは、audience、issuer、secretをプロパティの設定値と同じ値にする必要があります。JWTの期限は1時間に指定します。アプリケーションにとって適切だと考えられる任意の期間を設定します。簡単に変更できるようにプロパティファイルから読み出すこともできます。IDを指定していませんが、指定すれば固有のIDを持つトークン(例:UUIDを使ったトークン)を生成できます。IDがリストにないことをチェックできます。ソーシャルログインプロバイダートークンを生成できたので、次はユーザーが実際にログインするための方法を実装します。ここでソーシャルログインプロバイダーの出番です。ユーザーをソーシャルログインプロバイダーにリダイレクトさせる機能を追加し、ログインに成功した場合はJWTトークンを生成してブラウザーのJavaScriptエンジンに渡し、以降のリクエストで使います。必要なピースはほぼ揃っているので、つなぎ合わせるだけです。Passportでソーシャルログインプロバイダーを使う手順は2つに分かれます。第一に、プラグインでPassportに設定を追加し、ソーシャルログインプロバイダーを利用します。第二に、認証を始めるためにユーザーを移動させ、認証が成功したときにユーザーをリダイレクトさせるためのExpressのルートを用意します。これらのURL(ルート)をブラウザーの子ウィンドウで開きます。ウィンドウは完了後に閉じます。開いたときにJavaScriptのメソッドを呼び出します。この方法はユーザーにログイン処理を意識させません。認証情報を入力するウィンドウが表示される程度で、設定によってはログインに完了したこと以外はユーザーから見えません。ブラウザーはポップアップウィンドウのビューと、メインウィンドウの2つです。ビューを処理するJavaScriptはフレームワークを使えば簡単に作成できますが、今回は単純さを考慮して素のJavaScriptを使います。JavaScriptのメインページは以下の通りです。'/start');}アクセストークンを格納するグローバル関数オブジェクト(authenticateCallback)をwindowに登録し、認証を開始するためのルートであるapi/authentication/{provider}/startにアクセスします。この関数は認証を開始する方法に合わせて任意の方法で起動できます。通常はヘッダーエリアに設置したログインリンクをクリックしたときに起動しますが、詳細はアプリケーションにより変わります。2つ目のパートは認証の成功時に描画されるビューです。今回はMustacheを使用していますが、好きなビュー技術で構いません。</body></html>JavaScriptは、ウィンドウのオープナー(つまりアプリケーションのメインウィンドウ)のauthenticateCallbackメソッドを呼び出し、閉じているだけです。JWTトークンをアプリケーションのメインウィンドウで任意の目的に利用できるようになりました。Google認証の実装Google認証を実装するにはpassport-google-oauthモジュールを使います。以下の3つの情報が必要です。Consoleに登録すると入手できます。リダイレクトURLはアプリケーション内のURLで、ユーザーがGoogleの認証情報を使ってサインインしたあとに戻ってくる場所です。リダイレクトURLはアプリケーションをどこに、どのようにデプロイするかによって異なりますが、今回はとりあえずハードコードします。Google認証のためのPassport設定は以下の通りです。}));}ユーザーが認証を成功させたあとにリダイレクトされて戻ってくるのに合わせて、Googleのシステム内のユーザーIDと、ユーザーのプロファイル情報の一部が提供されます。このユーザーが以前ログインしたことがあるかを確認します。ログインしたことがあればユーザーの記録を取得し処理を完了します。ログインしたことがなければアカウントを登録し、以降の処理で利用します。このメカニズムのおかげでユーザーは初回ログイン時におけるユーザー登録を意識する必要がないのです。ここでは触れませんが、こうしたトランスペアレントな(利用者にその存在が意識されない)ユーザー登録を実現する方法はいろいろあります。次に、ログインを管理するためのルートハンドラーをセットアップします。generateUserToken);ルート/api/authentication/google/startと/api/authentication/gogle/redirectですが、前述のとおり/startはアプリケーション側から開くURLで、/redirectはログインの成功時にGoogleがユーザーをリダイレクトするURLです。上記の認証完了ビューを開いて生成されたJWTをビューに渡します。最初のソーシャルログインプロバイダーを用意できたので、2つ目を追加します。Facebook用にpassport-facebookモジュールを使います。このモジュールはGoogleモジュールとほとんど同じ動作をし、必要なプロパティやセットアップ方法も同じでが、URL構造が異なります。Facebook認証を設定するには、Google認証のときと同じようにクライアントID、クライアントシークレット、リダイレクトURLが必要です。ConsoleでFacebookアプリケーションを作成すると入手できます。Login」プロダクトを追加します。Facebook認証のためのPassportの設定は以下の通りです。}));}「facebook」の文字だけが異なりますがGoogleとほぼ同じです。以下に示すように、URLルートも似ています。generateUserToken);スコープはデフォルト設定で十分です。スコープを指定した場合、GoogleとFacebookの設定はほぼ同じになります。最後にソーシャルログインプロバイダーを利用すると、ユーザーのログインや登録機能を簡単に、短時間で追加できます。ユーザーをソーシャルログインプロバイダーにリダイレクトし、さらにアプリケーションに戻すという処理は、従来型のアプリケーションでは簡単ですが、シングルアプリケーションと組み合わせるのは意外と大変です。本記事ではソーシャルログインプロバイダーをシングルアプリケーションと組み合わせる方法について説明しました。みなさんが将来利用したいと考えているプロバイダーにも簡単に応用できるものであると信じています。Passportにはさまざまなプロバイダーと連携するためのモジュールが多く用意されているので、目的に合ったものが見つかるはずです。これらのモジュールは本記事で紹介したGoogleやFacebookの認証と同じ方法で利用できます。Kolceが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。Coxイギリス出身のソフトウェア開発者です。15年近い経験があり、主な使用言語はJavaとJavascriptですが、ほかにも幅広い言語をカバーできます。