Chapter 7
セキュリティと認証・アクセス権

この章は、INTER-Mediator Ver.14をもとに記載しました。

この章が目指す最も重要な目標は、作成するアプリケーションのセキュリティを確保することです。もちろん、手軽にできるのであれば長々と説明する必要はありません。しかしながら、INTER-Mediator自身のさまざまな設定に加えて、INTER-Mediator外のデータベースやサーバーOSといった部分にも注意が必要です。この章では、全体的なセキュリティ設定に加えて、データベースエンジンで確保すべき動作と、INTER-Mediatorのソリューションに対する設定などを説明します。

7-1Webアプリケーションセキュリティの前提

INTER-Mediatorによるシステムを作成した場合の、サーバーやクライアントにおいて、何を前提としてセキュリティ設計するべきかをまず最初にまとめておきます。

INTER-Mediatorを稼働するサーバーの前提条件

 INTER-Mediatorで作成したソリューションを稼働するには、原則として何らかのサーバーが必要です。すでにこれまでに紹介している通り、Dockerコンテナでの稼働も可能ですが、サーバー機での運用、クラウドサービス上のインスタンス、VPS、レンタルサーバーなど、さまざまな形態で稼働させることができます。ここでのセキュリティ上の原則は、「管理者以外はログインできない」そして「サーバーは健全に稼働している」ということです。

 管理者であれば、ログインできて、ファイルの内容を参照したり、ファイルを書き換えたりができますが、そうでないユーザーはログインができないということが大原則になります。この原則が守られていて、サーバーが意図した通りに稼働していれば、PHPのファイル、つまり定義ファイルや、あるいは別の設定ファイルなどにデータベース利用のための「パスワードを記述する」ということをしても、パスワード自体は外部に漏れません。

 また、以前にはよく見られたトラブルとして、PHPが稼働していないときにWebブラウザーから.phpファイルを開くと、PHPのプログラムが丸見えになるようなことがあります。この時、もちろん、プログラム自体に何かのパスワードが書かれていれば、見えてしまい、パスワードの漏洩が発生します。しかしながら、これは意図した動作ではありません。そうならないように稼働させるのが管理者の役割であり、あるいはプロバイダの役割でもあります。「Webサーバーは生きているのにPHPだけ落ちる可能性がある」という指摘もあるかもしれませんが、問題がある状況を前提にするのは、かなり信頼性が疑われるシステムであり、このようなシステムはそもそも業務の実運用に利用するべきではありません。通常の「稼働していることが前提」のシステムは、問題があればWebサーバーを停止させるのが原則であり、言い換えれば、問題があれば一切の機能を落とすくらいの作業をしなければ、「PHPのソースが見える」だけでなく、さまざまな悪影響が出てしまう可能性があります。これは、サーバー運用のポイントでもあります。

 結果的には、サーバーでは、管理者が意図したサービスのみをクライアントに提供している状態にするのが原則です。例えば、WebサーバーでTLS(一般にはSSLと言われますが、この章ではSSLの後継規格である「TLS」を用います)でしか接続させていないというのであれば、そのサーバーはネットワークから見ると、443番ポートしか開いていないというのが原則です。管理者がそのようにセットアップをして、第三者による変更を許さないようサーバー管理者が責任を持つ、という前提条件でWebアプリケーションは成り立っています。

INTER-Mediatorを利用するネットワークの前提条件

 Webアプリケーションなので、ネットワーク上に存在しているということは当然なのですが、業務系のアプリケーションの場合、文字通りの意味で世界中のネットワークを通るわけではなく、場合によっては、組織内のクローズドなネットワークだけを通してのアプリケーションの利用に限られていることもあります。状況に応じて脅威は変化し、対策も変わってきます。

 しかしながら、アプリケーションの開発者や利用者にコントロールできるのは、TLSの利用をするかどうかという点が一番大きいでしょう。また、アプリケーションによっては認証の利用やアクセス権(認可)の設定が必要かどうかを検討するかもしれません。認証やアクセス権については、この章の大きな目的であって各所で説明があるので、ここではTLSについての基本的なスタンスを説明します。

 執筆時点で最新規格であるTLS 1.3は、送信者と受信者以外に通信内容を取り出せないように暗号化通信を実現できます。その前の規格のTLS 1.2の段階では「適切な暗号スイートを利用すれば」と但し書きが必要でしたが、TLS 1.3は安全な暗号スイートに絞られており、設定のミスで危険性を高める可能性は低いと言えます。TLSを利用すれば、暗号化した通信内容を解読するには極めて長い時間を要することから、将来致命的な弱点が見つからない限り、現実的な時間での解読はほぼ困難と言えるでしょう。なお、SSLについては攻撃手段が発見されて、通信の傍受の可能性は0ではなくなったので、利用する仕組みとしては現在は既に除外されています。

 TLSで守られるとしたら、どういう状況でしょうか? まず、クライアントとサーバーでは、通信前後に暗号化が解除される段階があるので、TLSによって、すべてが守られるわけではありません。一方、Wi-Fiの電波を解析したり、Ethernetに流れる通信結果を取り出しても前述の通り解析は事実上不可能ですので、漏洩はTLSを使う限り原則的には発生しません。通信内容を読み取られたとしても通信経路上のデータは暗号化されているからです。また、サーバーとクライアントの間には、たくさんのルーティングの機材(ルータあるいはホスト)が介在しますので、その機材を通る間のパケットを傍受される可能性もあります。しかし、TLSの場合は、中間で暗号化の解除は通常はできませんので、この脅威も取り除かれることになります。

 では、組織内のネットワークしか通過しないのなら、TLSは必要でしょうか? これは、組織内のネットワークが正しく運用されているかと、組織内のスタッフを信用できるかどうかに関わってきます。「社内だから大丈夫」と思ったら、Wi-Fiに誰もが入れるようなお粗末な管理だと、「内部ネットワーク」とは言えず、結果的にはインターネットカフェなどと同じ程度の安全性になります。つまり、組織内だからTLSは不要であるという結論を出すには、ネットワークの管理が正しく行われているかどうかに依存します。その上で、スタッフを信頼できるかどうかは、是非とも組織内でディスカッションをしてみるべきテーマです。

ブラウザーのセキュリティ

 現在、セキュリティ上の懸念点があるのは、サーバーやネットワークよりもむしろブラウザーであると言えるでしょう。ソフトウェアは作り方によってはセキュリティ上の問題点を発生します。特にXSS(クロスサイトスクリプティング)については、あらゆるWebアプリケーション開発者が常に意識すべき問題です。これについては、このすぐ後の『クロスサイトスクリプティング攻撃(XSS)とinnerHTML』で説明します。

 ブラウザーでの一番の問題は、利用者が切り替わる場合の対処です。認証が必要なサイトをあるユーザーが利用していたとして、しばらくデスクを空けていたとします。その間に、画面がスリープやロックになって、元のユーザーにしか分からないパスワードでしか解除できない状況なら問題はありません。しかし、ブラウザーのウインドウが開きっぱなしならどうでしょうか? それまでのユーザーになりすまして、別のユーザーも利用できてしまいます。これは、使用中だけでなく、使用後と思っても正しくログアウトできていない場合、やはり別のユーザーが偶然に認証済みのページを見つけてしまうかもしれません。この問題は、すべてのアプリケーションに存在し、防ぐ方法はありません。言い換えれば、利用者が端末を適切な方法で利用しない場合には、脅威として必ず存在します。もちろん、INTER-Mediatorだけの問題ではありません。アプリケーションを実装するときに、何もしないと認証が時間切れになる仕組みを利用したり、あるいはその時間を短くするなどで、他人がなりすます可能性を少しは減るかもしれませんが、力ずくで端末を開いた状態で取り上げられるようなことがあれば、やはり脅威となり得るのです。

 ブラウザー上で稼働するJavaScriptのプログラムの場合、常に、変数の結果を参照したり、あるいは書き換えたり、場合によってはプログラムの書き換えができてしまう点に注意を払う必要があります。INTER-Mediatorでは、パスワードそのものは変数にも残さないようにしていますが、認証の手がかりとなる情報は変数に残しています。ある操作をすれば、それは参照できますが、原則として、本人が操作して見えてしまっても、本人である限りは問題ないと言えます。また、書き換えた結果、何もできなくなるのなら、それは書き換えた人の責任であり、開発する側は責任を取る必要はないと考えます。しかしながら、問題になるのは、そうして書き換えをした結果、別のユーザーになりすますことができるような状況があるかどうかです。INTER-Mediatorでは、その点はテストしており、認証時に別のユーザーになりすますことはできない仕組みになっています。少なくとも、開発時にはその点も考慮しました。

 いずれにしても、JavaScriptのプログラムをクライアントで触れる点は問題になりそうですが、それによって他のユーザーになりすましたり、あるいはシステムやデータに損傷を加えることがなければ問題はないと言えます。ただし、この点について「バグが発見されていない」というだけのことで、この点について保証できるものではありません。潜在的には何かしらの問題点がある可能性があります。バグによる脆弱性の存在の可能性はINTER-Mediatorに限らずどんなソフトウェアにも存在します。何らかの懸念点があれば、アプリケーション開発者でもフレームワークの動作を検証する必要はあるかもしれません。

クロスサイトスクリプティング攻撃(XSS)とinnerHTML

 クロスサイトスクリプティング攻撃(XSS)は、現在のWebアプリケーション開発において、必ず考慮されなければならないセキュリティ要件です。このXSSは、攻撃者が仕込んだ任意のJavaScriptが実行できる状況において発生し、その結果、例えばある特定のサイト向けのクッキー情報やページ上に表示された情報を、攻撃者のサイトにネットワーク転送してしまうような悪用が考えられます。つまり、ページ上で、任意のスクリプトが実行される状況を作ってしまうと、XSSが可能なセキュリティホールが発生します。これは、フレームワークに内在する問題ではなく、どんなフレームワークを使っても、アプリケーション側で意図せずに実装されてしまう可能性のあるセキュリティホールです。

 XSSが発生する代表的な例が、「誰でも書き込みができるBBS」の事例です。せっかくのBBSを盛り上げるために、HTMLで自由にスタイルなどを設定するようにしていたとします。すると、書き込むメッセージにSCRIPTタグがあるとします。そのSCRIPTタグのプログラムは、書き込んだ人だけでなく、BBSを閲覧している人のブラウザー上でも実行します。クッキーの情報は、記録したドメインと同じドメインのサイトに接続しないと読み出せないといった制限がかかってはいるものの、スクリプトはまさにそのサイトにおいて実行しているので、他の人のクッキーを得ることができます。クッキーに含まれる情報で認証の成立を確認する状況は比較的利用されており、クッキーを第三者に取り出されてしまうと、その第三者によってなりすましのログインができてしまう場合も最悪はあり得ることになります。

 この時、メッセージのHTMLテキストを表示するのに利用されるひとつの方法が、innerHTMLプロパティへの代入です。HTML文字列をそのまま代入できて便利なのですが、その文字列にSCRIPTタグが含まれている可能性があるならば、innerHTMLの利用は危険です。INTER-Mediatorでは、意図的にinnerHTMLを使用するように設定した場合と、認証パネルのカスタマイズの部分を除いて、innerHTMLは利用しないようになっています。通常のデータベースから得られたデータをタグ要素にマージするときには、DOMのAPIを使ってテキストノードとして追加をしています。

 INTER-Mediatorで制限付きながらもinnerHTMLをサポートしている理由を説明しましょう。innerHTMLの危険性がある一方で、innerHTMLがあれば必ずXSS攻撃を受けてしまうということにはなりません。任意のHTMLテキストをデータとして書き込めるユーザーを限定すれば、第三者からのXSS攻撃は防ぐことができます。例えば、ある組織の通販サイトを運用するのであれば、メッセージを書き込むのは通常は通販会社の担当者です。利用者からのメッセージは例えばテキストだけにするということが一般的です。そういう状況においては、INTER-Mediatorは「任意のスクリプトを第三者が実行できる」状態にはなっていません。そのためにも、HTMLテキストを書込み権限は認証したユーザーだけに与えることをベースにして設計する必要があります。悪意のある人に書き込み権限を与えない方策を備えていれば、innerHTMLを全面的に排除していないことが、問題にはならないと考えています。すなわち、システムを運用する組織としての信頼関係があるユーザーだけに書き込みを許可すれば、XSSの発生となる根本原因を除去できるということです。もちろん、信頼した相手が攻撃をしないという前提の元で成り立つことです。

クロスサイトリクエストフォージェリ(CSRF)を排除する

 クロスサイトリクエストフォージェリ(CSRF)は、最終的にはユーザーが意図しないサイトへ誘導されて、意図しない処理をさせられてしまう可能性のある攻撃です。攻撃者は攻撃用のWebページを公開し、あるユーザーがそのWebページを参照したとします。そのページの中にあるJavaScriptが、全く別のサイトに接続してリクエストを送るということができます。例えば、偽の「お買い得情報!」メールを送り、それを見たユーザーが攻撃者のサイトとは知らずにアクセスしたとします。その際に、そのサイトのファイル一式の中に、オンラインショップに接続して何か購入することを決定するようなスクリプトが組まれていたとしたらどうでしょう。そのユーザーがいつも使うオンラインショップであれば、認証がすでに有効になったままかもしれません。そのままJavaScriptのAJAXの機能を利用して自動的にオンラインサイトを操作して、購入決定までさせてしまうことは技術的には可能です。購入操作が無事完了したかに見えますが、商品は届かないかもしれません。もちろんオンラインショップのコントロールは難しそうですが、匿名の掲示板だとそれほど困難ではありません。ユーザー自身が知らないうちにあちこちのBBSに、攻撃者が記述したなりすましメッセージを書き込まれてしまうということにもなってしまいます。

 INTER-Mediatorでは、作成したアプリケーションがCSRFを受け入れる脆弱性を持ってしまうことを防ぐための対策も組み入れています。対策方法としては、リクエストヘッダーにX-FromおよびOriginを利用する手法を利用しました。ポイントは、HTTPリクエストがサーバーに来た時にOriginヘッダーをチェックすることです。JavaScriptで作成した通信処理はXMLHttpRequestクラスを利用しますが、通信においてはそのページが生成されたサーバーのURLがクライアント側で自動的にOriginヘッダーの値に設定されます。そこで、INTER-Mediatorのサーバーでは自分自身のFQDN値を記録し、クライアント側ではX-Fromヘッダーにその値に付与するように動作します。元サーバーから自動的に付与されるOriginがX-Fromと同じであり、それがサーバーに設定したFQDNと同じであれば、CSRF攻撃ではないと判断できます。さらに、DNSサーバーの応答を操作することでの攻撃を回避するためにHostヘッダーも確認しています。攻撃側では攻撃対象のサーバーのFQDNはもちろん分かりますが、それを攻撃プログラムに組み込んでもDNSが正しく稼働している限りはOriginは攻撃者のサーバーになるので、OriginとX-Fromは一致せずCSRF攻撃であるとみなして処理はスキップします。X-Fromを攻撃者のサーバーと同じに設定しても、サーバーに設定したFQDNとも違うので、やはり攻撃が成り立ちません。CSRF対策として自分自身のFQDNをサーバーへ設定する方法については『2-6 設定ファイルparams.php』で説明します。

データベースアカウントへのアクセス権設定

 データベースエンジンには通常、アクセスするためのアカウントを設定可能です。INTER-Mediatorはデータをデータベースに保持するので、データベースアカウントを利用することで、可能な限り安全な運用が可能です。なお、SQLデータベースのひとつであるSQLiteではアカウントの設定機能はありません。設定の詳細については、『2-1 データベースからの取り出し設定』で説明があります。

 SQLiteはネットワーク接続ができませんが、その他のINTER-Mediatorが対応するデータベースはすべてネットワークからの接続ができます。しかしながら、INTER-MediatorあるいはWebサーバーで稼働するWebアプリケーションから利用するときには、ネットワーク接続が必須なわけではありません。例えば、Webサーバーとデータベースサーバーが同一の場合は、データベースへのネットワーク接続は不要です。その場合は、localhostからの接続だけに限定することで、セキュリティ面では「他のコンピューターからの直接接続」は排除できて、不正なアクセスを許す要因は取り除かれます。また、Webサーバーとデータベースサーバーが異なるホストでも、データベース側ではWebサーバーからのアクセスのみを許可することで、他のコンピューターからの接続は排除できます。不要なネットワーク処理を利用できる状態にしないというのは、データベースに限らず、すべてのサーバー運用では鉄則と言える対処です。

 MySQLやPostgreSQLなどSQLサーバーでデータベースを構築するときには、データベースサーバー自体に対して何でもできるスーパーユーザーを設定します。MySQLではroot、PostgreSQLではpgsql、SQL ServerではSAというユーザー名が一般的でしょう。また、FileMaker Serverは、各データベースにすべての設定変更が可能な管理者ユーザーを設定します。名称は任意ですが、Adminなどの名前をつけます。これらのアカウントには原則としてパスワードを設定します。PostgreSQLの場合は、OSのアカウントを利用するのですが、パスワードを利用してログインをします。これらデータベースエンジンのアカウントは、最初にデータベースを作ったり、スキーマの適用をする場合には必要になりますが、実運用、特に、Web経由のアクセスに応答するために利用することは、可能なかぎり避けるべきでしょう。理由は、「制限したユーザーでの利用の方が少しでも安全になる確率が高い」からです。

 もし、Webアプリケーションからのアクセスが、データベースからの読み出しのみの場合、データベースのユーザーは、スキーマ定義ができないことはもちろん、必要なテーブルに対してSELECTのみ、あるいは読み出しのみの権限のみを与えておきます。また、Web側から更新があるのなら、スキーマ定義はできないものの、SELECT/DELETE/INSERT/UPDATE、あるいは読み書き権限を与えておきます。こうした、スーパーユーザーでないユーザーの運用制限を加えておくことで最悪の事態が起きた場合でもデータの安全が確保されることになります。最悪の場合に考えられるのは、ソースコードごと漏洩してデータベースのスーパーユーザーが知られてしまう可能性があることや、設定を誤り、抜け道を作ってしまい、読み出ししかできないはずが認証もなくデータを消すことができてしまったといったことがあります。

 もし、サーバー管理を正しく構築し、ソリューションにセキュリティホール無く作ることができれば、スーパーユーザーでも問題ないのかもしれません。しかしながら、一種の保険として、データベースへのアクセス権を必要以上に広げないようにすることをお勧めします。そんなことは不要、あるいは過剰と思われるかもしれませんが、サーバー運用において不要なポートを閉じるのと同様な、予防的な措置として検討しましょう。

 なお、プロバイダのレンタルサーバーでは、スキーマ定義や読み書きができるひとつのアカウントしか使えないことがあります。その場合は仕方ありませんので、他の手法でセキュリティを確保することにします。

セキュリティ設定に関連するヘッダーを通信に含める

 セキュリティの確保のための設定がparams.phpにあります。表7-1-1にある変数を定義することで、通信ヘッダー等に情報が追加されます。

変数名既定値用途
$xFrameOptions"SAMEORIGIN"変数に指定した値をX-Frame-Optionsヘッダーの値として応答に含める。使用できる文字列は、"SAMEORIGIN", "DENY", "ALLOW-FROM uri"の形式で、""(空文字列)にすれば、X-Frame-Optionsヘッダー自体を出力しない
$contentSecurityPolicy""(空文字列)変数に指定した値をContent-Security-Policyヘッダーの値として応答に含める。""(空文字列)にすれば、Content-Security-Policyヘッダー自体を出力しない
$accessControlAllowOrigin(未定義)変数に指定した値をAccess-Control-Allow-Originヘッダーの値として応答に含める。未定義ないしは ""(空文字列)の場合は、Content-Security-Policyヘッダー自体を出力しない
$webServerName設定なしWebアプリケーションが稼働しているホストのFQDN名を配列で指定してCSRF攻撃対策を行う。例えば、array('www.inter-mediator.com', 'inter-mediator.org')など。ひとつだけであってもarrayで指定する
表7-1-1 params.phpに記述できるセキュリティ関連の変数

 HTTPレスポンスに含めるセキュリティ関連のヘッダーについての変数が、$xFrameOptionsと$contentSecurityPolicyです。X-Frame-Optionsヘッダーは通常、値がSAMEORIGINでヘッダーに含まれています。異なるドメイン名のサイトの内部にINTER-Mediatorのページをiframeタグ要素で挿入するような場合、このヘッダーを非表示にしたり、特定URIに対する許可を与えるなどする必要がありますが、その場合クロスサイトスクリプティング攻撃の可能性が高まりますので注意が必要です。Content-Security-Policyヘッダーは通常は出力されていませんが、こちらも必要なら指定ができます。このヘッダーを適切に指定することで、クロスサイトスクリプティング攻撃の機会を減らすことができます。設定の記述は多岐に渡りますので、詳細はこちらを参照してください。さらに、$webServerName変数を指定することで、CSRF対策を行います。URLと接続した先のサーバー名が同一FQDNかどうかを判定する仕組みを稼働させます。Webアプリケーションのセキュリティに関することは、『7-1 Webアプリケーションセキュリティの前提』も参照してください。

 Webアプリケーションは通常サーバーに配備され、クライアントはそのサーバーとのやりとりだけで完結できます。しかしながら、クライアントがさらに別のサーバーに対してAjax通信を行おうとしても、ブラウザーはセキュリティ上の理由(Same-Originポリシー)から通常は阻止をします。その時は、Access-Control-Allow-Originヘッダーを応答に追加すれば、値に指定したURLの通信は別のサーバーでも許可されます。params.phpファイルに$accessControlAllowOrigin変数を定義して、URLを含む文字列の値を代入します。サーバーAとクライアントCがあって、サーバーAにあるWebアプリケーションにクライアントCから接続して利用している場合を想定してください。この場合、AとCのやり取りは特に何もしなくても可能です。もし、Cのクライアントアプリケーションで、別のサーバーBに対してAjaxつまりXMLHTTPRequestクラスを使った通信を行おうとするとき、通常はエラーになります。このとき、AのサーバーのURLを値に持つAccess-Control-Allow-OriginヘッダーをBのサーバーがクライアントCに返すことで、Aの配下のクライアントCであってもBへのアクセスを許可します。Access-Control-Allow-OriginヘッダーをINTER-Mediatorで使う場面として、Web APIを作成するような場合があります。任意のクライアントからWeb APIを利用できるようにこのヘッダーの指定が必要になります。もしくは、定義ファイルを複数のサーバーで分散処理する場合にもこのヘッダーを利用する必要があります。INTER-Mediatorが扱う分散処理の例として、ページファイルのサーバーと定義ファイルのサーバーが異なるURLになる場合があります。そのような場合にはこのヘッダーの設定を利用して、ページファイルのURLからの要求を定義ファイルを供給する側のサーバーで許可するように設定します。

セクションのまとめ

 Webアプリケーションは、「サーバー側を誰もが自由に参照したり改変することはできない」という前提のもとで、セキュリティ対策を行っています。つまり、Webサーバーにログインできるユーザーを制限することが大前提です。また、データベースを利用するためのユーザーについても、余分な処理をできないようにしておくことで、他の問題が発生してもセキュリティが確保される確率は幾分は高くなるでしょう。

7-2ユーザー認証とアクセス権適用を行う仕組み

INTER-Mediatorは従来形式のWebアプリケーションと違い、クライアントサイドでの処理がむしろ主になっています。認証とアクセス権適用の考え方については、クライアントとサーバーでの役割が従来とは異なります。ただし、アプリケーション利用者にとっては違いがないようになっています。このセクションでは、INTER-Mediatorの認証やアクセス権の機能に加えて、その実現に必要なことを説明します。

認証とアクセス権

 改めて、「認証(Authentication)」についての定義を記載します。認証は、利用者の特定を行い、アカウントが確定することです。ユーザー名とパスワードを入力するのが認証の代表的な手法です。何らかの方法で、アカウント、つまりシステムに記録された利用者のどれかを確定することが認証で実現されることです。もちろん、他のアカウントになりすましたり、他のアカウントとして振るまえてしまうということがないようにする必要があります。ユーザー名とパスワードを使う方式ではさまざまなセキュリティ的なリスクがありますが、一番大きいものが「パスワードの漏洩」です。言い換えれば、パスワードはそのアカウントに対応する人しか知りえないという前提の下で成り立つセキュリティの確保の手法です。

 「アクセス権(Authorization)」は、「認可」とも呼ばれます。通常は認証によって確定したアカウントに対する処理範囲の制限を行う機能です。制限は、アプリケーション開発者やあるいはシステム管理者によって指定されます。一般には、特定の処理だけに制限するような設定、ないしは特定のアカウントに対してだけ特定の処理の許可を与えるといった設定が可能です。アクセス権についてのセキュリティ的なリスクとしては、もちろん、フレームワークのバグや、バックドアなどのアクセス権回避手段が存在しないといった、フレームワークの実装面のことがあります。また、アクセス権の設定は複雑になりがちであり、設定のミスが起こりやすいとも言えるので、作成したアプリケーションの十分な実証テストは欠かせません。

アカウントとグループ

 INTER-Mediatorは、アカウントとして「ユーザー」「グループ」、そして内部的なアカウントとして「クライアント」の3種類をサポートします。グループは、ユーザーの集合ではありますが、グループにグループを所属させることができます。ユーザーは自分が所属するグループの一覧を得て、アクセス権の適用を受けます。

 ユーザーのアカウントを記録する方法としては、アプリケーションが利用するデータベースに含まれるテーブルあるいはビューを利用する方法(ユーザー認証)が基本です。また、外部の認証サーバを利用するSAML認証にも対応していますが、その場合でも利用するデータベースでのユーザー用テーブルは必須となります。

 一方、グループについては、INTER-Mediator側で定義したものだけが指定できます。データベースエンジン側で定義したものに対してのアクセス権設定はできません。ユーザー認証時はもちろん、ネイティブ認証時にもアプリケーションが利用するデータベースにグループを管理するためのテーブルを定義し、そのグループレコードの利用のみです。

 クライアントのアカウントは、通信のやり取りが発生したときにINTER-Mediatorによって自動的に割り振られるコードです。専用のテーブルは不要ですが、認証のための情報を記録するテーブルのひとつのフィールドとして記録されます。このクライアントコードを利用して、クライアント、正確には「ブラウザーのウインドウ」を識別しますので、同じページを同一のブラウザの別々のタブで表示していても、クライアントとしては別々のものとして認識しています。

ユーザーのテーブル

 利用するデータベースでは、ユーザーのテーブルが必要です。既定のテーブル名は「authuser」としています。この名前は設定で別のものにもできますが、以下、特定しやすいように、ユーザーアカウントのテーブルはauthuserテーブルと呼ぶことにします。なお、テーブル名は任意の名前を指定できますが、フィールド名は決められたもののを使用する必要があります。表7-2-1に必要なフィールドを記述します。認証だけならテーブル自体は読み込みのみのアクセス権でも構いませんが、パスワードの変更をログインパネル上で行う場合には、パスワードのフィールドは書き込み可能である必要があります。また、テーブルの内容をアプリケーションで追加編集する場合には、一般には読み出しだけでなく変更や追加、削除の権限も必要になります。サポートするデータベースのサンプルファイルあるいはサンプルスキーマには、authuserテーブルそのものあるいは生成コマンドが含まれているので、自分で作成するアプリケーションの場合はその部分をコピーすると良いでしょう。

フィールド名型の例説明
idINT AUTO_INCREMENT連番の数値を入れて、キーフィールドとする
usernameVARCHAR(64)ユーザー名(長さは任意、重複した名前が定義されないようにする)
realnameVARCHAR(20)利用者名で、認証には特には関係ないのでなくても構わないが、SAMLやOAuth2では得られることが多く文字列として記録しておく(長さは任意)
hashedpasswdVARCHAR(72)パスワードのハッシュ値で72バイト分必要
emailVARCHAR(100)電子メールアドレス。ユーザー名の代わりに使用したり、パスワードのリセットで利用
limitdtDATETIMESAML認証で必要。キャッシュしたアカウントの期限
表7-2-1 authuserテーブルのフィールド

 パスワードのフィールドには、パスワードはそのまま入れずにダイジェスト関数によって処理された値を使います。当初はSHA-1を利用していましたが、セキュリティ上の問題が内在されておりSHA-1の利用自体が問題視される状態になったので、Ver.8でSHA-256に対応しました。なお、認証におけるセキュリティ的な問題は、ダイジェスト関数の脆弱性によるものだけでなく、むしろディクショナリ攻撃や不味い運用(全員同じパスワードなど)によるものの方が深刻であると考えられるものの、SHA-1を使っているというだけでセキュリティ上の問題と判断される風潮でもあるので、SHA-2ベースに移行しました。SHA-1を使っている状態を「認証バージョン1」、その後に改訂してSHA-256を使っている状態を「認証バージョン2」と称することにします。Ver.11現在、過去との互換性を考慮して、既定値は認証バージョン1になっています。新たにアプリケーションを構築する場合は特に縛りはないと思われるので、認証バージョン2での運用をお勧めします。なお、認証バージョン2で運用している場合でも、以前のアプリケーションでバージョン1のみで運用していた時代のauthuserのレコードはそのまま認証で使えます。

 パスワードのハッシュ値の計算方法は以下のとおりです。独自に生成する場合には、saltを常に異なる値にすることが必要です。なお、JavaScript側のライブラリの制約により、パスワードおよびsaltはASCII文字として表現可能な範囲にします。コントロールコードや漢字は利用しないようにします。シェルスクリプトで生成する場合のサンプルは、INTER-Mediatorのレポジトリにあるdist-docs/usergenerator.shを参照してください。

リスト7-2-1 hashedpasswdフィールドの値の求め方
pw:パスワード
salt:4バイトのソルト値
hash():認証バージョン1ではSHA-1、認証バージョン2ではSHA-256を5000回繰り返しによるダイジェスト値を求める関数
hex():16進法による表記に変換する関数
+:文字列の結合

hashedpasswdフィールドの値 = hex(hash(pw + salt)) + hex(salt)
認証バージョン1は48バイト、認証バージョン2は72バイト

 表7-2-1に示したauthuserテーブルのフィールドは必須のフィールドですが、任意のフィールドを追加してもかまいません。例えば、VARCHAR(20)型のrealnameフィールドに、ユーザーのフルネームを入れておけば、ログインした人の氏名をページ上に表示するようなことにも利用できるでしょう。

 もし、すでに別の名前でユーザーテーブルが作られているのであれば、SQLデータベースの場合には、authuserビューを定義し、フィールド名はビュー定義のコマンドで別の名前に付け替えるような記述を行えば良いでしょう。FileMakerの場合には、レイアウト名をauthuserにすることで、任意のテーブルを利用できます。ただし、既存のフィールドが作られている場合ではフィールド名が表7-2-1のようになっていない可能性があります。その場合はフィールド名を変更するか、あるいは計算フィールド等フィールドの追加を行い、表に示したフィールドが存在するように見えるようにします。

グループのテーブル

 グループを構成するテーブルは2つ必要です。まず、ひとつはグループそのものを定義する「authgroup」テーブルです。加えて、ユーザーやグループの所属関係を定義する「authcor」テーブルです。いずれも、任意の名前を利用できますが、テーブルの種類を特定する場合、authgroupテーブル、authcorテーブルと呼ぶことにします。表7-2-2と表7-2-3に、必要なフィールドを記述します。

フィールド名型の例説明
idINT AUTO_INCREMENTグループを識別するための番号
groupnameVARCHAR(48)グループ名
表7-2-2 authgroupテーブルのフィールド
フィールド名型の例説明
idINT AUTO_INCREMENTレコードを識別するための番号
user_idINT所属するユーザーのidフィールドの値
group_idINT所属するグループのidフィールドの値
dest_group_idINT所属されるグループのidフィールドの値
表7-2-3 authcorテーブルのフィールド

 authgroupテーブルは単にグループ名とid番号の割り当てのためのものです。グループへの所属関係を記録するauthcorテーブルのひとつのレコードでは、user_idフィールドもしくはgroup_idフィールドのどちらかを入力し、一方はNULL(FileMakerでは"")にします。これらのフィールドには、authuserおよびauthgroupテーブルのidフィールドの値を設定します。dest_group_idフィールドにはauthgroupテーブルに存在するidフィールド値を指定します。ユーザー名やグループ名を指定するのではありません。

 例えば、{user_id: 1, group_id: NULL, dest_group_id: 101}といったレコードがあれば、authuserテーブルのid=1のレコードが示すユーザーが、authgroupテーブルのid=101というグループに所属することを意味します。また、autugroup_id=3のグループがauthgroupテーブルのid=101というグループに所属することは、{user_id: NULL, group_id: 3, dest_group_id: 101}と言ったレコードで記述します。

ユーザーレコード生成のためのスクリプト

 データベースのスキーマを読み込む段階で、すでにいくつかのユーザーを作っておきたい場合もあるでしょう。もちろん、自分でハッシュを計算することで可能ですが、INTER-Mediatorには、macOSあるいはLinuxで利用できるユーザーレコード作成のコマンドが用意されています。INTER-Mediatorフォルダー内のdist-docsフォルダー内にある「passwdgen.sh」というシェルスクリプトです。

 このシェルスクリプトを使えば、hashedpasswdフィールドの値を求め、場合によっては、SQLステートメントの形式で得られます。INTER-Mediator/dist-docsがカレントディレクトリにあるとして、例えばリスト7-2-2のようにコマンド入力できます。行の最初に$があるのがコマンド入力行です。最初のコマンド例は、引数として--userでユーザー名、--passwrodでパスワード文字列を指定しており、出力は、CSVファイルの形式で、ユーザー名、パスワード、hashedpasswdフィールドの値の順に表示されます。ソルトは乱数で生成しています。2つ目のコマンド例は、さらに、--sqlを引数に指定しており、これにより、SQLステートメントの形式で出力をしています。なお、ハッシュ値の計算は、認証バージョン2に対応したものです。opensslで5000回のハッシュ計算を内部的には行えず、コマンドを5000回処理しているので、スクリプトの処理にはちょっと時間がかかります。

リスト7-2-2 passwdgen.shスクリプトの利用例
$ ./passwdgen.sh --user=test --password=testpassword
'test','testpassword','07559ce0fc95e44760dcb9a7794060ab740aad861b41f12b0a4856323d6e3b4c677a6867'
$ ./passwdgen.sh --user=test --password=testpassword --sql
INSERT authuser(username,initialpass,hashedpasswd) INSERT authuser(username,initialpass,hashedpasswd) VALUES('test','testpassword','a1ec3bb4e914822a35427c0fce3e25a43e86dbbc753ca525488bc9d8426df5f4636e6246');

 一方、CSVファイルを用意して、そのファイルを「./passwdgen.sh --sql --csv=CSVファイルのパス」のように指定すると、上記の処理をCSVファイルから取り出して行うので、多数のユーザー用のSQLステートメントを生成できます。CSVファイルは1列目がユーザー名、2列目がパスワードです。カンマで区切りますが、それぞれのフィールドは、シングルクォートやダブルクォートで囲っても構いませんし、囲わなくても構いません。

 同じフォルダーに、usergenerator.shというスクリプトファイルもあり、こちらは、パスワードの自動生成などを行います。なお、引数は特に取りません。なお、これらのスクリプトでは不満がある場合もあるかもしれません。いずれもシェルスクリプトですので、必要に応じて改良して使ってください。また、スクリプトのソースはユーザー自動生成の方法の参考になると思います。

ハッシュ値用テーブルの内容

 認証を行う場合、ユーザとグループのテーブルだけでなく、表7-2-4に示すテーブルが必要です。このテーブル名についてもカスタマイズは可能ですが、ここでは既定値の「issuedhash」テーブルと呼ぶことにします。通信を行うたびにレコードを生成するので、1回のページ表示でたくさんのレコードを作成します。そのため、このテーブルをFileMakerで運用するにはパフォーマンス上の問題が発生しますが、FileMaker Serverで運用しつつ、issuedhashテーブルのみをSQLiteで運用するということもできるようになっています。FileMakerではこの方法により、パフォーマンスを大きく損なうことなく認証処理ができるようになっています。

フィールド名型の例説明
idINT AUTO_INCREMENTレコードを識別するための番号
user_idINTauthuserテーブルのキーフィールドとなるid値
clienthostVARCHAR(64)クライアントを識別する自動生成されるコード
hashVARCHAR(64)チャレンジに使うハッシュ値。実際には24バイトの16進数文字列
expiredDateTimeチャレンジの有効期限を示すタイムスタンプ値
表7-2-4 issuedhashテーブルのフィールド

 このテーブルの意味を理解するには、INTER-Mediatorのクライアントサーバー間でのプロトコルを理解する必要があります。ここで解説は行いますが、機能を利用する上ではこの仕組みまで知る必要はありません。セキュリティのアセスメントが必要な方はソースコードを分析して理解してください。

INTER-Mediatorの認証プロトコル

 INTER-Mediatorの認証プロトコルは、『7-3 認証とアクセス権の設定』の『定義ファイルでの設定』で説明する、storingの設定に依存します。この設定は当初は、coockieおよびcookie-domainの設定しかできず、クッキーを利用した認証のみでした。その後にsession-storageを利用した認証が加わりましたが、その後にセキュリティ面を見直して、「credential」という設定を追加しました。原則として、これらの設定はいずれも使用は可能ですが、現在はもっともセキュリティへの配慮が進んだcredentialを使ってください。以下の説明も、基本的はcredentialを使った上での説明を中心とします。credentialは、http-onlyのクッキーを使った手法です。初期からある方法とは大きく異なり、認証を成立させる方法を、クライアント側のJavaScriptから一切取り出せないようにすることを意図した手法です。

 表7-2-5は、クライアントからサーバーに対してデータベースアクセス等の要求がある場合のやり取りに、認証情報がどのように関わるかを示したものです。storingがcredential以外の手法は、常にこのプロトコルに従いますが、credentialの場合にはこのプロトコルは認証パネルを出して、最初に認証するときのみに利用されれます。変数名と計算方法はリスト7-2-3に示します。単にアクセス要求と応答があるのではなく、チャレンジ要求が最初にあり、チャレンジをもとに要求を出したときに、サーバー側で認証が判定されるという点が大きな流れです。もちろん、認証が成立したら、データベースから取り出したデータを返したり、更新処理に入ります。認証が成立しなければ、ログインパネルを再度表示し、このプロトコルを最初からやり直します。

クライアント側での処理転送される認証情報サーバー側での処理
チャレンジ要求→user→データベースよりhpwを取得しsaltを求める
chを乱数より生成,cidが未発行なら乱数で生成
user, cid, ch, 日付時刻をデータベースに記録
resを求める←salt, cid, ch←
データベース要求→cid, user, res→issuedhashテーブルより,cidからチャレンジを取得
res'を計算し、resと同じなら認証成立
表7-2-5 認証を伴う処理の場合のINTER-Mediatorプロトコル
リスト7-2-3 変数と式
user:ユーザー名(ユーザーが入力)
pw:本来のパスワード(ユーザーが入力)
salt:ユーザーごとに異なるソルト(4バイトのASCII文字)
hpw:データベースに保存されているパスワードのハッシュ値
pw':入力したパスワード
cid:クライアントid(16進数で記述)
ch:サーバーから送られるチャレンジ(16進数で記述)
res:クライアントから送られるレスポンス
res':サーバー側の情報から得られるレスポンスの期待値
hash():認証バージョン1ではSHA-1、認証バージョン2ではSHA-256を5000回繰り返しによるダイジェスト値を求める関数
hmac(k, m):HMAC-SHA256で求められるMAC値を求める関数。ハッシュ関数はSHA-256,鍵がk,メッセージがm
hex(m):16進法による表記に変換する関数(hex(m) + hex(n) = hex(m + n)が成り立つ)
+:文字列の結合

hpw = hash(pw + salt) + salt
res = hmac(hash(pw' + salt) + salt, ch)
res' = HMAC(hpw, ch) = hmac(hash(pw + salt) + salt, ch)
pw = pw' であれば res' = res となる

 まず、ログインパネルでユーザー名とパスワードを入力します。そして、ユーザー名(user)をサーバーに送ります。サーバー側では、そのユーザーのパスワードハッシュ値(hpw)とidフィールドの値をauthuserテーブルから取得します。hpwの最後の8バイトからソルト(salt)が得られます。そして、クライアントid(cid)とチャレンジ(ch)を乱数で生成します。そして、issuedhashテーブルにレコードを作成して、ユーザーのid値、cid、ch、日時を記録します。クライアントへは、応答としてsalt、cid、chを送ります。クライアントはデータベースの要求に加えてres値を計算して、サーバーへの要求に付加します。res値は、応答で得られた値と、入力したパスワードから求めておきます。また、要求にはuser、cidも返します。要求を受け取ったサーバーは、issuedhashテーブルを検索して、chを求めます。そして、ユーザー名から得られるsalt、返されたcidをもとに、res'を計算します。このres'がresと等しければ、クライアント側で正しいパスワードが入力されたと判断できるということです。resとres'は、式の上ではpwとpw'の部分だけが違います。

 この手法の大きな特徴は、ネットワーク上に流れるデータからパスワードを求めることができないということです。ネットワーク上のデータは、user、salt、cid、ch、resであり、これらの値からパスワード自体を求めることはできません。ハッシュ値hpwをデータベースに保存はしますが、パスワードはもちろん、そのハッシュ値自体もネットワーク上を流れないということです。この一連の作業で使われるチャレンジ(ch)は、サーバー側では共有しません。原則として1回のやり取りの間だけ有効になり、リクエストがあるたびに生成をします。したがって、resの値は要求ごとに異なる値になります。

 クライアントid(cid)については、可能な限り再利用を行います。この認証だけなら、cidでなくても認証はできそうに見えるかもしれませんが、同一ユーザーで同時に複数のページを利用している場合があることを考えれば、チャレンジ要求とデータベース処理要求を結びつける意味では必要です。

 なお、認証をすれば通常は1回の通信が2回へと倍になるのではないかと思われるかもしれませんが、より効率的な動作をします。データベース処理に対する応答には、次の要求のためのチャレンジ(ch)などのデータがすでに含まれていて、2回目以降は、データベースの応答と次のチャンレンジ応答を1回の応答で済ませてしまいます。したがって、認証がない時にn回の通信をする場合、認証があればn+1回の通信を行うことになり、1回、つまり最初のチャレンジ要求だけが増えることになります。

 storingにcredentialを指定した場合は、認証成立後に、http-onlyのクッキーがサーバから送られてクライアントに記録されます。クッキーの値は、その時のチャレンジ、クライアントid、ユーザパスワードのハッシュ値を合成した文字列に対するSHA-256のハッシュ値を求めています(sha256(ch + cid + (hash(pw + salt) + salt))。クッキーに記録されたので、次にまたサーバを利用する時にはその値がサーバに送られます。そして、サーバーで値が正しいものかを判定し、正しければ認証が成立したものとみなします。前の応答では、{cid, ch, user} がissuedhashテーブルに記録されているので、cidを元に残されたデータを検索して、クッキーで送り込まれたハッシュが正しいものかは判定できます。なお、ユーザ名は応答に入れてありますが、issuedhashテーブルのレコードの検索に利用するので、クライアント側でユーザ名を書き換えれば異なるチャレンジを想定していることになり認証は成立しません。応答時には、また新しいチャレンジが生成されるので、クライアントに送り込むクッキーも新しいものが生成されます。

 このように、credentialを使った認証ではJavaScript側からクッキーを取り出したりあるいは書き込むことができないことから、何らかの方法でXSS等で第三者によるコード実行ができたとしても、認証に使うクッキーには手を出せない状態になっています。もちろん、第三者によるコード実行はできないようにしておくのが基本です。

 ログインパネルが表示された場合、そこにパスワードは打ち込まれる必要があります。そしてそれはプログラム処理するために変数に設定される必要がありますが、ローカル変数に設定されるため、時間的には即座に消えると考えてください。パスワードを取得したら、即座にresの値を求めますが、認証が成功すると、プロパティに置いたresはそこでクリアします。したがって、認証が成功したら、パスワード、そしてパスワードのハッシュ(hpw)、認証で利用するresの値は全部消されます。XSSによる第三者コードを使ってパスワードなどが、JavaScriptのコードを通じて盗まれる可能性はかなり低いと言えるでしょう。もちろん、第三者のコードが実行される機会がなければ、パスワードやハッシュ値は横取りされる心配はないと言えるでしょう。

演習ユーザー管理の簡易アプリケーションを使ってみる

 INTER-Mediatorにはauthuser, authgroup, authcorテーブルの内容を編集する簡易アプリケーションが付属しています。テーブルの定義は必要ですが、それに加えて、このアプリケーションをベースにして、独自のユーザー管理アプリケーションを作成することもできるでしょう。この演習では、単にアプリケーションの存在と動作の確認だけ行います。実際の利用は、この後のセクションで何度か出てきます。

ユーザー管理アプリケーションを参照する

1演習環境を利用して、認証に利用するユーザなどのデータがどのようにデータベースに格納されているのかを確認します。ブラウザーで、「http://localhost:9080」に接続します。
2ページ内にある「トライアル用のページファイルと定義ファイル」というタイトルの部分を特定します。「ユーザー管理ページサンプル」というタイトルの部分をクリックします。
3ユーザー名とパスワードを入力するパネルが表示されるので、ユーザーに「user1」、パスワードに「user1」と入力して、「ログイン」ボタンをクリックします。
このアプリケーションは「認証が通れば参照して変更できる」ようになっていますが、一般には、こうしたアプリケーションのユーザー管理機能は特定のグループのユーザーだけが参照し変更できるようにするのが安全な利用方法です。
4ページが表示されました。「User Accounts」と「Group Accounts」の2つのテーブルが表示されています。
パスワードの変更はこの段階では行わないでください。変更するには、ページ冒頭にある「New Password」の右にあるテキストフィールドにパスワードを入力して、User AccountsのHashed Password列の該当するユーザーの「Set」ボタンをクリックします。この操作により、ソルトの自動生成やハッシュ値の計算などを自動的に行って、hashedpasswdフィールドへ正しい値を設定します。

設計内容を確認する

1このアプリケーションの定義ファイルを参照します。GitHubにあるソースコードを参照しましょう。こちらをクリックして、内容を参照します。
2認証の設定は次のセクションで説明します。ここでは関連テーブルの内容の把握が主な目的です。定義ファイルを見ると、次の5つのコンテキストが定義されています。nameキーの値と、()内にはviewおよびtableキーの値をピックアップしました。また、relationキーの有無も記載しました。
3ページの最初の方に、このファイルへのパス「INTER-Mediator/Auth_Support/MySQL_contexts.php」が見えている箇所があります。ここで、「Auth_Support」をクリックすると、Auth_Supportフォルダーの内容が表示されます。そこにあるページファイルの「MySQL_accountmanager.html」をクリックして、MySQL_accountmanager.htmlファイルの中身を表示します。
4User Accountsの見出しの下のテーブルを見てみます。authuserコンテキストのusernameやhashedpasswdフィールドなどが見えています。また5列目は、belonggroupコンテキストがSPANタグで展開されていますが、SELECTタグによるポップアップメニューは、belonggroupコンテキストの関連レコードdest_group_idを表示するようになっています。また、ポップアップメニューの選択肢はgroupnameコンテキストにより、authgroupテーブルのすべてのレコードが表示されています。
5列目は、結果として、そのユーザーが所属するグループの数だけポップアップメニューが並びます。つまりひとつのリピーターにひとつのポップアップメニューがあるので、該当するauthuserテーブルのidフィールドと同じ値をauthcorテーブルのuser_idフィールドに持つ関連付けられたレコードの数だけ繰り返されます。dest_group_idフィールドはauthgroupテーブルのidフィールドの値が入力されていますが、実際のグループ名に変換するのはポップアップメニューの仕組みを使っています。この点は、このすぐ後にauthcorテーブルの内容を確かめるので、その時に改めて内容を確認すると良いでしょう。
authuserコンテキストのbelongingフィールドは、元のテーブル定義にはありません。これは、サーバー側のPHPプログラム(同じフォルダーにあるUserList.php)によって、検索後にサーバーサイドの処理で付加されたフィールドです。この仕組みは、『Chapter 8 サーバーサイドでのプログラミング』で説明します。
5スクロールして、Group Accountsの見出しの下のテーブルを見てみます。authgroupコンテキストのgroupnameフィールドが見えています。2列目は関連付けられたgroupingroupコンテキストのdest_group_idフィールドの値を持つポップアップメニューが定義されています。そして、ポップアップメニューの選択肢はgroupnameコンテキストから得ています。2列目のポップアップメニューは、User Accountsテーブルの5列目と同様な構成になっています。

ユーザーとグループの対応関係を確認する

1「ユーザー管理ページサンプル」をクリックして表示した、ユーザー管理アプリケーションのページを表示します。通常は、以前に開いたタブあるいはウインドウがあるので、それを呼び出せば良いでしょう。
2User Accountsのうち、Groupsの列を見て、group1に含まれているのは、user1とuser2、user3の3つのアカウントであることを確認します。
3Group Accountsのgroup1は、さらにgroup3に含まれていることを確認します。
4User Accountsの中で、user1とuser2、user3は、Groupsの列を見る限りはgroup1にしか登録されていません。しかしながら、Belongingsの列を見ると、group1とgroup3に含まれていることになっています。つまり、group1に含まれるユーザーは自動的にgroup3にも含まれることになります。
5逆にgroup3に含まれるユーザーは、Belongingsの列にgroup3があるユーザーで、user1〜user5が相当します。言い換えれば、group3に対してアクセス権を与えることで、user1〜user5に与えるということができます。
6このように、グループは単にユーザーの分類ではなく、グループを所属させることによる階層的な構成をとることもできます。authuserとauthgroupテーブルの内容は、ページファイルに見える通りですが、authcorテーブルはこの状態では以下のような内容です。なお、フィールドの内容は実際にはidの整数値ですが、それだと分かりにくいので、対応するusernameあるいはgroupnameフィールドの値を()で付記しました。
user_idgroup_iddest_group_id
1(user1)NULL1(group1)
2(user2)NULL1(group1)
3(user3)NULL1(group1)
4(user4)NULL2(group2)
5(user5)NULL2(group2)
4(user4)NULL3(group3)
5(user5)NULL3(group3)
NULL1(group1)3(group3)
表7-2-6 authcorテーブルの値

演習のまとめ

セクションのまとめ

 INTER-Mediatorでの認証やアクセス権設定では、ユーザーやグループを使用します。それらは、authuser、authgroup、authcorのそれぞれのテーブルに記録しておくのが基本です。これらのテーブルの内容を編集するためのWebアプリケーションがINTER-MediatorのレポジトリのAuth_Supportフォルダーに作られています。さらに、認証をチャレンジ-レスポンスによって行うためのissuedhashテーブルも必要です。認証のための通信では、パスワードやそのパスワードから求められてデータベースに記録されたハッシュ値を通信経路上に流すことなく、認証が行われます。

7-3認証とアクセス権の設定

認証やアクセス権に対する設定は、params.phpファイルや定義ファイルに行います。これらの設定は、原則としてJavaScriptでカスタマイズすることはできません。言い換えれば、設定はクライアント側から変更することを一切できないようにすることで、セキュリティの確保を行っています。このセクションでは、設定方法と設定例を説明します。

認証バージョンの指定

 認証バージョンの指定については、params.phpファイルで行います。このファイルを利用した設定の全体像については、『2-6 設定ファイルparams.php』で説明します。params.phpファイル内では、以下のような認証に関連する変数設定を行う箇所があります。ここで、$passwordHash変数の行の頭の//を削除して、変数定義を有効にし、=の右側の値を '2' にすることで認証バージョン2で稼働します。新規に稼働するアプリケーションの場合には、 '2' に設定しておくのが良いでしょう。

リスト7-3-1 params.phpファイルの一部で認証バージョン指定等の箇所
/* Authorization
 * =================== */
//$passwordHash = '1';  // '2m' supports SHA-256 and Wrapping SHA-1 with SHA-256,
// '2' supports SHA-256 password hash only,
// No specification or other string support SHA-1, SHA-256, and wrapping.
//$alwaysGenSHA2 = true; // On the password changing, generate SHA-2 hash. The default is false.
//$migrateSHA1to2 = true;// If the login account relays on SHA-a, exchange it with 2m style SHA-2 hash. The default is false.
//$credentialCookieDomain = ""; // The domain information of the cookie for 'credential' auth. Falsy value means no domain, also the default.

 ここで、以前から認証バージョン1で運用したシステムを利用している方向けの情報を記述します。$passwordHashの値を無指定、あるいは1にすると、認証バージョン1で稼働するのはもちろんですが、認証バージョン2およびそれらの中間とも言えるマイグレーションモードでの稼働も行うようになっています。$passwordHashを'2m'にすると、マイグレーションモード、'2'にすると認証バージョン2のみでの運用となります。

 マイグレーションモードは、文字とおり、SHA-1とSHA-2との混在を示しています。ハッシュ値自体はSHA-1のものを利用しますが、通信上ではSHA-2によるハッシュを行うため、全面的にSHA-1ではないという状態です。ユーザのデータベースをそのままに可能な限りSHA-2で運用するというモードです。

 認証バージョン1で運用しているときに、$alwaysGenSHA2をtrueにする、つまりparams.phpのこの変数の定義部分のコメントを外すと、パスワードの変更を行ったときに、パスワードのハッシュ値は認証バージョン2に対応したものになります。つまり、認証バージョン1で運用を続けるものの、パスワード変更を行ったユーザはそれ以降はSHA-2対応のハッシュ値で計算がなされるようになります。さらに、変数$migrateSHA1to2をtrueにすれば、ログイン成功時に、そのユーザのパスワードはSHA-2に移行します。ログイン時にはパスワードが入力されているので、こうした自動変換が可能です。

定義ファイルでの設定

 認証やアクセス権に関する設定は、定義ファイルで行います。JavaScriptのAPIは用意していないので、原則として、すべて定義ファイル側で行うと考えてください。設定項目は多岐に渡ります。まず、リスト7-3-2に示したのは、定義ファイルのPHPによる記述で記述可能なキーをすべて示したものです。それぞれのキーに対する設定内容は、後で示します。IM_Entry関数の最初の引数(コンテキスト定義)に指定するコンテキスト定義に設定する項目があり、加えてIM_Entry関数の第2引数(オプション設定)に指定する項目もあります。

リスト7-3-2 定義ファイルでの認証やアクセス権設定の場所
IM_Entry(
	array(	//コンテキスト定義
		array(	
			"name"=>"context",	// コンテキスト名
			"authentication"=>array(
				"all" => array(
					"user" => array( .... ),
					"group" => array( .... ),
					"target" => "....",
					"field" => "....",
				),
				"read" => array(
					/* "all" と同様 */
				),
				"update" => array(
					/* "all" と同様 */
				),
				"create" => array(
					/* "all" と同様 */
				),
				"delete" => array(
					/* "all" と同様 */
				),
			),
			"protect-writing" => array( .... ),
			"protect-reading" => array( .... ),
		),
	),
	array(	//オプション設定
		"authentication"=>array(
			"user" => array( .... ),
			"group" => array( .... ),
			"user-table" => "string",
			"group-table" => "string",
			"corresponding-table" => "string",
			"challenge-table" => "string",
			"authexpired" => "string",
			"storing" => "cookie|cookie-domainwide|session-storage|credential",
			"realm" => "string",
			"email-as-username" => "boolean",
			"issuedhash-dsn" => "string",
		),
	),
....);

 定義ファイルエディターでのコンテキスト内の設定は図7-3-1のようなもので、「Show All」ボタンをクリックすることで表示されます。最初の状態では認証関連の設定は見えていません。このうち、リスト7-3-3で、「array(...)」つまり配列で指定が必要な項目については、半角のカンマ(,)で区切ってテキストを記述することで、それぞれを要素とする配列をキーに対する値として設定します。設定値については、表7-3-1に示します。表に記載の内容も、順次説明します。

図7-3-1 定義ファイルエディターでのコンテキスト定義内の認証関連設定
指定場所キーキー値と動作
コンテキスト/authenticationallCRUDのすべての操作に対する設定をまとめて与える
userユーザー名の配列を指定すれば、それらのユーザーのみに許可が与えられる。省略するとすべてのユーザー
groupグループ名の配列を指定すれば、それらのグループのみに許可が与えられる。省略するとすべてのグループ
target"table"あるいは省略の場合テーブルに対してアクセス権を設定、"field-user"あるいは"field-group"なら次のfieldキーの値で指定したフィールドにあるデータをユーザー名ないしはグループ名として、レコード単位でそのユーザーあるいはグループに対してのみアクセス権を付与する
fieldレコード単位のアクセス権設定において、ユーザー名やグループ名が入力されるフィールドの名前
readテーブルからの読み出し(クエリー)に対するアクセス権を設定する
user/group/target/field("all"の場合と同じ)
updateレコードへの更新に対するアクセス権を設定する
user/group/target/field("all"の場合と同じ)
createテーブルへの新規レコード作成に対するアクセス権を設定する
user/group/target/field("all"の場合と同じ)
deleteテーブルからのレコード削除に対するアクセス権を設定する
user/group/target/field("all"の場合と同じ)
media-handlingtrueを指定すると、メディア向けのチャレンジを生成してクライアントに送る
コンテキストprotect-writingフィールド名を配列で指定する。これらのフィールドはクライアントからの更新を受け付けなくなる
protect-readingフィールド名を配列で指定する。これらのフィールドはクライアントからの読み出しを受け付けなくなる
表7-3-1 コンテキスト定義内の認証関連設定

 定義ファイルエディターでのオプション設定にある認証関連の設定は図7-3-2のようなものです。こちらにも、userおよびgroupは配列での指定が必要であり、複数の要素を設定する必要がある場合にはカンマで区切って指定します。設定値については、表7-3-2に示します。表に記載の内容も、順次説明します。

図7-3-2 定義ファイルエディターでのオプション設定内の認証関連設定
オプション配列指定params.phpでの変数名既定値設定の説明
第1次元第2次元
'authentication''user'(未定義)なし利用可能なユーザを配列で指定。この記述がなければすべてのユーザにアクセス権。データベースのネイティブユーザで認証する場合には、値を「array('database_native')」と指定する。
'group'(未定義)なし利用可能なグループを配列で指定。この記述がなければすべてのグループにアクセス権
'authexpired'$authExpired3600認証が自動的に継続される時間を秒数で指定する。省略すると'3600'(1時間)。バージョン4.4以降では、0を指定すると有効期限はWebブラウザー終了時まで
(未定義)$defaultGroupNameなしグループに全く所属しないユーザの場合、ここで指定した文字列のグループに所属しているものとして扱う。このグループはauthgroupに登録されてなくても良い
'storing'$authStoring'credential'認証情報のクライアントへの保存を指定。'session-storage'ならブラウザのセッションストレージに保存、'credential'であれば認証情報をhttp-onlyのクッキーに保存する
'realm'$authRealm''認証領域名。ログインパネルの上部に表示される。また、認証情報を記憶するクッキーの名称の末尾に付与される。
'email-as-username'$emailAsAliasOfUserNamefalsetrueを指定すると、authuserテーブルのusernameと同時にemailフィールドも検索する。つまり、電子メールアドレスでの認証が可能になる。
(未定義)$passwordHash"1""1"なら、SHA-1、SHA-256互換、SHA-256のいずれも対応。"2m"なら、SHA-256互換、SHA-256のいずれも対応。"2"なら、SHA-256のみ対応。
(未定義)$alwaysGenSHA2false$passwordHashが"1"で、この変数がfalseの場合、パスワード変更するとSHA-1でハッシュする。この変数がtrueなら、パスワード変更時にSHA-256でハッシュする。$passwordHashが"1"以外の場合は、この変数に関係なく、パスワードを変更すると、SHA-256でハッシュをかける。
(未定義)$migrateSHA1to2falsetrueにすれば、SHA-1でハッシュされたパスワードでログインをした後、そのハッシュ値をSHA-256互換のハッシュにコンバートする。実質的に、SHA-1のパスワードを変更しないでSHA-256に移行できる。
is-required-2FA$isRequired2FAfalse二要素認証を有効にする。ユーザ名とパスワードの認証に加えて、メールで送られたコードの入力を必要とするため、authuserテーブルのemailフィールドに正いメールアドレスが入力されていることが必要
mail-context-2FA$mailContext2FAなし二要素目となるコードを送信するメールのコンテキスト。例えば、"mailtemplate@id=995"など。サンプルデータベースのmailtemplateテーブルにあるid=995の内容を参考にできる
expiring-seconds-2FA$expiring2FA1000二要素認証のコード入力が有効な時間を秒数で指定する。
digits-of-2FA-Code$ditigsOf2FACode4二要素認証のコードの桁数。なお、コードは数字のみを使う
(未定義)$fixed2FACodeなしここで数値によるコードを文字列で指定すると、二要素目の認証での正いコードは、常にここで指定したコードになる。二要素認証のテスト等で利用することを想定しており、実利用のためのものではない
表7-3-2 オプション設定内の認証関連設定
オプション配列指定params.phpでの変数名既定値設定の説明
第1次元第2次元
'authentication''user-table'(未定義)'authuser'ユーザ情報が保存されているテーブル名
'group-table'(未定義)'authgroup'グループ情報が保存されているテーブル名
'corresponding-table'(未定義)'authcor'ユーザとグループが対応づけられている情報が保存されているテーブル名
'challenge-table'(未定義)'issuedhash'チャレンジが保存されているテーブル名
'issuedhash-dsn'$issuedHashDSNなしissuedhashテーブルに対するDSNを指定する。例えば、FileMakerデータベースで、issuedhashテーブルをSQLiteで運用する場合に使える
'password-policy'$passwordPolicy''パスワード変更時に適用されるパスワードポリシー。useAlphabet useNumber useUpper useLower usePunctuation length(10) notUserName の各単語をスペースで区切って指定する。useAlphabetはアルファベットを使用していないといけなくする。その他、同様に単語から意味が分かるはずである。length(10)はパスワードは10文字以上にする必要があるという意味で、( ) 内には任意の数値を記述できる。
'reset-page'$resetPageなしログインパネルに、パスワードリセットページへのボタンを設置する。ここに、パスワードリセットページのURLを記載する。
'enroll-page'$enrollPageなしログインパネルに、ユーザー登録ページへのボタンを設置する。ここに、ユーザー登録ページのURLを記載する。
(未定義)$suppressDefaultValuesOnCopyfalseレコードの複製を行うときに、新規レコードの既定値を適用しないようにする。認証だけに関係するわけではないがリレーションシップのキーフィールドに複製が重複してかからないようにすることが期待できる。
(未定義)$suppressDefaultValuesOnCopyAssocfalse関連レコードの複製を行うときに、新規レコードの既定値を適用しないようにする。認証だけに関係するわけではないがリレーションシップのキーフィールドに複製が重複してかからないようにすることが期待できる。
(未定義)$suppressAuthTargetFillingOnCreatefalseレコードを作成するときに新規レコードの既定値を適用しないようにする。認証だけに関係するわけではないがリレーションシップのキーフィールドへの入力が重複してかからないようにすることが期待できる。
表7-3-3 認証動作に関する設定

 ユーザーやグループなど、同様な設定がコンテキスト定義にもオプション設定にもあります。もちろん、オプション設定は、定義ファイルで定義したすべてのコンテキストに対して適用されるのに対して、コンテキストへの定義はそのコンテキストだけに適用されるものです。ただし、すべての設定が両方にあるわけではなく、実運用を考慮してオプション設定には全体的な設定、コンテキスト定義には詳細な設定が存在するようにしました。

 storingキーには多様な選択肢がありますが、Ver.11現在で新たなアプリケーションを開発する場合は、credentialにして、http-onlyのクッキーに認証情報を記録する方法を採用してください。この方法では認証情報はJavaScriptから取得ができなくなり、安全性がいくらか高くなります。それ以外の方法ではJavaScriptから認証情報を取り出せるため、何らかのアタックに引っかかった場合に認証情報が第三者に取り出される可能性はあります。

設定例による認証の設定

 設定の詳細も重要ですが、複雑なので、ここと次のセクションで、用途に応じてどのような設定を行うのかを例で示します。以下の表記は定義ファイルでのPHPでの記述を行います。なお、いずれも、『7-2 ユーザー認証とアクセス権適用を行う仕組み』で説明したテーブルが用意されていて、ユーザーやグループが定義されていることを前提として説明します。

すべてのコンテキストで認証が必要

 あるページにおいて、認証を必要とするものの、認証さえできれば、ページ内での読み書きすべてができるといった、非常にシンプルな動作の場合は、リスト7-3-3のように、オプション設定にauthenticationキーの値さえあれば構いません。なお、定義ファイルでは、値が「array()」という設定はできません。そのため、storingキーで認証継続の設定や、authexpiredキーで認証継続の時間等をデフォルトと同じでもいいので設定をして、authenticationキーに要素のある配列が設定されるようにします。

リスト7-3-3 すべてのコンテキストで認証が必要になる定義ファイル
IM_Entry(
	array(	/* コンテキストにauthenticationの指定はなし*/	),
	array(
		'authentication' => array( ),	//項目のみ
	),
	...
)

特定のユーザーのみがログインできる

 ページ全体にわたって、特定のユーザーのみがログインして利用できるようにするには、リスト7-3-4のように、オプション設定のauthenticationキーに、userキーの配列を指定します。この設定は定義ファイルエディターではuserの項目に「user3, user4」のように指定をします。カンマの前後の空白は無視します。したがって、読みやすくするために自由に空白を入れることができます。ここでは、user3ないしはuser4はログインができて、ログイン後は読み書きの処理が全て許可される状態になります。その他のユーザーは、たとえパスワードが正しくても、認証エラーとなってログインができず、データ処理は一切できない状態になります。なお、この目的だけなら、オプション設定のauthenticationキーは不要ですが、認証継続の方法などの指定が必要ならば、オプション設定の記述を行います。

リスト7-3-4 特定のユーザーのみがログインできる定義ファイル
IM_Entry(
	array(	/* コンテキストにauthenticationの指定はなし*/	),
	array(
		'authentication' => array(
			'user' => array( 'user3', 'user4' ),
		),
	),
	...
)

特定のグループのユーザーのみがログインできる

 ページ全体にわたって、特定のグループに所属するユーザーのみがログインして利用できるようにするには、リスト7-3-5のように、オプション設定のauthenticationキーに、groupキーの配列を指定します。この設定は定義ファイルエディターではgroupの項目に「group1, group2」のように指定をします。カンマの前後の空白は無視します。ここでは、group1ないしはgroup2に所属するユーザーはログインができ、ログイン後は読み書きの処理が全て許可される状態になります。その他のユーザーは、たとえパスワードが正しくても、認証エラーとなってログインができず、データ処理は一切できない状態になります。なお、この目的だけなら、オプション設定のauthenticationキーは不要ですが、認証継続の方法などの指定が必要ならば、オプション設定の記述を行います。

リスト7-3-5 特定のグループのユーザーのみがログインできる定義ファイル
IM_Entry(
	array(	/* コンテキストにauthenticationの指定はなし*/	),
	array(
		'authentication' => array(
			'group' => array( 'group1', 'group2' ),
		),
	),
	...
)

ログインの継続方法と設定

 storingの設定がcredentialの場合は、ログインパネルでログインをした結果、その後にログイン状態を続けるためにクッキーを利用しています。同一の認証空間で、複数のページがある場合、あるページで認証したら、その状態を継続して別のページでも利用できないとかなり不便です。INTER-Mediatorはクッキーで認証情報を記録しており、ページ内はもちろん、ページ間をまたいでの認証継続も可能です。その場合、同一のissuedhashテーブルを使っている範囲が「同一の認証空間」となります。なお、authexpiredキーの値が経過すると、認証は不成立となります。この設定を省略すると、3600秒つまり1時間と成ります。この設定は、最後にアクセスしてからの時間であり、「何もしない時間がauthexpiredだけあればクッキー情報は消える」ということになります。

 クッキーやセッションストレージに記録される情報を表7-3-4にまとめました。なお、realmキーの値を設定すると、これらのキーの後にアンダーラインに続いて指定されます。realmが「Sample」なら、最初のキーは「_im_username_Sample」となります。異なるサイトで別々に認証したいのは当然なので、通常はサイトに固有のrealmの値を指定する必要があるでしょう。

キー名記録内容
_im_usernameログインしているユーザー名
_im_clientidサーバー側で発行したクライアントID
_im_credential_token認証用のトークン(http-only)
_im_mediatoken画像等のメディア認証のための情報
表7-3-4 クッキーに記録される情報

 クッキーでの記録はブラウザーを終了しても時間が来るまで残るので、その意味では便利です。期間を1週間くらいにしておくと、ほぼずっと認証が継続されるようになります。クッキーを第三者に取り出される危険性は、通信の傍受と、任意のJavaScriptコードを実行される場合があります。傍受ができないように、必ずTLSを使用してクライアントとサーバー間の通信を暗号化してください。さらに、_im_credential_tokenについては、JavaScript側から参照したり書き換えができないクッキーですので、任意のコードを実行されても取り出すことはできません。もちろん、任意のコードが実行できる状態でも大丈夫と言えなくもないのですが、任意のコードを実行できる状態では他にたくさんのセキュリティ的なアタックが可能なので、そのような状態にしないようにするのが基本です。

 この章の最初でも説明したとおり、認証に関する大きな脅威はログイン中のブラウザーを他人が使うことです。あるユーザーがログインをしてサイトを利用している時、PC/Macを操作できる状態で席を立つとします。すると、別の人が開いているブラウザーを使用して、元からログインをした人になりすますことができます。この問題は、すべての認証を伴うWebサイトに発生する問題点であり、INTER-Mediatorも例外ではありません。もちろん、クリックするたびにパスワード入力をすれば防げるとはいえ、それではシステムを利用する気にはなれないでしょう。この問題は、「ログアウトしない」という点に集約できます。利用者が席をはずすときには、ログアウト、あるいはOSの機能を利用してロックするのが現在のPC/Macの利用の上では原則です。INTER-Mediatorではその対策はできないですが、それは他のフレームワークやシステムも同様です。

ユーザ名を電子メールアドレスにする

 通常は、authuserのusernameフィールドの値をユーザー名としますが、電子メールアドレスをユーザー名としたい場合もあります。その時は、オプション設定に、email-as-usernameキーと値を追加し、値はtrueにします。あるいはparams.phpファイルで変数emailAsAliasOfUserNameにtrueを代入します。これで、authuserテーブルのemailフィールドの電子メールアドレスをユーザー名として扱って認証ができます。ただし、内部的には、usernameの値を使うので、usernameは一意な値を指定しておく必要があります。電子メールでの認証を可能にした場合、電子メールアドレスをusernameフィールドの値に変換するためにデータベース処理が増えます。SQLサーバーはその程度であればあまり問題にならないでしょうが、FileMakerの場合はパフォーマンスに影響が出る可能性があるので、慎重に導入しましょう。

issuedhashテーブルのみPDO経由で利用する

 オプション設定にあるissuedhash-dsnキーには、issuedhashテーブルに対するDSNを別途指定することができます。params.phpファイルでは$issuedHashDSN変数で指定します。FileMakerデータベースではissuedhashテーブルのやり取りに多数のデータベース処理を行うと目に見えてパフォーマンスが落ちます。そこで、issuedhashテーブルをSQLiteで運用することができるように、こうした設定を追加しました。issuedhashテーブルは独立して利用できるように設計されています。authuserテーブルなどはFileMaker側に持ち、issuedhashテーブルだけをSQLiteで運用します。データベースを保存したパスなどを含むDSNを値として指定します。なお、SQLiteのデータベースを用意するには、INTER-Mediatorに含まれているdist-docs/sample_schema_sqlite.sqlを利用します。このファイルよりデータベースファイルを作り、それを利用することで問題ありません。他のテーブルは無視されます。こうして作成したSQLiteのデータベースを/var/db/imに置いたとすれば、issuedhash-dsnキーの値としては以下のように指定して利用することができます。

リスト7-3-6 issuedhash-dsnキーの指定例
'issuedhash-dsn' => 'sqlite:/var/db/im/sample.sq3',
リスト7-3-7 $issuedHashDSN関数の指定例
$issuedHashDSN = 'sqlite:/var/db/im/sample.sq3';

ログインパネルとカスタマイズ

 INTER-Mediatorではログインパネルを自動的に生成します。どのようなログインパネルなのかはこの後の演習の画面ショット等で確認してください。ログインパネルのページがあるわけではなく、既存のページで認証が要求されると自動的にオブジェクトをページに追加して、ログインパネルを表示します。背景が薄く見えるようなスタイル付けをしています。

 なお、ログアウトは、ページネーションコントロールに表示されるので、ページネーションの表示の定義が行われていれば、ログアウトボタンもページ上に表示されます。あるいは、以下の1行のプログラムを実行するボタン等を追加してください。なお、この1行の実行にも、ページファイルを読み込むSCRIPTタグが必要です。

リスト7-3-8 ログアウトを行うJavaScriptのプログラム
INTERMediatorOnPage.logout()

 INTER-Mediator標準のログインパネルの動作等をカスタマイズするためのJavaScriptのAPIをリスト7-3-9に紹介します。いずれも、INTERMediatorOnPage.doBeforeConstructメソッド(『8-5 ブラウザーを判断するページ』を参照)内など、ページ合成の前に実行します。設定が全てプロパティとなっているので、代入文による利用がほとんどでしょう。

リスト7-3-9 標準のログインパネルのカスタマイズ
//認証の失敗回数の変更(既定値は4)
INTERMediatorOnPage.authCountLimit = 2;

//パスワード変更の機能をログインパネルに出さない
INTERMediatorOnPage.isShowChangePassword = false;

//ログインパネルのスタイル設定をしない
INTERMediatorOnPage.isSetDefaultStyle = false;

//ログインパネルのRealm名を置き換える
INTERMediatorOnPage.authPanelTitle = 'なんとかシステム';

//ログインパネル自体を独自のHTMLに置き換える
INTERMediatorOnPage.loginPanelHTML = "....";
params.phpで変数名設定の説明
$extraButtons連想配列のキーがボタン名、値がリンク先のURLとして、連想配列を指定することで、ログインパネルに別のページを表示するボタンを作成できる。
$resetPageURLを文字列で指定すると、ログインパネルに「パスワードをリセット」のボタンが表示され、クリックするとそのURLへジャンプする。ボタン名のメッセージの番号は2023
$enrollPageURLを文字列で指定すると、ログインパネルに「ユーザー登録をする(要メールアドレス)」のボタンが表示され、クリックするとそのURLへジャンプする。ボタン名のメッセージの番号は2022
$authPanelTitleログインパネルに表示するタイトル
$authPanelTitle2FA二要素認証の入力パネルに表示するタイトル
$authPanelExpログインパネルに表示する説明文
$authPanelExp2FA二要素認証の入力パネルに表示する説明文
$limitEnrollSecondログインパネルに表示する説明文
$limitPwChangeSecond二要素認証の入力パネルに表示する説明文
表7-3-5 ログインパネルのカスタマイズに関する設定

 ログインパネルのカスタマイズとして、メッセージの文字列を入れ替える方法があります。表7-3-6に示した番号がそれぞれのメッセージに割り当てられています。例えば、「ログイン」ボタンは2004番のメッセージを使っています。このログインボタンのボタン名を変えるには、『システムが生成する文字列とそのローカライズ』に記載された方法を利用して行えます。ラベルの文字列だけを変えるにはこの方法が手軽です。

番号既定のメッセージ(日本語)既定のメッセージ(英語)
2002ユーザー名User:
2011ユーザー名(メールアドレス)User(Mail Address):
2003パスワードPassword:
2004ログインLogin
2005パスワード変更Change Password
2006新パスワードNew Password
2026SAML認証SAML Login
表7-3-6 ログインパネルに見えるメッセージの番号

 ログインパネルの各要素は、スタイルシートを利用して、スタイルの設定ができます。そして表7-3-7に示す、クラスあるいはID属性の要素に対して、スタイル設定を行います。もちろん、一部の設定のみを変更しても構いません。

タグ設定対象
<div class="_im_authpback">認証パネルの背景(画面全体)
<div class="_im_authpanel">認証パネル全体
<span class="_im_authrealm">ログインパネルのタイトル
<span class="_im_authlabel">「ユーザー名」「パスワード」のラベル
<input id="_im_username">ユーザー名のテキストフィールド
<input id="_im_password">パスワードのテキストフィールド
<button id="_im_authbutton">ログインボタン
<div class="_im_login_message">ログイン失敗時のメッセージ
<div class="_im_auth_exp">ログインパネルのメッセージ
<span class="_im_newpassword">「新パスワード:」のラベル
<input id=_im_newpassword">新しいパスワードのテキストフィールド
<button id="_im_changebutton">パスワード変更ボタン
<div class="_im_newpass_message">パスワード変更のメッセージ
<div id="_im_progress">回転するギアを表示するウインドウ全体のパネル(z-indexにより_im_authpbackより背後に配置)
表7-3-7 ログインパネルのスタイル設定のためのセレクタ

 カスタムログインパネルは、params.phpファイルで$customLoginPanel変数にHTMLの文字列を定義しても構いません。指定したHTMLにログインパネルを置き換えます。ここでのHTMLは、HTMLタグから始まるものではなく、通常はDIVタグで開始することになると思われます。この時、HTML内では、表7-3-7でidの要素が含まれているタグを内部に記述するようにします。例えば、「<input type="text" id="_im_username"/>」などの要素が含まれている必要があります。少なくとも、_im_username、_im_password、_im_authbuttonは必要ですが、パスワードの変更が不要ならそれに関連する要素は用意する必要はありません。また、FORMタグは必要ありません。

パスワードのリセット

 パスワードのリセットは、ログインパネルにその機能を含めてあります。この時、現在のパスワードを正しく入力しないと、パスワードの変更はできません。

 現在の自分のパスワードが分からなくなってしまった場合は、事前に登録してある電子メールアドレスへのメッセージを利用してパスワードをリセットする機能もINTER-Mediatorで利用できますが、PHPを利用したプログラミングが必要です。こちらについては、『7-6 メールを利用したユーザー登録とパスワードのリセット』で説明を行います。

演習認証の実現とパスワード変更

 この演習では、認証が必要なページを作成するための必要な記述と、実際の認証の動作を確認します。設定により、INTER-Mediatorによってログインパネルが表示されてログインができるようになります。また、ユーザーが自分自身のパスワードを変更する方法を説明します。

最初のページ用の定義ファイルに必要な設定を行う

1演習環境を起動します(『1-2 演習を行うための準備』を参照)。続いて、ブラウザーで、「http://localhost:9080」に接続します。「トライアル用のページファイルと定義ファイル」というタイトルの部分を特定します。
2「def22.phpを編集する」をクリックし、定義ファイルエディターでdef22.phpファイルを編集します。(もし、22番を他の用途に使ってしまっていれば、別の番号のファイルを利用してください。異なる番号のセットを利用した場合、ソースコードの記述が変わる部分がありますが、以下の手順では可能な限り注記します。)
3Contextsの中のQueryと書かれた背景がグレーの部分を特定します。そして、その次の行の右の方にある「削除」をクリックして、Queryの設定がある行を削除します。
4「レコードを本当に削除していいですか?」とたずねられるので、OKボタンをクリックします。
5同様に、Sortingの次の行にある「削除」ボタンを押し、確認にOKボタンをクリックして、こちらの設定も削除しておきます。
6Contextsでは、name、table、viewに「invoice」、keyに「id」、pagingに「true」repeat-controlに「confirm-insert confirm-delete」、recordsおよびmaxrecordsに「1000」と指定します。Contextsにあるその他のテキストフィールドは空白にします。この設定によりinvoiceテーブルを表示し、ページナビゲーションは表示しますが、ページの移動はおそらく発生しないくらいの1ページのサイズになります。
7Database Settingsに設定を行います。
[MySQL]の場合
db-classは「PDO」のままでかまいません。dsnに「mysql:host=db;dbname=test_db;charset=utf8mb4」と入力します。そして、userに「web」、passwordに「password」と入力します。
[FileMaker]の場合
db-classを「FileMaker_DataAPI」に書き換えます。databaseは「TestDB」、userに「web」、passwordに「password」、serverに「gateway.docker.internal」、portに「443」、protocolに「https」、cert-vefifyingに「false」と入力します。
8Debugについては、「false」にすると、デバッグ情報が出なくなります。なお、デバッグ情報をみながら動作を確認したい方は、「2」のままにしてこの後の作業を行ってください。

最初のページのページファイルの作成

1「http://localhost:9080」で開いたページに戻り「page22.htmlを編集する」をクリックし、ページファイルのpage22.htmlを編集するページファイルエディターを開きます。HTMLでの記述内容を以下のように変更します。太字が追加する箇所を示します。(別の番号のセットで作業している場合には、該当する番号のリンクをクリックしてください。番号にかかる部分は、SCRIPTタグのプログラムの内部にもあります。)
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title></title>
  <script type="text/javascript" src="def22.php"></script>
</head>
<body>
<div id="IM_NAVIGATOR"></div>
<table>
  <tbody>
    <tr>
      <th>作成日</th>
      <td><input type="text" data-im="invoice@issued"/></td>
	<td></td>
    </tr>
    <tr>
      <th>タイトル</th>
      <td><input type="text" data-im="invoice@title"/></td>
      <td></td>
    </tr>
  </tbody>
</table>
</body>
</html>
2「http://localhost:9080」で開いたページに戻り「page22.htmlを表示する」をクリックします。(別の番号のセットで作業している場合には、該当する番号のリンクをクリックしてください。)初期状態では3レコードが作られていますが、別の演習でレコードが増減しているかもしれません。ここでは内容はなんでも構いませんので、レコードが見えていることをまずは確認してください。

ページを開くのに認証を必要とするように変更する

1def22.phpを定義ファイルエディターで編集します。すでにタブあるはウインドウで見えている場合はそれを呼び出します。閉じてしまった場合には、「http://localhost:9080」で開いたページに戻り「def22.phpを編集する」をクリックします。
2ページの最初にある「Show All」ボタンをクリックして、設定項目を全て表示します。そして、ページの最後の方に移動し、Optionsの中の「Authentication and Authorization」の「storing」に「credential」と入力します。Tabキー等をクリックして別のテキストフィールドに移動し、確実に設定を保存してください。
3page22.htmlを表示します。すでにタブあるはウインドウで見えている場合はそれを呼び出します。そして、ブラウザーの更新ボタンをクリックして、画面の更新を行ってください。閉じてしまった場合には、「http://localhost:9080」で開いたページに戻り「page22.htmlを表示する」をクリックしてページを開きます。
4すると、ログインパネルが表示されます。このログインパネルは、INTER-Mediator標準のもので、フレームワークが生成しています。
5まずは、正しいユーザー名とパスワードを入力してみます。ユーザー名とパスワードいずれも「user1」と入力して、「ログイン」ボタンをクリックします。
6ページが表示され、データベースの内容が表示されています。見えている内容が、認証を必須にする前と同様であれば問題はありません。
ここではすでに認証に必要なテーブルが用意されていて、ユーザーも最初から設定されているので、ページの定義ファイルに認証が必須になるような設定を追加するだけで認証ができるようになっています。作業は簡単ですが、実際の開発ではテーブルを用意したりといった作業が発生する点は知っておきましょう。
7ページナビゲーションには、ログインしているユーザーとして「user1」が見えています。また、「ログアウト」ボタンも見えています。「ログアウト」ボタンをクリックして、ログアウトしておきます。

認証とパスワード変更の動作を確認する

1ログアウトすると、またログインパネルが表示されます。ここで、ユーザー名は「user1」として、パスワードに空欄あるいは「user1」以外の間違えたパスワードを入力して、「ログイン」ボタンをクリックします。赤い文字でエラーが表示されログインができず、ページの内容やデータベースのデータは表示されません。
2間違ったパスワードを何度も入力してみてください。5回目に認証エラーとなって、これ以上作業はできなくなります。パスワードを空欄で入力した場合はカウントされませんので、適当に間違えたパスワードを何か入力してください。
3ブラウザーの更新ボタンをクリックして画面を更新します。するとログインパネルがまた表示されます。続いて、パスワードの変更を行ってみます。
4ユーザー名に「user1」と入力します。パスワードは「user1」以外の文字列を入力します。つまり、パスワードは違っている状況であるとします。「新パスワード」に「1234」と入力します。「パスワード変更」ボタンをクリックします。すると、パスワードの変更に失敗したことがレポートされています。この状態ではパスワードは変わっていません。
5ユーザー名とパスワードに、いずれも「user1」と入力します。つまり、正しいユーザー名とパスワードを入力します。「新パスワード」に「1234」と入力します。パスワードはなんでも構いませんが、以下の手順をこの通り進めるために、ここでは「1234」と入力してください。そして「パスワード変更」ボタンをクリックします。パスワード変更が成功したメッセージが表示されます。

認証の継続の様子を確認する

1ユーザー名「user1」、パスワード「1234」でログインを行って、認証された状態でページを表示します。ここでページのウインドウあるいはタブを閉じます。
2「http://localhost:9080」で開いたページで「page22.htmlを表示する」をクリックしてページを再度開きます。
3元のページが表示されており、認証が成功したことを示しています。認証情報はクッキーで保存されているため、ページを閉じても認証情報は残っており、指定した一定時間内であれば認証は継続するようになっています。

認証の継続の仕組みを確認する

1クッキーにどのような情報が記録されているかを確認してみます。以下の操作はブラウザの「デベロッパーツール」を使います。場合によってはクッキーの情報は、ここでの画面ショットを見るだけでも結構です。
2デベロッパーツール(Safariでは「Webインスペクター」)を表示して、上のタブで、「ストレージ」を選択します。左側でCookieを選択すると、現在のページに設定されたクッキーが一覧されています。
3_im_usernameはユーザー名が設定されます。_im_clientidは、クライアントごとにサーバから振られるランダムな番号です。_im_credential_tokenは認証が成功した時にサーバ側から設定され、この値をサーバに接続するごとに送信して、以前に認証が成功したことを伝達します。この値は、第三者に漏れるとそのユーザーとしてサーバーにアクセスできてしまう可能性があるものですが、HttpOnlyのチェックがついており、クライアントのJavaScriptからは読み出すことができません。また、通常はドメイン制限をしているので、他のサイトに移動したときに取り出すこともできません。HTTPの通信を暗号化したHTTPSで通信していれば、第三者による取得が可能な明確な方法はないと言えます。
4データベースに保存されているhashedpasswdフィールドの値を調べてみます。データベースエンジンによって操作が違います。
5[MySQLの場合]「http://localhost:9080」のアドレスのページを開き、「ユーザー管理ページサンプル」をクリックして、ユーザー管理アプリケーションを起動します。ログパネルでは、ユーザー名に「user1」、パスワードに「1234」を入力して、認証を行います。user1のHashed Passwordの列の値と、_im_credential_tokenの値は違うものになっています。
6[FileMakerの場合]FileMaker Proで、Test_DBデータベースを開き、「authuser」レイアウトを開きます。そこで、該当するユーザーのhashedpasswdフィールドの値を確認してください。
セキュリティ上で考慮しなければならない点は、すでに説明しています。認証情報をクッキーに入れる場合にはクッキーのデータがHTTPのやり取りでネットワークを流れるので、少なくとも、TLSを利用して、通信を傍受されないようにすることは必須です。

演習のまとめ

メールを利用したユーザー登録

 INTER-Mediatorには、メールを利用してのユーザー登録や、メールを利用したパスワードのリセットを行う仕組みを組み込む最低限の機能を持ったファイルを、samples/Auth_Support/User_Enrollmentおよびsamples/Auth_Support/PasswordResetフォルダーに入れています。こちらについてはサーバーサイドのプログラムについての全般的な知識が必要なので、『7-6 メールを利用したユーザー登録とパスワードのリセット』でまとめて説明をします。

パスワードポリシーの定義

 params.phpファイルで$passwordPolicy変数を定義して、パスワードのルールを指定できます。表7-3-8に設定可能なキーワードを示します。これらの文字列を空白で区切ってひとつの文字列で指定します。例えば、"useNumber useAlphabet length(10)" は、指定したパスワードに、数字、アルファベットが必ず含まれ、かつ10文字以上である必要があることを定義します。

キーワード適用されるルール
useAlphabetアルファベットが必ず入っている
useNumber数字が必ず入っている
useUpperアルファベットの大文字が必ず入っている
useLowerアルファベットの小文字が必ず入っている
usePunctuation記号類が必ず入っている
length(n)長さがn文字以上
notUserNameユーザー名と同一ではいけない
表7-3-8 パスワードのルールに指定できる文字列

 なお、この設定は、authuserテーブルに単にパスワードを指定するときには適用されません。初期パスワードをSQLステートメントのテキストで与えるような場合には、ここで指定したルールに合致しているかどうかの判定は行われません。ここで指定したパスワードのルールは、ログインパネル上でパスワードの変更したときや、ログインパネルのパスワード変更処理で呼び出されるJavaScriptあるいはPHPのメソッドを利用した場合に、ルールが適用できます。したがって、このルール適用は、パスワード変更時に適用されるという理解で問題はありませんが、初期値がルール通りかどうかは開発者あるいはシステム管理者が決定して必要なら自身でチェックをしてください。

このセクションのまとめ

 認証のための設定は、多くは定義ファイル上で行うことで実現可能です。むしろ、データベースにユーザーやグループのテーブルを用意する方がよほど手間がかかる仕事と言えるでしょう。このセクションでは認証を実現する方法やそこでのさまざまな設定、ログインパネルやそのカスタマイズなどを説明しました。しかしながら、認証を伴うアプリケーションはセキュリティ上の問題を抱えやすいシステムでもありますので、どういう原理で認証が稼働していて、やってはいけないことや、運用上どうしても必要になることに対してなぜそのような結果になっているのかをなるべく正しく理解をするようにしましょう。

7-4コンテキストにおけるアクセス制御

アクセス権の制御は、基本となる4つのデータ操作のCRUD(Create Read Update Delete)について、それぞれ、どのユーザーあるいはグループに対して許可しているのかをコンテキスト定義で規定します。操作ごとのアクセス権は、オプション設定では指定はできず、個々のコンテキスト定義で行います。

設定例によるアクセス権の設定

 前のセクションに続いて、用途に応じてどのような設定を行うのかを例で示します。以下の表記は定義ファイルでのPHPでの記述を行います。なお、いずれも、『7-2 ユーザー認証とアクセス権適用を行う仕組み』で説明したテーブルが用意されていて、ユーザーやグループが定義されていることを前提として説明します。また、設定の詳細は、『7-3 認証とアクセス権の設定』の最初の部分に表で示してあります。

読み出しはできて書き込みができないコンテキスト

 読み出しはできても書き込みを一切できないようにしたい場合、認証の設定を使わない方法もあります。リスト7-4-1のように、コンテキストのviewキーの値には存在するビューやテーブル名を指定し、tableキーの値には存在しないエンティティ名(ビューあるいはテーブルの名前)を指定します。authenticationキーの設定はありません。もちろん、その状態で書き込みをしてもエラーになるということで何も起こらないことを意図したものです。ユーザーインターフェースで書き込み可能なものを配置しないだけでは、定義ファイルへネットワークを通じて直接アクセスされた場合に書き込みや更新ができてしまう可能性があります。この設定は、そうした意図しないアクセスからも、書き込み処理を排除してデータを保護します。単に読み出しだけでいいようなコンテキストは認証の有無にかかわらずよくあり、安全面からも、なるべくtableキーの値に存在しないエンティティ名を指定するようにすべきです。また、認証がされていても書き込みを排除したい場合には、この手法は有効です。

リスト7-4-1 読み出しはできて書き込みができないコンテキスト定義
IM_Entry(
    array(
        array(
            'name' => 'person',
            'view' => 'person_view',	//データベースには存在する
            'table' => 'dummydummy',	//データベースには存在しない
            'records' => 1,
            'key' => 'id',
        ),
    ),
    array( ... ),
    ...
);

特定のユーザーが読み込みだけの権限でログインできる

 特定のコンテキストで、認証を必要とし、加えて書き込みができないようにするには、リスト7-4-2のような定義ファイルを作成します。この例では、コンテキストのtableキーに存在しないテーブル名を指定して、書き込み処理がエラーになるようにしています。そして、adminユーザーは認証して読み出しができますが、書き込みができるユーザーは誰もいないという状態になります。この時、この定義ファイルの他に、authenticationキーのないコンテキストがあれば、どのユーザーもログインを行うことなくデータベースを利用できます。

リスト7-4-2 特定のユーザーが読み込みだけの権限でログインできるコンテキストを含む定義ファイル
IM_Entry(
	array(	
		array(
			'name' => 'mycontext',
			'view' => 'mycontext',
			'table' => 'dummydummy',	//実在しないテーブル名
			'authentication' => array(
				'read' => array(
					'user' => array( 'admin' ),
				),
			),
		),
	),
	array(	/* 'authentication' キーはなし */ ),
	...
)

認証したユーザーに対して読み出しと更新のみを許可するコンテキスト

 データベースの4つの主要な処理であるCRUDそれぞれに対するアクセス権を、コンテキスト定義の中で設定することができます。この時、read、update、create、deleteの4つの操作をすべて記述してください。そして、その中で、userあるいはgroupキーで許可を与えるユーザーやグループを配列で与えます。それぞれの操作で一切許可しないものは、架空のグループを割り当てておけば良いでしょう。リスト7-4-3のコンテキストmycontextでは、adminsグループのユーザーであればログインをして、読み出しと更新処理はできますが、dummyグループは存在しないグループなので、レコード作成や削除ができなくなります。ここでは、viewとtableキーがないので、「mycontext」テーブルに読み書き処理が行われます。

リスト7-4-3 認証したユーザーに対して読み出しと更新のみを許可するコンテキストを含む定義ファイル
IM_Entry(
	array(	
		array(
			'name' => 'mycontext',
			'authentication' => array(
				'read' => array('group' => array( 'admins' ),),
				'update' => array('group' => array( 'admins' ),),
				'create' => array('group' => array( 'dummy' ),),
				'delete' => array('group' => array( 'dummy' ),),
			),
		),
	),
	array( /* 'authentication' キーはなし */ ),
	...
)

認証したユーザーに対して全ての操作を許可するコンテキスト

 CRUDのそれぞれの操作に対する設定が全て同じであるのなら、allキーを使って1行で指定が可能です。リスト7-4-4のコンテキストmycontextでは、adminsグループのユーザーであればログインをして、データベースの全ての処理ができます。

リスト7-4-4 認証したユーザーに対して全ての操作を許可するコンテキストを含む定義ファイル
IM_Entry(
	array(	
		array(
			'name' => 'mycontext',
			'authentication' => array(
				'all' => array('group' => array( 'admins' ),),
			),
		),
	),
	array( /* 'authentication' キーはなし */ ),
	...
)

コンテキストでのフィールド単位の制限

 コンテキスト定義の中では、特定のフィールドに対する書き込みや読み出しできないようにする設定が可能です。リスト7-4-5のように、protect-writingあるいはprotect-readingというフィールドの設定ができます。例えば、外部キーフィールドや、次のセクションで説明するユーザー名のフィールドに対して更新できないようにしたり、検索やソートでは使うものの読み出しはできないようにしたいフィールドがあれば、その名前を配列で指定します。

リスト7-4-5 コンテキストでのフィールド単位の制限を含む定義
IM_Entry(
    array(
        array(
            'name' => 'person',	// view, tableは省略
            'records' => 1,
            'key' => 'id',
            'protect-writing' => array( 'id' ),
            'protect-reading' => array( 'username' ),
        ),
    ),
    array( ... ),
    array( ... ),
    false
);

演習コンテキストにアクセス権を設定する

コンテキストに対するアクセス権の設定を行ってみます。ここでの演習は、『7-3 認証とアクセス権の設定』の『認証の実現とパスワード変更』で作成したページを続けて利用します。データベースの4つの処理に対して、それぞれ可能なユーザーやグループの設定ができます。ここではグループの設定と、権限がない場合にはデータベース処理がなされないことを確認します。

アクセス権の設定と動作を確認する

1前のセクションの演習から続けて作業を行う場合には、いったんWebブラウザーを終了して、改めて起動し、「http://localhost:9080」に接続して演習環境のホームを表示してください。
2def22.phpを定義ファイルエディターで編集します。すでにタブあるはウインドウで見えている場合はそれを呼び出します。閉じてしまった場合には、「http://localhost:9080」で開いたページに戻り「def22.phpを編集する」をクリックします。
3ページの最初にある「Show All」ボタンをクリックして、設定項目を全て表示します。そして、コンテキストの途中にある「Authentication, Authorization and Security」で入力を行います。readのgroupに「group2」、updateのgroup、createのgroup、deleteのgroupに存在しないグループである「dummy」を指定します。 最後にTabキーを押して、4つのテキストフィールドに対応するフィールドの更新を確実に行うようにします。
ここでの設定は、このコンテキストからの読み出しはgroup2に対して許可するが、その他の更新や新規レコード、レコード削除はdummyグループに対してのみ許可することを意味します。しかしながら、dummyグループは存在しないので、すべてのユーザーは更新などの書き込み処理はできません。コンテキストのユーザーやグループの設定を行うときには、4つのデータベース処理のすべてに対して設定を行ってください。空欄にすると、設定がないものとみなしてしまいます。
4page22.htmlを表示します。すでにタブあるはウインドウで見えている場合はそれを呼び出ます。そして、ブラウザーの更新ボタンをクリックして、画面の更新を行ってください。閉じてしまった場合には、「http://localhost:9080」で開いたページに戻り「page22.htmlを表示する」をクリックしてページを開きます。
5ログインパネルが表示されるので、ユーザー名とパスワードに「user2」と入力して、「ログイン」ボタンをクリックします。user2はgroup2には所属していないために、正しいパスワードを入力してもページの表示は行いません。つまり、アクセス権が設定されていないので、認証はできても認可エラー扱いにして、再度認証パネルが表示されます。
6group2に所属する「user4」でログインします。ユーザー名もパスワードも「user4」です。すると、データベースの内容を表示することができました。ログインユーザーがuser4であることを確認してください。
7ここで、適当なテキストフィールドのデータを変更してTabキーを押して、更新を行います。ここで、このコンテキストのupdateのgroupに存在しないグループのdummyが設定されていることを思い出してください。
8ページが表示されますが、データは修正されていません。ブラウザーの更新ボタンをクリックして、画面を更新し、データベースの値を再度取り出してみます。すると、フィールドに見えている値は修正前の値になっています。したがって、実際には、データベースの更新はアクセス権がないために行われませんでした。
更新できない時の動作としてはあまりスマートな感じではありませんが、ここでの演習は、「コンテキストにアクセス権を設定すれば、更新が阻止できる」ことを示すためのものです。もし、あるデータベースフィールドの内容を表示しかしないのであれば、ここで作ったページのようにテキストフィールドを利用することはあり得ません。DIVやSPANタグで表示をすれば済みます。しかしながら、利用者がサーバー通信を独自に構築して更新をしようとしても、コンテキストの設定で阻止ができることをこの演習で確認していただきました。コンテキストの設定はサーバー側で記録されているので、コンテキストのアクセス権の設定はユーザーには変更できず、アクセス権の設定は設計者の意図通りに機能します。

レコード作成と削除の権限がない場合の動作

1def22.phpを定義ファイルエディターで編集します。すでにタブあるはウインドウで見えている場合はそれを呼び出します。閉じてしまった場合には、「http://localhost:9080」で開いたページに戻り「def22.phpを編集する」をクリックします。
2ページの最初にある「Show All」ボタンをクリックして、設定項目を全て表示します。そして、コンテキストの途中にある「Authentication, Authorization and Security」にあるupdateのgroupに「group2」を指定します。Tabキーを押して、テキストフィールドに対応するフィールドの更新を確実に行います。
3page22.htmlを表示します。すでにタブあるはウインドウで見えている場合はそれを呼び出ます。そして、ブラウザーの更新ボタンをクリックして、画面の更新を行ってください。閉じてしまった場合には、「http://localhost:9080」で開いたページに戻り「page22.htmlを表示する」をクリックしてページを開きます。
4適当なフィールドの内容を変更してTabキーを押し、データの更新を行います。
5問題なく、更新ができました。今度は、group2に対して更新の権限が与えられているので、group2に所属するuser4での更新ができたということです。
6何からのレコードの「削除」ボタンをクリックして、レコード削除を試みます。確認のダイアログボックスでは、OKをクリックします。
7ページが再度表示されていますが、削除したレコードはそのままで削除はされていません。ブラウザーの更新ボタンをクリックして、ページを再表示しても、削除しようとしたレコードは削除されずに残っています。つまり、user4には削除の権限が与えられていないので、レコード削除ができないのです。
8ページネーションコントロールにある「レコード追加: invoice」をクリックして、レコードの追加を試みます。ダイアログボックスが表示されれば、OKをクリックします。以降、適当にダイアログボックスへ対処してください。
9ページが再表示されますが、レコードの個数を見ても、レコードが新しく作れていないことは確認できます。ブラウザーの更新ボタンをクリックしても、レコードが追加された形跡はありません。user4にはレコード作成の権限が与えられていないのです。
レコード作成や削除ができない時の動作としてはあまりスマートな感じではありませんが、ここでの演習は、「コンテキストにアクセス権を設定すれば、レコード作成や削除が阻止できる」ことを示すためのものです。もし、あるコンテキストに対してレコード作成や削除をしないのであれば、ここで作ったページのように作成や削除のボタンを表示することはあり得ません。しかしながら、利用者がサーバー通信を独自に構築してレコードの作成や削除をしようとしても、コンテキストの設定で阻止ができることをこの演習で確認していただきました。コンテキストの設定はサーバー側で記録されているので、コンテキストのアクセス権の設定はユーザーには変更できず、アクセス権の設定は設計者の意図通りに機能します。

演習のまとめ

このセクションのまとめ

 コンテキスト定義において、CRUDの各操作が可能なユーザーやグループの指定ができるので、特定のユーザーに対するアクセス権の付与が可能です。また、tableキーの値に存在しないテーブル名を与えて、書き込み処理を阻止するという手段も利用できます。

7-5レコード単位のアクセス権とメディアデータのアクセス権

同一のテーブルでレコードごとに違うユーザーに読み書き権限を与えて運用できます。そして、各レコードは他のユーザーからは操作できないように保護されています。また、写真のファイル等の読み出しにおいて、レコード単位のアクセス権を適用することもできるので、画像などもユーザーごとに保護した状態で参照ができます。

レコード単位のアクセス権

 同一のテーブルの個々のレコードに対してアクセス権を設定するためには、そのレコードが誰に対してアクセス権を持っているのかを何らかの方法で記録が必要です。そのために、そのテーブルに、ユーザー名あるいはグループ名を記憶するフィールドを設けて、そのフィールドに設定されているユーザーあるいはグループに対しての権限が与えられるような動作をフレームワークが行います。

 設定はコンテキスト定義のauthenticationキーの配列の中で、操作名をキーにした配列で、targetキーとfieldキーを指定します。targetキーは、「field-user」ならfieldキーで指定したフィールドにある名前のユーザーに対して権限が与えられます。targetキーの値が「field-group」ならfieldキーで指定したフィールドにある名前のグループに対して権限を与えます。targetキーが「table」あるいは省略の場合には、テーブル全体で同一のアクセス権の設定となります。

 fieldキーに指定するフィールドは文字型にします。そのフィールドには、authuserテーブルのusernameフィールドの文字列を入力します。主キーとなるidフィールドの値ではなく、ユーザー名の文字列を入力します。また、電子メールをユーザー名に使う場合でも、対応するusernameフィールドの値を設定します。

 クエリーやレコード削除、更新の場合の検索条件に、ANDで「fieldのフィールド名=ログイン中のユーザー名」という検索条件を付与します。したがって、全ての操作で、他のユーザーのレコードに対してデータの削除・更新が行われることはありません。また、新規にレコードを作成するときに、fieldで指定したフィールドにユーザー名を自動的に設定します。こうして、レコードの生成から更新、削除に至る全ての場面で、ログインしているユーザーに権限のあるレコードだけが処理対象になるということです。また、言い換えれば、他人のレコードを削除しようとしても認証しない限りはできないのです。ただし、このAND演算がポイントであるため、FileMakerの場合に検索条件をORで構成している場合には、レコード単位のアクセス権の設定は適用できません。

 リスト7-5-1はレコードごとのアクセス権を、chatというコンテキストに対して設定しています。オプション領域には、user1あるいはgroup2に所属するユーザーのみが、まず認証してログインできることが定義されています。その上でコンテキスト定義のauthenticationでは、全てのデータベースオペレーションに対して、userという名前のフィールドにユーザー名を記録して、各レコードはuserフィールドのユーザーだけが読み書きできるようになります。さらに、protect-writingキーで、userフィールドの更新を阻止します。この設定を行っても、新規レコードを作るときには、userフィールドにはログインしているユーザーのユーザー名が設定されます。

リスト7-5-1 レコードごとのアクセス権を設定した例
IM_Entry(
    array(
        array(
            'records' => 100000000,
            'name' => 'chat',
            'key' => 'id',
            'repeat-control' => 'delete',
            'authentication' => array(
                'all' => array(
                    'target' => 'field-user',
                    'field' => 'user',
                ),
            ),
            'protect-writing' => array( 'user' ),
        ),
    ),
    array(
        'authentication' => array( // オプション設定
            'user' => array('user1'), // ログイン可能なユーザー
            'group' => array('group2'), // ログイン可能なグループ
        ),
    ),
    array('db-class' => 'PDO'),
    false
);

演習レコード単位のアクセス権を設定する

 同一のテーブル内で、レコードごとに参照できるユーザーを切り替えたい場合があります。その時、ユーザー名やグループ名を入力する文字型フィールドを用意して、そのフィールドをログインしているユーザー名で自動的に絞り込むという動作で実現しています。レコードを新規作成するときに、そのフィールドに自動設定されることなども確認しましょう。

最初のページ用の定義ファイルに必要な設定を行う

1演習環境を起動します(『1-2 演習を行うための準備』を参照)。続いて、ブラウザーで、「http://localhost:9080」に接続します。「トライアル用のページファイルと定義ファイル」というタイトルの部分を特定します。
2「def23.phpを編集する」をクリックし、定義ファイルエディターでdef23.phpファイルを編集します。(もし、23番を他の用途に使ってしまっていれば、別の番号のファイルを利用してください。異なる番号のセットを利用した場合、ソースコードの記述が変わる部分がありますが、以下の手順では可能な限り注記します。)
3Contextsの中のQueryと書かれ背景がグレーの部分を特定します。そして、その次の行の右の方にある「削除」をクリックして、Queryの設定がある行を削除します。
4「レコードを本当に削除していいですか?」とたずねられるので、OKボタンをクリックします。
5同様に、Sortingの次の行にある「削除」ボタンを押し、確認にOKボタンをクリックして、こちらの設定も削除しておきます。
6Contextsでは、nameに「invoice-all」、table、viewに「invoice」、keyに「id」、pagingに「true」と指定します。Contextsにあるその他のテキストフィールドは空白にします。この設定によりinvoiceテーブルを表示します。単にすべてのレコードを一覧表示したいので、簡単な定義とします。
7Contextsの見出しの下にある「追加」ボタンをクリックして新たなコンテキスト定義を追加します。そして、name、table、viewに「invoice」、keyに「id」、repeat-controlに「delete insert」と指定します。Contextsにあるその他のテキストフィールドは空白にします。この設定によりinvoiceテーブルを表示し、レコードの追加や削除ができるようにします。
8定義ファイルエディターのページの冒頭にある「Show All」ボタンをクリックします。そして、nameが「invoice」の方のコンテキスト定義の中にある「Authentication, Authorizaton and Security」の見出しにあるall:targetを「field-user」、all:fieldを「authuser」とします。Tabキーを押すなどして、入力したフィールドから別のフィールドに移動して、確実に保存をしてください。
9Optionsの中にある「Authentication and Authorizaton」を探します。そして、storingに「credential」、realmに「Sample」と入力します。こちらも、Tabキーを押すなどして、入力したフィールドから別のフィールドに移動して、確実に保存をしてください。
10Database Settingsに設定を行います。
[MySQL]の場合
db-classは「PDO」のままでかまいません。dsnに「mysql:host=db;dbname=test_db;charset=utf8mb4」と入力します。そして、userに「web」、passwordに「password」と入力します。
[FileMaker]の場合
db-classを「FileMaker_DataAPI」に書き換えます。databaseは「TestDB」、userに「web」、passwordに「password」、serverに「gateway.docker.internal」、portに「443」、protocolに「https」、cert-vefifyingに「false」と入力します。
11Debugについては、「2」のままにしてこの後の作業を行ってください。最後にデータベースとの通信結果を確認します。デバッグ情報を見ない場合には、ページの冒頭にある「clear」ボタンをクリックするなどして、適時画面から消しても構いません。

最初のページのページファイルの作成

1「http://localhost:9080」で開いたページに戻り「page23.htmlを編集する」をクリックし、ページファイルのpage23.htmlを編集するページファイルエディターを開きます。HTMLでの記述内容を以下のように変更します。太字が追加する箇所を示します。(別の番号のセットで作業している場合には、該当する番号のリンクをクリックしてください。番号にかかる部分は、SCRIPTタグのプログラムの内部にもあります。)
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title></title>
    <script type="text/javascript" src="def23.php"></script>
</head>
<body>
<div style="display: flex">
<div id="IM_NAVIGATOR"></div>
<table>
  <thead>
    <tr><th>id</th><th>作成日</th><th>タイトル</th><th>ユーザー</th></tr>
  </thead>
  <tbody>
    <tr>
      <td data-im="invoice@id"></td>
      <td><input type="text" data-im="invoice@issued"/></td>
      <td><input type="text" data-im="invoice@title"/></td>
      <td data-im="invoice@authuser"></td>
    </tr>
  </tbody>
</table>
<table style="margin-left: 10px">
  <thead>
    <tr><th>id</th><th>作成日</th><th>タイトル</th><th>ユーザー</th></tr>
  </thead>
  <tbody>
    <tr>
      <td data-im="invoice-all@id"></td>
      <td data-im="invoice-all@issued"></td>
      <td data-im="invoice-all@title"></td>
      <td data-im="invoice-all@authuser"></td>
    </tr>
  </tbody>
</table>
</div>
</body>
</html>
2「http://localhost:9080」で開いたページに戻り「page23.htmlを表示する」をクリックします。(別の番号のセットで作業している場合には、該当する番号のリンクをクリックしてください。)
3ログインパネルが表示されるので、ここではユーザー名、パスワードを「user2」としてログインを行います。
4ページが表示されました。authenticationのキーのある左側のinvoiceコンテキストはレコードが表示されていません。右側のinvoice-allのコンテキストは、invoiceテーブルのすべてのレコードを表示しています。ユーザー列は、authuserフィールドの値を示しており、おそらくすべてのレコードが空欄になっていると思われます。ページネーションコントロールでは、現在のログインユーザーを確認できます。

コンテキストにレコードを追加する

1左側のテーブルの下にある「追加」ボタンをクリックします。すると、左側にレコードが追加されます。作成日とタイトルを適当に入力します。作成日は、MySQLでは「2023-7-1」のように年月日、FileMakerでは「07/01/2023」のように、月日年でスラッシュ区切りとなるように入力します。
2念のためにページネーションコントロールの「更新」ボタンをクリックして、ページ内容を更新します。左側には追加されたレコードが見えていますが、右側のテーブルにも見えるようになりました。
3左側で「追加」ボタンをクリックしてもうひとつレコードを追加します。そして、適当にフィールドに入力し、「更新」ボタンをクリックして、右側のテーブルも更新します。
右側のテーブルは、inoviceテーブルをすべて表示しています。これに対して、authenticationキーを設定した左側は、「ログインをしているユーザー名が、authuserフィールドに設定されているレコードのみ見えている」という状況になっています。
4ページネーションコントロールの「ログアウト」ボタンをクリックして、ログアウトします。
5ログインパネルが表示されるので、次は前と違うユーザーでログインをします。例えば、ユーザー名とパスワードに「user3」と入力して「ログイン」ボタンをクリックします。
6user3でログインをしました。まだ、authuserフィールドがuser3のレコードがないので、左側のテーブルにはレコードは表示されていません。
7「追加」ボタンをクリックします。左側の「ユーザー」列にはすでにuser3と見えており、ログインしているユーザーのユーザー名が自動的に設定されていることが分かります。
8フィールドに適当に入力し、ページを更新します。今度は、ログインしているuser3のレコードだけが左側で見えています。

データベースへのリクエスト内容を確認する

1ページを更新し、レコードが表示された状態で、デバッグログは消さずに、左側のテーブルの下にある「追加」ボタンをクリックした状態にして、データベースへ送信されたSQLステートメントあるいはリクエストのパラメーターを確認します。なお、以下の画面は一例です。異なるユーザーでログインしている場合にはその名前に入れ替わるなど状況によって異なるので、手元の結果を確認してください。
2まず、invoiceコンテキストにレコードを追加する部分を探します。ログの中に「新規レコードアクセス: Accessing:/def23.php, Parameters:access=create&name=invoice…」と記載された部分を特定します。
3[MySQLの場合]少し先にINSERTステートメントがあり、初期値としてauthuserフィールドに対してログインしているユーザーのユーザー名が入力されたレコードのみを検索しているSQLコマンドが見えています。([FileMakerの場合]は表示が異なります。)
4続いて、invoiceコンテキストのデータ取得部分を探します。ログの中に「クエリーアクセス: Accessing:/def23.php, Parameters:access=read&name=invoice…」と記載された部分を特定します。
5[MySQLの場合]その少し先に「SELECT * FROM `invoice`…」の部分をみてください。ここでは、authuserフィールドがログインしているユーザ名のuser3のレコードに絞り込む検索条件が自動的に追加されています。([FileMakerの場合]は表示が異なります。)

ユーザー名のフィールドの更新を阻止する

1def23.phpを定義ファイルエディターで編集します。すでにタブあるはウインドウで見えている場合はそれを呼び出します。閉じてしまった場合には、「http://localhost:9080」で開いたページに戻り「def23.phpを編集する」をクリックします。(別の番号のセットで作業している場合には、該当する番号のリンクをクリックしてください。)「Show All」ボタンを押した状態でないのであれば、ページの冒頭にある「Show All」ボタンをクリックしてください。
2nameが「invoice」の方のコンテキストを特定します。「Authentication, Authorizaton and Security」の見出しにあるprotect-writingを「authuser」とします。Tabキーを押すなどして、入力したフィールドから別のフィールドに移動して、確実に保存をしてください。
3page23.htmlをページファイルエディターで編集します。すでにタブあるはウインドウで見えている場合はそれを呼び出します。閉じてしまった場合には、「http://localhost:9080」で開いたページに戻り「page23.htmlを編集する」をクリックします。(別の番号のセットで作業している場合には、該当する番号のリンクをクリックしてください。)
4以下のように太字の箇所を変更します。左側のテーブルの「ユーザー」列のフィールドを、ページ上で変更可能にします。
<body>
<div style="display: flex">
<div id="IM_NAVIGATOR"></div>
<table>
  <thead>
    <tr><th>id</th><th>作成日</th><th>タイトル</th><th>ユーザー</th></tr>
  </thead>
  <tbody>
    <tr>
      <td data-im="invoice@id"></td>
      <td><input type="text" data-im="invoice@issued"/></td>
      <td><input type="text" data-im="invoice@title"/></td>
      <td><input type="text" data-im="invoice@authuser"/></td>
    </tr>
  </tbody>
</table>
:
5page23.htmlがすでにタブあるはウインドウで見えている場合はそれを呼び出し、ブラウザーの更新ボタンをクリックして、ページを再度表示します。閉じてしまった場合には、「http://localhost:9080」で開いたページに戻り「page23.htmlを表示する」をクリックします。(別の番号のセットで作業している場合には、該当する番号のリンクをクリックしてください。)
6左側のテーブルの「ユーザー」の列の適当なテキストフィールドの内容を書き換えてみます。
7Tabキーを押して更新処理をしますが、更新ができない旨のエラーが表示されています。
8ページを更新してください。書き直したフィールドは、データベース上では更新されていません。
テキストフィールドでなければ書き直しはできませんが、それでも、定義ファイルへのアクセスを独自に記述して直接行うことで書き直してしまうことは、技術的には不可能ではありません(もちろん、プログラムを自分で作ることになります)。しかしながら、protect-writingの設定があれば、更新可能なコンテキストでも指定したフィールドは更新ができなくなります。
9「追加」ボタンをクリックして、レコードを追加してみます。レコードの追加はエラーなく行え、ユーザー列には現在ログインしているユーザー名が見えています。

演習のまとめ

データベース外のファイルに対して認証アクセスする

 データベースからデータを取り出すときに、認証を確認し、アクセス権を適用しますが、一方で、画像などのようにデータベースに入れない情報の取り出しについても認証やアクセス権を設定したい場合もあります。ここでは、画像ファイルなどのデータを「メディアデータ」と呼ぶことにします。

 メディアデータを送信するための準備としては、メディアデータのファイルを保存するディレクトリをWebサーバーの公開ディレクトリ外に用意して、ディレクトリやファイルに適切な、すなわちWebサーバーのプロセスが、読み出し可能なアクセス権を設定しておくことです。そして、そのディレクトリのパスを、オプション設定(IM_Entry関数の2つ目の引数)のmedia-root-dirキーの値に指定します。これは、Webサーバーのドキュメントディレクトリ外にします。ドキュメント内部では、場合によっては偶発的にパスが漏洩してしまって、認証せずに画像を見られてしまうかもしれません。

 画像などのメディアデータをWebブラウザーから参照させるには、定義ファイルへのアクセスを利用します。定義ファイル自体はINTER-Mediatorの本体を取り出したり、データベースアクセスを行って結果を送り返すなどさまざまな用途に利用しますが、URLのパラメーターにmedia=で値を指定すると、その値を相対パスとしたファイルの内容を取り出します。基準となるパスは、media-root-dirキーの値です。

 例えば、nameキーの値がcontextというコンテキストがあって、そのコンテキストのimagefileフィールドに、画像ファイル名が入力されているとします。そのコンテキストにおいて、認証が通るか、オプション設定にあるユーザーやグループでの認証が通った時に、画像に対しても認証が通ったクライアントからのリクエストのみ受け付けるには、まず、コンテキストをリスト7-5-2のように記述します。必要最低限の記述を示します。

リスト7-5-2 メディアデータの読み出しに認証を適用する定義ファイルの例
IM_Entry(
    array(
        array( //コンテキスト定義
            'name' => 'context',
            'key' => 'id',
            'authentication' => array(
               'media-handling' => true,
            ),
        ),
    ),
    array( // オプション設定
        'media-root-dir' => '/var/www/files',
        'authentication' => array(
        ),
    ),
    array('db-class' => 'PDO'),
    false
);

 media-handlingがあれば、このコンテキストに対して読み出しのリクエストがあった時、そのレスポンスに、ランダムなコードを返します。そのコードを、定義ファイルのアクセス時にクッキーとしてサーバーに送り、そのコードがサーバーによって発行されたものであるならば、認証されたものとみなします。サーバーとクライアントで認証情報をやり取りする方法はクッキーで固定されています。1回のレスポンスで返るコードは、複数のメディアデータへのアクセスに利用できるので、逆に言えば、セキュリティ面には注意が必要です。この方法でメディアデータに認証を設定するのであれば、TLS 1.2での通信回線上の暗号化が必要になります。

 リスト7-5-2の定義ファイルがdef.phpだとして、contextコンテキストのimagefileフィールドにパスが保存されていた場合のIMGタグの記述例はリスト7-5-3の通りです。例えば、あるレコードで、imagefileフィールドが「products/pencil.jpg」だったとします。すると、「def.php?media=products/pencil.jpg」というURLがsrc属性に設定され、その時に画像を取得するリクエストが定義ファイルに対してクライアントから送られてきます。media=とあるのでmedia-root-dirの値と合成され、サーバー上では「/var/www/files/products/pencil.jpg」という絶対パスのファイルを開き、ファイルの内容とともにMIMEタイプを適切設定されたレスポンスがクライアントに返されます。その結果、サーバー上にある画像ファイルの内容が、クライアントにも見えるということです。このリクエストを送る時、media-handlingキーが設定されているので、サーバーから得られたコードをクッキーに乗せてサーバーに送り込み、サーバー側では自分が発行したものかを確かめて、そうであるならファイルの内容を返します。関係ないコードの場合には何も返しません。

リスト7-5-3 画像ファイルを取り出すIMG要素の例
<img src="def.php?media=" data-im="context@imagefile@#src" />

 この章の最初に、ユーザー名やグループ名をフィールドに記述することで、そのレコードに対するアクセス権を指定したユーザーやグループにだけ許可する方法を『レコード単位のアクセス権』として説明しました。その特定のレコードに記録したパスのファイルを、特定のユーザーにだけ表示可能にすることもできます。なお、グループに対するアクセス権の設定はメディアデータではサポートしていません。

 まず、定義ファイルの例は、リスト7-5-4の通りです。オプション設定側には、メディアデータのルートを指定するmedia-root-dirを指定しています。そしてコンテキスト定義「context」にはauthenticationキーがあり、メディアデータに対する認証を行うための情報をコンテキストの構築時にクライアントに送ります。また、userフィールドに設定されたユーザーに対してレコードのアクセス権をCRUDのすべての操作に割り当てるようにしています。

リスト7-5-4 メディアデータの読み出しにレコードごとのアクセス権を適用する定義ファイルの例
IM_Entry(
    array(
        array(
            'name' => 'context',
            'key' => 'id',
            'authentication' => array(
                'media-handling' => true,
                'all' => array(
                    'target' => 'field-user',
                    'field' => 'user',
                ),
            ),
            'protect-writing' => array( 'user' ),
        ),
    ),
    array( // オプション設定
        'media-root-dir' => '/var/www/files',
        'authentication' => array(
        ),
    ),
    array('db-class' => 'PDO'),
    false
);

 そして、画像ファイルを取り出すIMG要素の例は、リスト7-5-5の通りです。レコード単位のアクセス権を設定するには、ファイル自体を規定されたディレクトリに入れる必要があります。例えば、ここでは、contextコンテキストのあるレコードについて、idフィールドは「120」、imagefileフィールドは「cutter.jpg」だったとします。以下のIMGタグの設定は、このファイルが「/var/www/files/context/id=120/cutter.jpg」である必要があります。つまり、media-root-dirと定義ファイルへのURLのmediaパラメーターの値を繋いだものがファイルのパスになるのは、単に認証を設定する場合と同じですが、そのファイルが、どのレコードに対するメディアデータなのかをパスで記録し、アクセス時には指定が必要です。つまり、「コンテキスト名/キーフィールド=その値」という相対パスを間に入れて、画像ファイル名を指定します。サーバー上でもこのようなディレクトリを用意して記録しておく必要があります。

リスト7-5-5 画像ファイルを取り出すIMG要素の例
<img src="defs.php?media=context/id=$/"
data-im="context@id@$src context@imagefile@#src" />

 ここでは、contextコンテキストは、すべて、フィールドuserにログインしているユーザーと同じユーザー名がある場合のみに、データベースに対する処理が許可されています。メディアデータの読み込みの場合も同様ですが、リクエスト自体からアクセス許可されているかどうかを判定します。前の例だと、id=120のcontextコンテキストのレコードのuserフィールドと、ログインユーザー名が同じかどうかを確認します。ここで、他のユーザーに対するアクセス権があるパスを指定しても、userフィールドの値とログインユーザーは異なるため、メディアデータへのアクセスは拒否され、メディアデータ自体がユーザーのアクセス権を適用されていることになります。

 このような「context/id=120/cutter.jpg」のようなパスを指定するのは厄介ではありますが、計算フィールド等を利用して、パスを生成してもいいでしょう。また、『5-4 JavaScriptコンポーネントの利用』で指定したファイルのアップロードコンポーネントは、ファイルを保存するときにコンテキスト名や主キーフィールドと値を絡めたパスを生成します。つまり、「自分でアップロードしたファイルは自分しか見えない」という動作を想定して機能を組み込んでいます。

このセクションのまとめ

 ユーザー単位あるいはグループ単位にアクセス権を設定するテーブルの運用も可能です。ユーザー名やグループ名を記録するフィールドを用意し、定義ファイルのコンテキスト定義で、必要な設定を行います。メディアデータについても認証をかけることができます。なお、ユーザーによる制限のみがメディアデータに対しては可能です。

7-6メールを利用したユーザー登録とパスワードのリセット

INTER-Mediatorのレポジトリの中には、ユーザー登録をメール確認後に行う機能を作るためのファイルや、パスワードのリセットをメール経由で行うためのファイルが含まれています。これらは、全くそのまま使えるものではないかもしれませんが、主要な機能は組み込まれているので、ページのデザインを合わせれば即座に使えるものです。これらの機能の動作やカスタマイズのポイントを説明しましょう。

メールを利用したユーザー登録

 INTER-Mediatorのレポジトリにある「samples/Auth_Support/User_Enrollment」フォルダーには、ユーザー登録を自動的に行う仕組みのための素材が入っています。管理者が1人ずつauthuserテーブルにレコードを作ればユーザーを増やすことができますが、オンラインサービスなどいつどこからアカウントの申し込みがあるか分からない場合には、リクエストごとにユーザーを追加するのは多大な手間を必要とします。そこで、オンラインから自動的に申し込みたいのですが、そうなると勝手に人のメールアドレスで登録をしてしまうなどのトラブルが懸念されます。そこで、メールアドレスとともに申し込みを行い、そのメールアドレス宛に本当に登録をしていいのかどうかを問い合わせ、メールを受信できた人が手作業で登録許可を行うという仕組みが一般的に利用されます。そのような仕組みは「セルフサービス」と呼ばれることもあります。その仕組みの必要最小限の機能をコードで提供しますので、ご自分のニーズに合わせてデザインを行い、必要に応じて改変をしてください。ただし、改変にはPHPによるWebアプリケーションの開発に関する知識は必要です。『Chapter 8 サーバーサイドでのプログラミング』についても参照してください。INTER-Mediatorに含まれるセルフサービスのプログラムは、Ver.13の途中で改変しました。ここでは、改変後の結果を記載します。

 メールアドレスでの確認プロセスを行うサイトでは、ユーザー名をメールアドレスにすることが一般的と思われます。ここで紹介するプログラムも基本的にはユーザー名はメールアドレスにすることを前提にしています。そのためには、例えばparams.phpファイルに「$emailAsAliasOfUserName = true;」という行を指定します。しかしながら、改造をすればユーザー名もユーザー自身で指定できるようにできます。ただしそれはかなり大幅な改造になります。

 ユーザーの登録を「申し込み」と「確認」の2段階で行います。表7-6-1には、INTER-Mediatorに含まれるファイルのファイル名と主な用途をまとめました。

段階ファイル名用途
申し込みenroll.html申し込みのページのページファイル
enroll.js申し込みのページのJavaScriptのプログラム
enrollcontext.php申し込みのページの定義ファイル
EnrollStart.phpユーザーレコードを追加後に行う処理
確認confirm.php確認の段階で呼び出される。処理は呼び出したときに実行されるが、処理結果はHTMLで表示
表7-6-1 User_Enrollmentフォルダーにあるファイル

 これらのファイルに記述された処理のポイントを説明し、読者の皆さんが改造して自身のアプリケーションに組み込むことができるようになることをここでの解説の目標とします。

名前とメールアドレスを入力するページ

 enroll.htmlは、ポストオンリーモードで稼動するINTER-Mediatorのページファイルで、定義ファイルとしてenrollcontext.phpを利用しています。authuserテーブルのrealnameとemailフィールドに入力するデータを受け付ける2つのテキストフィールドが用意されています。INTERMediatorOnPage変数のオブジェクトに定義するprocessingAfterPostOnlyContextメソッドを定義して、登録が成功した場合にはボタンを非表示にしてメッセージを表示するようにしました。本来、その機能は定義ファイルへの設定で可能ですが、登録が失敗してもボタンを消してしまうような動作になっているため、自前で実装してあります。

 context.phpにはuser-enrollというコンテキストが定義されています。authenticationキーの指定により、新規レコードの処理しかできなくしてあります。また、validationキーにより、名前の入力やメールアドレスの形式判定を行っています。メールアドレスの形式判定の正規表現は、非常に大雑把なものですので、厳密な検査が必要な場合には、正規表現を変更してください。さらにsend-mailキー以下のtemplate-contextキーにより、新規レコードを作った時に、mailtemplateコンテキストからid=991のレコードを取り出してそれをメールのテンプレートとしてメール送信を行います。このテンプレートになるレコードは、サンプルのスキーマに定義がありますが、実際の運用ではconfirm.phpを呼び出す正しいURLを記述するなど、メール内容は書き直す必要があります。

ユーザーレコードの作成前後に行う処理

 user-enrollコンテキストでは、extending-classキーで「EnrollStart」が指定されています。EnrollStartクラスでは、新規レコードを作成する前に呼び出されるdoBeforeCreateToDBメソッドおよび作成後に呼び出されるdoAfterCreateToDBメソッドの2つのメソッドが定義されています。まず、doBeforeCreateToDBメソッドでは、まず、登録しようとしたメールアドレスが、すでに登録されているかどうかをチェックしています。Proxy_ExtSupportトレイトを利用して、シンプルにauthuserテーブルから該当するレコードを検索します。もし、レコードが存在していれば、doBeforeCreateToDBメソッドの返り値をエラーメッセージとして送り出し、新規レコード作成処理を中止します。ここで中止した場合は、processingAfterPostOnlyContextメソッドの引数でキーフィールドの値が得られないことから検知可能です。

 もし、メールアドレスがauthuserテーブルにが存在しないなら、authuserテーブルのハッシュ化したパスワードを保存するhashedpasswordフィールドの値として、無効な値であることが分かるように「dummydummydummy」を設定します。そして、usernameフィールドの値として、現在の日時とメールアドレスをつなげたものを指定しています。ここでは、usernameフィールドはユーザー側には見せないでシステム内部で使うためのものとして位置付けています。そのため、確実に重複のないユーザー名になるように文字列を作っています。もっとも、存在しないメールアドレスを利用しているのでシステム内のユニークIDとしてメールアドレスだけを使ったとしても、理論上は問題ありません。しかし、手作業でユーザーのレコードを作ることと併用したときに確実に区別できるように、意図的に日時とメールアドレスを組み合わせています。そして新規にauthuserにレコードを作成します。

 doAfterCreateToDBメソッドでは、引数から新規に作成されたレコードを取得しています。そして、userEnrollmentStartメソッドで、ランダムな文字列を作成し返り値として得ます。このメソッドはProxyクラスに定義していますが、現在背後でインスタンスが作られており、そのインスタンスへの参照は$this->proxyObjectで得られるようになっています。そのランダムな文字列は、issuedhashテーブルに保存されます。clientidフィールドをNULLにして、ユーザー登録時のランダム文字列であることが分かるようにしています。そして、返り値のレコードの中にhashフィールドを新たに付け加えて、ランダム文字列を値に指定しています。これで、送信するメールのテンプレートの3つ目のフィールドがレコードに加わり、ランダム文字列をメールに含めて送信できるのです。

DB_Proxy->userEnrollmentStart($userID)

引数に指定したユーザーID(idフィールドの値)のユーザーを、userEnrollmentStartメソッドで有効化するためのコードを生成して返す。ユーザー作成時にコードを生成してメールとしてユーザーに送り、そのメールを受け取ったユーザーがコードを利用してアカウントをアクティベートする仕組みを提供する。

レコード作成の確認とパスワードの設定

 ユーザーレコードを作った後のメールに記載されたURLにより、confirm.phpが「c=ランダム文字列」のパラメーターを伴って呼び出されます。confirm.phpは、単独で稼働するファイルです。データベース処理を行うためにDBAccessクラスを定義しており、Proxy_ExtSupportトレイトを取り込んで、データベース処理をシンプルに記述できるようにしています。generatePasswordメソッドはアルファベットと数字を使ってランダムな6文字の初期パスワードを作成します。そして、引数cつまりenroll.htmlの処理でuserEnrollmentStartから得られたコードを引数に、ProxyクラスのuserEnrollmentActivateUserメソッドを呼び出します。このメソッドは、コード、ランダム文字列、そして生のパスワードを記録するフィールド名を指定します。パスワードはそのハッシュ値をユーザーのhashedpasswordフィールドに設定します。その処理に成功すると、authuserテーブルのそのレコードを読み出して、その結果をもとにメールを送信することを行っています。template-contextキーの指定により、mailtemplateコンテキストからid=992のレコードを取り出してそれをメールのテンプレートとしてメール送信を行います。このテンプレートによって出されるメールで、ユーザー名(メールアドレス)に対するパスワードが確定して通知されます。

 ProxyクラスのuserEnrollmentActivateUserメソッドも、すでに背後でインスタンスが作られているものを利用したいのですが、そのために、1度dbReadメソッドでデータベース処理を行っています。つまり、mailtemplateコンテキストに対してダミーの読み出しを行っています。もちろん、パフォーマンスは悪化させる懸念はありますが、ほぼ無視できるレベルではないかと思われます。

DB_Proxy->userEnrollmentActivateUser($challenge, $password, $rawPWField = false))

引数$challengeにはuserEnrollmentStartメソッドの返り値、$passwordには新たに設定するパスワードを指定して、ユーザーをアクティベートする。その結果、ユーザーには引数に指定したパスワードのハッシュが設定され、事前に決められたユーザー名とこのパスワードでログインができるようになる。もし、パスワードそのものをどこかのフィールドに残したい場合は3つ目の引数に、生のパスワードを残すフィールド名を指定する。

メールを利用したパスワードリセット

 authuserテーブルのemailフィールドに、ユーザーごとに一意なメールアドレスが設定されている場合、そのメールアドレスを利用してパスワードのリセットを行うのが、INTER-Mediatorのディストリビューションにある「samples/Auth_Support/PasswordReset」フォルダーにある一連のファイルです。

 パスワードのリセットの処理は2段階で行います。最初の「変更要求」の段階では、フォーム上でメールアドレスを入力すると、そのメールアドレスに、ランダムな文字列を伴ったURLが送られます。その文字列はシステム側で、メールアドレスに対応したユーザーを特定することができます。メールのURLにアクセスすると、2段階目の「変更処理」に移行し、メールアドレスに対応するユーザーのパスワードを入力してリセットできるページが開きます。メールアドレスに届くメールが特定のユーザーにしか参照できない状況が保持されていれば、他人にパスワードのリセットは行えない仕組みです。しかしながら、メール自体は暗号化されていないこともあり、絶対安全とは言えない方法です。Webアプリケーション自体の通信がTLSで暗号化されているのであれば、2段階の処理をなるべく早く行い、リセットを行った後、ログインパネルからパスワードを変更するのが安全な方法であると言えます。ログインパネル上での通信はTLSで暗号化されていて盗聴の危険性はないからです。

 それぞれのファイルについては表7-6-2に概要を示します。

段階ファイル名用途
変更要求requestpwreset.htmlメールアドレスを指定して、変更処理が可能なURLをメールで知らせる
requestpwreset.jsJavaScriptのファイル
resetcontext.php定義ファイル
ResetStart.phpパスワードリセットの開始処理
変更処理resetpassword.htmlパスワードの変更処理を行うフォーム形式のページ
resetpassword.jsJavaScriptのファイル
resetcontext.php定義ファイル(前述の2つのHTMLファイルで共通に使う)
ResetFinish.phpパスワードリセットの完了処理
表7-6-2 Auth_Support/PasswordResetフォルダーにあるファイル

パスワードの変更要求処理

 パスワードの変更要求を行うrequestpwreset.htmlには、メールアドレスを入力するテキストフィールドが2つあります。この部分は一般的なフォームではなく、ボタンを押した後の処理をINTER-Mediatorで行う特殊な形式のページです。「パスワード再設定」ボタンをクリックすると、JavaScriptのプログラムを呼び出し、空欄かどうかやメールアドレスの形式に従っているかどうかなどをチェックした上で(ただし、簡易的なチェック)、resetcontext.phpファイルに定義されたauthuser_requestコンテキストに対して読み出しのリクエストを送っています。このコンテキストではResetStartクラスの呼び出しが行われるように設定されています。そのクラスのdoAfterReadFromDBメソッドがクエリーに先立って呼び出されますが、ProxyクラスのresetPasswordSequenceStartメソッドを呼び出して、パスワードリセット処理を介します。このメソッドでは、指定したメールアドレスを持つauthuserテーブルのレコードを特定し、issuedhashテーブルにランダムな文字列をユーザーレコードのキーフィールドの値とともにレコードを作成して記述します。返り値はfalseならメールが存在しないなどの理由で処理が失敗します。処理が成功すると連想配列が返りますが、キーがranddataなら生成したランダム文字列、usernameならばauthuserテーブルのusernameフィールドを取り出すことができます。

 その後に、コンテキスト定義にあるtemplate-contextキーの値により、mailtemplateコンテキストからid=993のレコードを取り出してそれをメールのテンプレートとしてメール送信を行います。つまり、パスワードの変更要求があったことを、メールで知らせます。このメールのテンプレートでは、次の段階であるresetpassword.htmlを呼び出すURLを正しく記述する必要があります。

DB_Proxy->resetPasswordSequenceStart($email)

引数に指定したメールアドレスemailフィールドに持つレコードのユーザーのパスワードをリセットするために呼び出す。返り値は連想配列で、'randdata'キーはランダムな値、'username'キーはユーザー名を得られる。ランダム値をresetPasswordSequenceReturnBackメソッドで与えてパスワードのリセットの可否を決める。

パスワードのリセット処理

 resetpassword.htmlも、requestpwreset.htmlと同様に、フォームではなく単にテキストフィールドなどが並べられていて、通信処理はINTER-Mediatorで行います。「パスワード再設定」ボタンでJavaScript側のプログラムに移行し、そちらでは入力チェックののちに、resetcontext.phpファイルに定義されたauthuser_finishコンテキストへの読み出し処理が行われます。この時、検索条件として、メールアドレスだけでなく、URLに組み入れたresetPasswordSequenceStartが生成するコードと、さらには変更後のパスワードのハッシュ値も一旦は検索条件として送り出します。

 通信処理によって、ResetFinishクラスのdoBeforeReadFromDBメソッドが呼び出されます。ここではまず、unsetExtraCriteriaメソッドを利用して、コードと、パスワードのハッシュを検索条件から取り除きます。そして、ProxyクラスのresetPasswordSequenceReturnBackメソッドを呼び出します。このメソッドは、メールアドレス、要求時に生成したランダムな文字列、そしてパスワードをハッシュ化した文字列を引数として持ちます。issuedhashテーブルを検索するなどして、ランダムな文字列がそのメールアドレスに対して発行されたものが確認されると、hashedpasswordフィールドの値を設定してパスワードのリセットが完了します。その結果trueが戻されます。

 trueが戻されると、念のために、パスワードが変更されたことを指定したメールアドレス宛にメールを送ります。コンテキスト定義にあるtemplate-contextキーの値により、mailtemplateコンテキストからid=994のレコードを取り出してそれをメールのテンプレートとしてメール送信を行います。

DB_Proxy->resetPasswordSequenceReturnBack($username, $email, $randdata, $newpassword)

引数には順番にユーザー名、メールアドレス、resetPasswordSequenceStartで得られるランダムなコード、そして設定する新しいパスワードを指定する。設定に成功すればtrueが返され、失敗するとfalseが返される。

このセクションのまとめ

 オンラインでメールを利用して承認を進める形式のユーザー登録、そしてメールアドレス宛にパスワード変更可能なURLを送付することでのパスワードリセット、これらの機能を持つ最小限のアプリケーションをINTER-Mediatorに含めています。オンラインサービスを構築するための素材として利用できます。このセクションではその動作の説明と、改良する場合のポイントをまとめてあります。

7-7SAML認証

 INTER-Mediatorでは内蔵の認証システム以外に、SAMLに対応した外部の認証システムを利用した認証にも対応しています。ここではINTER-MediatorでSPを運用して、認証に利用する方法を説明します。なお、演習はありません。

SAMLについて

 SAML(Security Assertion Markup Language)は、異なるドメイン間での認証を実現するXMLベースの規格ですが、現在はアプリケーションサーバーとは異なる認証サーバーで認証してアプリケーションを利用できる仕組みの基礎となる規格として利用されています。SAML Ver.2が2005年に制定され、現在もそのバージョンで利用されています。認証に利用できるサーバにはさまざまな種類がありますが、その認証サーバをシンプルに使うとなると、認証サーバの種類や方式ごとにアプリケーションサーバー側での対応が必要になります。一方、SAMLを利用することで、認証サーバによらない統一的な方法でアプリケーションに認証機能を組み込めるという点で非常に便利です。

 INTER-Mediatorでは内蔵の認証システムを持っており、単一の、あるいは同じデータベース上で稼働するアプリケーション上での認証機能はこれまでに説明したとおり実現できています。しかしながら、Active DirectoryやLDAPサーバに認証情報がすでにあるという組織の場合、それら認証サーバにあるアカウントを利用したいと考えます。INTER-MediatorはVer.6までの間に、LDAP、GoogleのOAuthに対応したクラスを用意してきましたが、認証方式ごとに対応するのは効率的ではないと考え、SAML2での認証に一本化をする前提で進めています。INTER-MediatorはVer.8でSAMLに対応しました。

 SAMLでは、認証の処理を実際に行うIdP(Identity Provider)と、認証要求を行うSP(Service Provider)が存在します。一般には、IdPは独立したドメインで運用し、SPはWebアプリケーションの内部に組み込んであるという状況が多いと思われます。IdP自身が認証処理を行う場合もありますが、別の認証サーバーと連携することもよく利用される形式です。例えば、IdPを立てると同時にIdPとLDAPサーバが通信して、実際のユーザ認証はLDAPサーバ側で行うという仕組みが構築できます。

 一方、SPは、Webアプリケーションと直接やりとりを行う部分です。SPとIdPはお互いに相互にメタデータ交換を行なっています。具体的には、SPのメタデータをIdPに登録すると同時に、IdPのメタデータをSPに登録します。メタデータにはそれぞれのホストのSAMLをやり取りするURLの情報などが入っています。加えて、それぞれのホストの公開鍵が入っており、この公開鍵を利用して、双方の通信を暗号化しています。交換されたということで、双方は信頼できると判断できますが、信頼性の検証は結果的に人間が行うことになります。

 Webアプリケーションで認証が必要になり、SPを利用すると、最初はまずクライアントが、IdPの認証のためのURLにリダイレクトされて、ユーザは認証パネルでユーザ名とパスワードを入れることになります。認証が成功すると、Webアプリケーション側のURLにさらにリダイレクトされます。そして、WebアプリケーションがSPを利用すると、今度はIdPとのやりとりを含めて認証が成立しているので、Webアプリケーションはその結果をもとに認証が通った状態の処理を続けることになります。大まかには以上の流れで認証が進められます。

PHPでSAMLを実現するSimpleSAMLphp

 PHPではSimpleSAMLphpという名前のライブラリがよく利用されており、INTER-MediatorでもこのライブラリをSPとして利用します。SimpleSAMLphpはSPとしての機能だけでなく、IdPにもなります。なお、SimpleSAMLphpは2023年まではVer.1系列が開発されてきましたが、2023年よりVer.2系列に入ったものの、さらにVer.3のアルファ版も見えるなど、どのバージョンを使うのか迷う状況でもあります。Ver.1系列は、Ver.1.19.4まであり、これを利用するという方法もありますが、Ver.2についてはVer.2.0.4までがリリースされています。これらはユーザインターフェースが結構違っており、Ver.2の方が洗練したデザインなのではありますが、状況によってはVer.1を使うことになるかもしれません。なお、SimpleSAMLphpのバージョン1も2も、SAMLのバージョンはVer.2ですので混同しないようにしましょう。

 すでにIdPとなるものが存在していれば、Webアプリケーション側はSPの用意だけで済みます。例えば、Shibboleth 1.3の認証サーバがある場合は、SPをセットアップすればOKです。なお、SimpleSAMLphpのVer.2ではShibbolethの機能は落とされているので、認証サーバがShibbolethの場合はVer.1系列で運用することも検討が必要です。

 一方で、IdPになるものがない場合、独自にIdPを立てる必要があります。もちろん、SimpleSAMLphpを使えば良いのですが、やはりサーバをひとつ起動して色々セットアップも難しいだけに、そこそこの手間はかかります。例えば、LDAPサーバーを利用したい場合、LDAPサーバーとIdPが連携するように、IdP側に設定を行います。SPはそのIdPを利用するというのが基本的な設置方法になります。

 ここからは、どこかにIdPが稼働しているという前提で話を進めます。IdP自体のセットアップも作業として発生する場合もあると思われますが、SimpleSAMLphpを使ったIdPの設置手順は筆者のBlog記事(SimpleSAMLphp Ver.2を使ってみる(1)SimpleSAMLphp Ver.2を使ってみる(2))で紹介しているので参考にしてください。

INTER-MediatorをSAMLのSPにする

 SimpleSAMLphpは、INTER-Mediatorのレポジトリのルートにあるcomposer.jsonファイルに記述があるので、composerを動かしたときにセットアップされます。もし、INTER-Mediatorディレクトリのルートをカレントディレクトリにしてcompose update等でインストールした場合、「INTER-Mediator/vendor/simplesamlphp/simplesamlphp」にSimpleSAMLphpがインストールされることになります。アプリケーションのテンプレートなどをINTER-Mediator自体もcomposerのインストール対象になっている場合は、「アプリケーションのルート/vendor/simplesamlphp/simplesamlphpにSimpleSAMLphp」にインストールされます。

 まず、そのSimpleSAMLphpのルートにあるpublicディレクトリが、Web公開されている必要があります。この部分へWebブラウザからアクセスすることにより、SPの管理ページを開くことができるのです。通常、INTER-Mediatorのディレクトリは公開しているので、publicディレクトリも公開されているのですが、さらに、「https://ホスト名/simplesaml」のURLで、そのpublicディレクトリを参照できるように設定を行います。管理ツールを使う場合など、simplesaml以下に自動的にパスが設定されるので、ドキュメントルートからsimplesamlで、SimpleSAMLphpのpublicに接続できるようにしておく必要があります。例えば、Apache2の場合だとリスト7-7-1のようなAliasディレクティブを記述するなどします。

リスト7-7-1 /simplesamlで公開すべきpublicディレクトをアクセスできるようにする
Alias /simplesaml "/var/www/demo_im_com/saml-trial/lib/src/INTER-Mediator/vendor/simplesamlphp/simplesamlphp/public"
# INTER-Mediatorは、/var/www/demo_im_com/saml-trial/lib/src/INTER-Mediator
# INTER-Mediator/vendor内にSimpleSAMLphpが存在する

SPの設定ファイルを用意する

 composerでインストールしたSimpleSAMLphpをSPとして利用するには、これらのディレクトリ内に設定ファイルを記述しなければなりません。設定ファイルをディレクトリ内部に直接記述してもいいのですが、composerの操作によってはそれらは消されてしまうので、どこか別のディレクトリに設定ファイルを作っておき、その設定ファイルを該当する場所にコピーして運用することにします。設定をサポートするスクリプトが「INTER-Mediatorのルート/samples/saml-config」にあります。このsaml-configフォルダをどこかにコピーします。例えば、アプリケーションのルート以下、libディレクトリあたりにコピーをしておき、必要ならアプリケーション自体のレポジトリに設定ファイルを記録しておくと良いでしょう。以下、「コピーしたsaml-configフォルダ」として、コピー先のフォルダを参照します。

 コピーしたsaml-configフォルダには、gettemplates.shというスクリプトがあります。このスクリプトは、libあるいはlib/src、さらにはアプリケーションのルートにあるvendorディレクト以下のINTER-Mediatorを探して、そこから設定ファイルのテンプレートをコピーします。いずれにしても、このgettemplates.shをシェルスクリプトとして稼働すると、ディレクトリを探して、config.php、authsources.php、acl.php、saml20-idp-remote.php、saml20-idp-hosted.php、saml20-sp-remote.phpのコピーを作ります。ファイルが作られない場合には想定したフォルダにINTER-MediatorやSimpleSAMLphpが存在しないことになり、その場合はスクリプトを修正して、設定ファイルのテンプレートが存在するディレクトリに変更してください。これらのファイルのうち、通常はconfig.php、authsources.php、saml20-idp-remote.phpのファイルを変更します。

証明書の用意

 SAMLではSP、IdPともに証明書を作ります。証明書というよりも、秘密鍵と公開鍵を作り、メタデータ交換により公開鍵を相手(SPならIdP)に渡しておくことで、自分自身への暗号化通信を実現しています。ただ、秘密鍵と公開鍵よりも、証明書として鍵を作った方が何かと便利なので、証明書を用意します。これは、WebサイトのTLSのために用意したものでも構いませんが、SimpleSAMLphpのインストールの説明では、自己署名証明書で期限が10年と言った証明書を作っており、確かに一定の条件を満たせば、それでも問題はないとも言えるでしょう。以下のコマンドは、saml-configディレクトリを利用するという前提なので、saml-configをカレントディレクトリにしてコマンド入力します。そして、生成する秘密鍵のファイルと証明書のそれぞれは、ファイル名をsp.pem、sp.crtというきめうちのファイル名にします。

リスト7-7-2 自己署名証明書を生成するコマンドの例
openssl req -newkey rsa:3072 -new -x509 -days 3652 -nodes -out sp.crt -keyout sp.pem

 このコマンド入力後、証明書の内容についての問い合わせが行われます。これらは適当に入力すればいいでしょう。ただし、Common NameではSPを稼働するホスト名を正しく入れておくのが良いと思われます。

設定ファイルの修正

 続いて、コピーしてきた設定ファイルを修正します。まず、config.phpについては、以下のキーの値を修正します。これらのキーはファイル内で連続した場所にないので、ひとつずつ探して修正をしていきましょう。なお、baseurlpathは、publicディレクトリがある場所への絶対パスを記述します。technicalcontact_emailは自分のメールアドレスを指定します。secretsaltについては乱数から生成しますが、ファイルのコメントで生成のためのコマンドが紹介されているので、そのコマンドで生成します。auth.adminpasswordはSPの管理ページで利用できるパスワードを指定します。

リスト7-7-3 config.phpファイルの修正ポイント
'baseurlpath' => 'saml-trial/lib/src/INTER-Mediator/vendor/simplesamlphp/simplesamlphp/public/',
'technicalcontact_email' => 'your_email',
'secretsalt' => 'your_salt',
'auth.adminpassword' => 'your_admin_pass',

 続いてauthsources.phpを修正します。default-spキーの配列の要素に、certificateとprivatekeyのエントリーを用意して、ここで作成したキーファイルと証明書ファイルを指定します。そして、entityIDをサイトのドメインに設定しておきます。

リスト7-7-4 authsources.phpファイルの修正ポイント
'default-sp' => [
  'saml:SP',
  'certificate' => 'sp.crt',
  'privatekey' => 'sp.pem',

   // The entity ID of this SP.
   'entityID' => 'https://demo.inter-mediator.com/',
   :

 さらに、saml20-idp-remote.phpを修正します。このファイルの最後(とはいえ、中身は短いコメントがあるのみ)に、IdPの管理ページからコピーしたIdPのメタデータを示す配列をコピーしておきます。

図7-7-1 saml20-idp-remote.phpにIdPのメタデータを追加する

書き直した設定ファイルなどを展開する

 証明書を用意し、設定ファイルを書き直すと、saml-configディレクトリに入っているcopyconfig.shというスクリプトファイルを実行します。これも、最初に稼働させたgettemplates.shと同様に、SimpleSAMLphpをいくつかの典型的な場所から探してその内部のいくつかのフォルダにファイルをコピーします。スクリプトを実行して、エラーが出なければ、おそらくはファイルの元をコピーしたSimpleSAMLphpのフォルダへ、設定ファイルを書き出していると思われます。もし、うまくいかない場合はスクリプトを修正するなどしてください。なお、このスクリプトで実行しているのは、sp.pem、sp.crtをSimleSAMLphpのルート以下certディレクトリへ、config.php、authsources.phpをconfigディレクトリへ、saml20-idp-remote.phpをmetadataディレクトリへコピーしています。

 なお、設定変更するときには、saml-configディレクトリにあるファイルを変更して、その都度、copyconfig.shスクリプトを実行すれば良いでしょう。

SPのメタデータを取り出す

 以上で、SPとして稼働するはずです。ここで、SPの管理ページにログインをして、SPのメタデータを取り出します。そのためには、ブラウザより「https://ホスト名/simplesaml/admin」に接続します。図7-7-2のようなログインパネルが表示されるので、ユーザ名はadmin、パスワードはconfig.phpのauth.adminpasswordキーに指定したパスワードを指定して認証します。ログインできるはずです。

図7-7-2 SPの管理ページにログインする

 管理ページにはさまざまな機能がありますが、上部で「連携」をクリックして、Hosted entitiesを表示し、そこにあるdefault-spの「V」の部分をクリックして、メタデータを表示します。このメタデータを、IdPの管理者に渡してこのデータをIdP側に登録すると、SAML認証ができるようになります。なお、通常はメタデータはXMLですが、IdPがSimpleSAMLphpなら、PHPのコードの方が作業が一手間少なくて済みます。

図7-7-3 SPのメタデータを取得する

SAML認証の設定と動作

 SAML認証に関わる設定は、params.phpで行います。リスト7-7-5に使用できる変数を示しました。また、表7-7-1にはIM_Entryのオプション引数あるいはparams.phpファイルに指定できる項目についてまとめておきました。

リスト7-7-5 params.phpファイルでのSAML関連の設定
$isSAML = true; # The default value of isSAML is false.
$samlAuthSource = 'default-sp';
$samlExpiringSeconds = 1800;
$samlWithBuiltInAuth = true;
$samlAttrRules = ['username' => 'uid|0', 'realname' => 'eduPersonAffiliation|0'];
$samlAdditionalRules = ['username' => '(user02|user03)'];
オプションの配列指定params.phpで変数名既定値設定の説明
第1次元第2次元
'authentication'is-saml$isSAMLfalseSAML認証を有効にする。なお、SAMLのSPになるための設定については、INTER-Mediatorのレポジトリにあるsample/saml-configディレクトリの内容と説明を参照してください。
(未定義)$samlAuthSourceなしSPの識別名
(未定義)$samlExpiringSeconds3600SAML認証をキャッシュした場合のキャッシュの有効秒数
saml-builtin-auth$samlWithBuiltInAuthfalsetrueにすると、SAML認証だけでなく、ビルトイン認証も並行して利用できる。そのため、INTER-Mediatorのログインパネルが表示され、そこに「SAML認証」ボタン(ボタン名のメッセージの番号は2026)が表示される
(未定義)$samlAttrRulesなしSAML認証時に得られる情報から、ユーザ名などを抜き出すルールを記述する
(未定義)$samlAdditionarlRulesなしSAMLで得られたユーザ情報が特定の値になっていない場合に認証を不成立とすることができ、そのルールをここに記述する
表7-7-1 SAML認証についての設定

 まず、SAML認証の機能を有効にするかどうかは、$isSAML変数で指定をします。通常はfalseになっているので、ここで明示的にtrueを代入しなければなりません。あるいは、IM_Entry関数の第2引数(オプション設定)で、authenticationキーの配列でis-samlキーでtrue値を指定しても設定可能です。この設定と同時に、$samlAutuSource変数で、SPのソース名を設定します。これまでの手順通りなら、設定名は「default-sp」になっているはずです。

 $isSAMLをtrueにすると、認証が必要なページでは、IdPのログインページにリダイレクトし、認証後はアプリケーションのページに戻るという動作になります。このとき、INTER-Mediatorは、内部的には組み込みのユーザを作り、通常の認証はそちらで行います。これは、認証サーバに都度都度認証処理をさせるとスピードが遅くなることを懸念してのことで、IdPでの認証が成功すると、特別な組み込みユーザを作り、乱数でパスワードを自動生成して認証の継続を行うようにします。ただし、直前の処理から$samlExpiringSecondsで指定した秒数経過していると、改めてIdPに認証を求めます。通常はIdP/SPの連携によって認証状態が継続されますが、例えば何日も経過していると、IdPの認証パネルが出てくるようになっているのが一般的な動作でしょう。

 なお、SAML認証では、組み込みの認証の機能も使うので、$passwordHash = '2'; の定義も行うようにしてください。

 $samlWithBuiltInAuthをtrueにすると、組み込みのユーザでのログインが可能になります。これを未定義のままにすると、この変数値はfalseになり、SAMLつまりはIdPが管理するユーザで以外はログインできません。一方trueにすると、IdPのユーザと、authuserテーブルで定義したユーザの両方でのログインが可能になります。このとき、アプリケーションでは、INTER-Mediatorのログインパネルを表示しますが、ログインパネルに「SAML認証」ボタンが追加されています。authuserに設定してあるユーザは通常通りログインパネルでユーザーとパスワードを入力してログインしますが、IdPのユーザでログインするには「SAML認証」ボタンをクリックして、IdPのログインページに移動して、ログインをする必要があります。

 $samlAttrRules変数は、認証時に得られる情報からauthuserテーブルにユーザを作るときにどのデータを取り出してどのフィールドに入力するかを示しています。変数に値を設定しなければ、uidの最初のデータをauthuserテーブルのusernameフィールドに設定するだけになります。この変数は連想配列で指定しますが、キーはauthuserテーブルのフィールド名、値はSAML認証で得られる認証ユーザーの情報からどのキーの何番目の配列の値を取り出すかを指定します。'realname' => 'eduPersonAffiliation|0'であれば、認証結果から得れたeduPersonAffiliationというキーの配列の最初の要素を、realnameフィールドにセットするということです。|の前にエントリー名、|に続いて配列の何番目の要素かを指定します。SAMLで得られるデータが複雑な場合は、この変数の要素として、'email' => 'urn:oid:0.9.2342.19200300.100.1.3|0' と言ったような記述を行うことにもなります。値の|までの部分はurn表記でのフィールド名となります。この設定は、次に説明する$samlAdditionalRulesに関連が深いとも言えます。

 変数の$samlAdditionalRulesは連想配列を指定し、authuserテーブルのフィールド名とそれに対する値を指定します。これは、認証したユーザーの情報に対して、特定のフィールドが想定された値であるかどうかをチェックすることを意味し、連想配列の定義、つまりあるフィールドの値が決められたものでなければ、認証を失敗したものとみなすという処理になります。言い換えれば、ユーザ属性に応じて認証を失敗させる設定が可能です。この配列の値は、正規表現での指定が可能です。'username' => '(user02|user03)'であれば、username、つまりSAML認証で得られたuid[0]の値がuser02かuser03のユーザでないと認証は成立しないようになります。

 $samlAttrRulesと$samlAdditionalRulesの定義に関しては、一般的な説明が難しいですので、詳細な設定が必要な場合には、INTER-Mediatorのソースをチェックされることをお勧めします。設定に必要な情報はデバッグ情報として見えるようにはなっていますが、ソースと対照しないと分かりづらいと思われます。

SAMLでのログアウト

 SAML認証を利用したアプリケーションでも、ログアウトは、JavaScriptでINTERMediatorOnPage.logout()を呼び出すだけで構いません。SAML側から提示されるログインとログアウトのURLは、JavaScriptのオブジェクトに記録されています。ログインのURLはINTERMediatorOnPage.loginURL、ログアウトのURLはINTERMediatorOnPage.logoutURLで得られますので、個別に処理をしたい場合はこれらのプロパティを利用できます。しかしながら、JavaScriptでINTERMediatorOnPage.logout()では、背後でINTERMediatorOnPage.logoutURLを呼び出したり、変数などを消去してログイン状態を解除するなど必要な処理を組み込んであるので、SAMLのログインやログアウトのURLを利用する必要は通常はないと思われます。

 $samlWithBuiltInAuthをtrueにしたときにログインパネルに表示される「SAML認証」ボタンは、INTERMediatorOnPage.loginURLへ移動するという動作になります。

このセクションのまとめ

 INTER-MediatorはSAMLに対応しています。SAMLのSPとして、Webアプリケーションに組み込んだSimpleSAMLphpを稼働させるための設定について、このセクションでは説明をしました。外部の認証サーバを利用したアプリケーション構築を行う場合には、SAMLの利用をまず検討しましょう。

7-8OAuth認証

 INTER-Mediatorで利用できるOAuth認証の利用方法について説明します。Ver.5.3でOAuthを実装したものの、その後に不安定な時期やサポート対象外にした時期もありました。Ver.14で復活しました。なお、SimpleSAMLphpのプラグインでのOAuthの仕組みもありますが、SAMLとは別に実装したものです。

OAuth2を認証に利用する

 INTER-Mediatorは、OAuth2での認証ができます。Ver.14の段階では、GoogleとFacebookのアカウントによるOAuth2認証(もしくはOpenID認証)に対応しています。INTER-Mediatorでは、独自にテーブル等を用意しての認証についてこれまでに説明してきていますが、OAuthの場合も、独自の認証機能と連動します。OAuthによってプロバイダでの認証が行われ、それに成功すると、INTER-Mediator独自の認証システムにランダムなパスワードでユーザを登録します。同時にクライアントでは「認証が成功した」と同様な状態にするので、タイムアウト等で認証が切れるまでは、INTER-Mediatorの認証システムでの認証が機能することになります。認証が切れれば、あらためてプロバイダに認証を求めるようになっています。

 INTER-MediatorでOAuthによる認証を利用するには、必要な設定を行うだけで良く、プログラミング等の実装は不要です。しかしながら、OAuthの仕組みにある「リダイレクトURI」についての理解は不可欠です。OAuthのサーバあるいはシステムは、どのようにして認証したかどうかをWebアプリケーションに伝えるのかということに大きく関わる仕組みです。例えば、Googleの認証システムを利用してOAuth認証をする場合、まず、Googleの認証のページに遷移します。この部分も背後では色々あるのですが、認証プロバイダが求めるパラメータを持って、指定されたURLをGETでアクセスすることで、あとはプロバイダが用意した認証画面が出てきます。そして、認証はもちおろん、アプリケーション側が取得する情報等の内容が示されて確定した場合、認証システムはリダイレクトURIへクライアントをリダイレクトします。その時に、認証結果を取得するためのコードなどがURLのパラメータとして与えられます。そして、このリダイレクトURIの先のアプリケーション(あるいはファイル)を、アプリケーション開発者が用意しておく必要があります。INTER-Mediatorでは典型的なリダイレクト先のプログラムを、/samples/Auth_Support/OAuthCatcher.phpというファイルで提供しているので、それをそのまま使うか、改変して利用します。

 OAuthCatcher.phpでは、リダイレクト時に得られたコードから、認証情報を取り出し、INTER-Mediatorの認証システムにOAuthシステムと対応するユーザを作成し、ランダムなパスワードを設定し、クライアントを認証された状態にします。すでにユーザがあれば、パスワードを更新します。そして、元のページに移動します。

 よくあるOAuth認証のサイトでは認証後に普通に利用できるようになっていますが、アプリケーションによってはそれは困るという場合もあるかもしれません。例えば、特定のユーザだけが使えたり、あるいは高い権限を与えたりという必要性もある場合もあるでしょう。その場合は、OAuth認証によりINTER-Mediatorの認証システム側に作られたユーザを確認して、状況に応じてグループに所属させるという処理が必要になります。手作業でやるなら、未処理の一覧等、色々なユーザインターフェースを作ることになるかもしれません。そうしたアプリケーション特有の処理は、OAuth側でできることには限界があるので、リダイレクトURLを受け取った先の処理としてきちんと設計をしてから実装しなければなりません。

Googleのアカウントから必要な情報を得る

 OAuth2での認証を行うには、認証プロバイダーによって発行されるIDやパスフレーズなどを入手しなければなりません。以下、Googleでの準備の流れを記述します。Googleの開発者向けサイトでは変更される可能性があるので、ここで記述した通りではないかもしれません。ここで紹介するGoogleサイトの画面は、2024年12月に撮影したものです。また、Googleには多種多様なアカウントがありますが、以下の流れは個人で取得したGmailのアカウントを利用しています。少なくとも、Google Apps for NPOで作成したアカウントでは異なる応答をすることが分かっており、そのバリエーションはINTER-Mediatorに実装済みです。

1Google Cloud(https://console.cloud.google.com/home/dashboard)に接続します。認証を求められれば、自分自身のGoogleのアカウントで認証してください。
2最初にプロジェクトを作成します。ひとつのWebアプリケーションでひとつのプロジェクトを作成するのが適切と考えられます。ページの上部のGoogle Cloudの右のボタン状のボックスがプロジェクトの選択や作成をおこなうパネルを呼び出します。画面ショットでは「Wits24-Android-Project2」と見えていますが、この文字列はログインした人によって変わります。ここをクリックするとパネルが表示されるので、「新しいプロジェクト」をクリックします。
3新しいプロジェクトを作成するパネルが表示されました。ここでは適当にプロジェクト名を指定して、「作成」ボタンをクリックします。
4プロジェクト(ここでは「IM-Trial」)は作られていますが、まだ選択されていません。上部のツールバー右の方にあるベルのアイコンをクリックすると、今作ったプロジェクトの選択ボタンが出てくるので、それをクリックして選択します。なお、すでに選択されていればこの操作は不要です。
5作成したプロジェクトが選択されました。「プロジェクトの情報」で名称を確認できます。ここで中央あたりにある「API」のボックスの下端にある「APIの概要に移動」をクリックします。
6「APIとサービス」のページに移動します。(筆者は色々なサービスを使っていてこのようにさまざまなライブラリがリストアップされていますが、認証だけであれば、特にAPIの選択は現在は不要のようです。)
7左側にある「認証情報」をクリックします。そして、「認証情報を作成」ボタンをクリックして認証情報を追加します。
8ポップアップメニューでは「OAuthクライアントID」をクリックします。
9続いて、アプリケーションの種類がたずねられるので、「ウェブアプリケーション」を選択します。
10「名前」は適当な識別名を記述します。そして、「承認済みのリダイレクトURI」の指定を行います。このURLのファイルは、Webアプリケーション側に用意しておく必要があり、Webアプリケーションの利用者のブラウザーからアクセス可能なものを指定します。「URIを作成」ボタンをクリックします。
11URIの枠が増えるので、URIを入力します。この後、演習環境を利用してOAuth認証のトライアルをやってみるのであれば、「http://localhost:9080/samples/Auth_Support/OAuthCatcher.php」と手入力します。そして、「保存」ボタンをクリックします。
12作成したことを示すパネルが表示されます。ここで、パネルの下部にある「JSONをダウンロード」という箇所をクリックします。ここで得られたJSONファイルに、クライアントIDとパスコードが記載されています。なお、後からでも同一のJSONは得られます。
13プロジェクトに、OAuth2クライアントの設定が付加されました。さらに、OAuth同意画面など、アプリケーションによっては更なる設定が必要かもしれませんが、認証の通信に含めるべきクライアントIDとパスコードについてはここまでの手順で作成できて、JSONファイルで手元に得られています。なお、JSONファイルは、ここでの「IM-Samples」と書かれた行の一番右にある⬇︎部分をクリックしても得られます。

Googleのアカウントで認証するための設定

 以上の手順で認証情報をGoogle側に作成します。後からの変更や参照は、「APIとサービス」に移動して、左側で「認証情報」を選択することで可能です。その時、プロジェクトをページ上部のツールバーで選択をしておく必要があることを念頭に入れておきましょう。

 認証情報の設定で得られたJSONファイルの内容を整形したものをリスト7-8-1に示します(コードなので隠匿のため一部省略します)。

リスト7-8-1 生成した認証情報(一部のデータは省略)
{
  "web": {
    "client_id":"279621829.....",
    "project_id":"im-trial","auth_uri":"https://accounts.google.com/o/oauth2/auth",
    "token_uri":"https://oauth2.googleapis.com/token",
    "auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs",
    "client_secret":"GOCSPX-....",
    "redirect_uris":["http://localhost:9080/samples/Auth_Support/OAuthCatcher.php"],
    "javascript_origins":["http://localhost:9080"]
  }
}

 params.phpファイルにはリスト7-8-3のように$oAuth変数を指定をします。キーはプロバイダを示す「Google」を指定し、その配列としてClientIDキーはJSONファイルのclient_idキーの値、ClientSecretキーはJSONファイルのclient_secretキーの値、RedirectURLキーはJSONファイルのredirect_urisキーの値のひとつを指定します。JSONファイルのその他の値は指定しなくて構いません。また、AuthButtonキーには、ログインパネルでの認証ボタンの名称を指定します。

リスト7-8-2 params.phpファイルに指定するOAuth2関連の設定
$oAuth = [
  'Google' => [
    'AuthButton' => 'Google Auth',
    'ClientID' => 'xxxxxxxx....',
    'ClientSecret' => 'xxxxxx....',
    'RedirectURL' => 'http://localhost:9080/samples/Auth_Support/OAuthCatcher.php',
  ],
];

 params.phpファイルに設定するOAuth2関連の設定に関して、表7-8-1にキーと設定値をまとめておきます。「$oAuth/[Provider]/AuthButton」とは、変数$oAuthに対して配列を代入するが、その配列のキーとして [Provider] を記載し、さらにその値が連想配列となってAuthButtonというキーで値を指定するという意味です。説明すると分かりづらいですが、リスト7-8-2と対照してください。

params.phpで変数名設定する値
$oAuthOAuth認証の設定を行う指定する文字列で、プロバイダ名をキーとした連想配列を指定する。値も連想配列(以下に記述)。プロバイダ名は "Google" "Facebook"に対応。
$oAuth/[Provider]/AuthButtonログインパネルに配置されるOAuth認証に移行するためのボタンの名前 [Google, Facebook]
$oAuth/[Provider]/ClientIDプロバイダから供給されるクライアントID [Google, Facebook]
$oAuth/[Provider]/ClientSecretプロバイダから供給されるシークレット [Google, Facebook]
$oAuth/[Provider]/RedirectURL認証後に戻ってくるリダイレクトURL [Google, Facebook]
表7-8-1 OAuth2認証に関する設定

Facebookのアカウントから必要な情報を得る

 Facebookのアカウントでの認証を行うには、Facebookからアプリケーションに組み込む情報を得る必要があります。例えば、以下のような手順で進めます。Facebookでの「アプリ」を定義することでコードが発行されます。

1FacebookのDevelopersページを開きます。URLは、https://developers.facebook.com/です。ページトップのナビゲーションにある「マイアプリ」をクリックします。
2アプリのページに移動しました。「アプリを作成」ボタンをクリックします。
3説明が出てくるので、「アプリを作成」ボタンをクリックします。
4アプリ名は、アプリケーションを識別する名前を適当に設定します。連絡先は自分のメールアドレスを指定してください。
5「ユースケースを追加」という表示に切り替わります。ここでは、「Facebookログインでの承認およびユーザデータのリクエスト」の枠の右端にあるチェックを入れておきます。ここの追加だけで、認証機能は利用できるようです。「次へ」をクリックします。
6ビジネスポートフォリオことが記載されていますが、「現時点ではビジネスポートフォリオをリンクしない」を選択しておいて、「次へ」をクリックします。
7「完了」の表示が出てきました。これでアプリがひとつ作成されています。「ダッシュボードに移動」をクリックします。
8パスワードの入力が必要になりますので、Facebookアカウントのパスワードを入力します。
9アプリダッシュボードについての説明が出ますので、「ダッシュボードにアクセス」ボタンをクリックします。
10作成したアプリの詳細情報が見えています。最初の方にある「アプリID」と「app secret」が、作成するWebアプリケーション側に設定すべきコードです。app secretは、さらに右の「表示」ボタンをクリックして表示してコピーします。また、「アプリドメイン」には、ここで稼働するアプリケーションのドメインを入力します。ここでは、演習環境でアプリケーションを稼働するのでlocalhostとしました。
11左側で「ユースケース」をクリックします。前に選択した「Facebookログインでの承認およびユーザデータのリクエスト」の枠が出るので、その右にある「カスタマイズ」をクリックします。すると、「アクセス許可」についての設定が出ます。電子メールアドレスの情報を取得したい場合は、ここで、emailの項目の右にある「アクション」ボタンをクリックして、画面に従って作業します。
12左側で、「設定」をクリックします。そして、「有効なOAuthリダイレクトURI」にリダイレクトする先のアドレスを設定します。また、「JavaScript SDKに許可されたドメイン」ではアプリケーションのドメインを記述しておきます。なお、検証ツールもあるので、URIを確認してから設定することもできます。

 画面上の情報では不完全な設定に見えますが、ここまでの設定をすればとりあえず、テストはできるようです。実運用に至る場合は、Facebookのアプリについての他の設定も必要になると思われますので、ドキュメントを調べて、必要な設定を追加してください。

 Facebookのアカウントによる認証を有効にするためには、params.phpファイルの$oAuth変数に、例えば以下のような設定を行います。Googleと基本的には同じですが、キーがFacebookになっています。リダイレクトURLについても同一のものを指定しています。

リスト7-8-3 params.phpファイルに指定するOAuth2関連の設定
$oAuth = [
  'Facebook' => [
    'AuthButton' => 'Facebook Auth',
    'ClientID' => 'xxxxxxxx....',
    'ClientSecret' => 'xxxxxx....',
    'RedirectURL' => 'http://localhost:9080/samples/Auth_Support/OAuthCatcher.php',
  ],
];

演習OAuth2のアカウントで認証する

 演習環境を使って、OAuth2認証を組み込んでみましょう。ここでは、前に説明した方法で、Googleに関しての設定のためのコードが得られているとします。そして、演習環境にあるINTER-Mediatorのサンプルコードを利用して、OAuth認証を行なってみます。もちろん、localhost経由で稼働しており共有可能な状態ではありませんが、OAuth認証の設定と動作はわかると思われます。

最初のページ用の定義ファイルに必要な設定を行う

1演習環境を起動します(『1-2 演習を行うための準備』を参照)。php-apache_imというコンテナが稼働しているので、そのコンテナを選択して、Execのタブを選択しておきます。
2コマンドとして、「cd /var/www/htm」を入力し、さらに「composer update」を入力して、INTER-Mediator等を更新しておきます。
3Filesのタブを選択し、/var/www/html/lib/params.phpというファイルを特定します。そして、その項目をダブルクリックして、ファイルの内容を編集します。
4スクロールして、$oAuth変数の定義部分を探します。通常は、コメントになっています。
5記述を修正して、Googleに関する設定を有効にします。そして、ClientIDとClientSecretを、自分で収集したものを記述します。AuthButtonとRecirectURLは既定値のままで大丈夫なはずです。
6Save Changesボタン(フロッピーディスクのアイコンのボタン?)をクリックして、ファイルを保存します。
7
8続いて、ブラウザーで、「http://localhost:9080」に接続します。「サンプルプログラム」というタイトルの部分を特定します。
9サンプルプログラムのページで一番下までスクロールして、「For E2E Test」の下にある「Links to e2e test pages」のリンクをクリックします。
10E2E Test用のページのリンクがあります。この中で「Authenticating Page with MySQL (Credential-email account)」をクリックしてページを開きます。
11INTER-Mediatorのログインパネルが表示されます。ここに「Google Auth」というボタンが追加されています。このボタンをクリックします。
ログインパネルにあるパスワード変更では、Googleなどの認証サービスプロバイダー側のパスワードの更新はできません。通常のログインのためのユーザーやパスワードを入れる枠も、不要といえば不要です。ただし、Google等の外部のプロバイダーによるアカウント以外に、authuserテーブルに追加したユーザーでも認証して利用した場合には、このままでも可能です。つまり、OAuth2対応にしても通常のログイン認証もできます。
12Googleのページに移動し、認証作業が始まります。適当に自分のアカウントで認証を行います。
13OAuth2では、このように、アプリケーション側に伝達される情報がどのようなものかを提示して、その情報の伝達を許可するかどうかをたずねるようになっています。ここで確認して、「次へ」ボタンをクリックします。
14これで、テスト用のシンプルなページが開きます。「追加」ボタンをクリックすると、レコードが追加されますが、左側の列の2行あるデータのうち、下の方が、ログインしているユーザになります。ログインユーザ名は、Googleが伝達するユーザを特定するコード(応答に含まれるsubキーの値で、ここでは「11316...」)と、プロバイダ名のGoogleを@で繋いだものとしています。

 初めて認証が成功したときには、認証プロバイダから得られたコードと@にプロバイダ名をつなげて作ったユーザ名のユーザーを、authuserテーブルに追加します。その時に、ランダムな文字列のパスワードを生成して、それをクライアントに伝達します。クライアントはユーザー名とパスワードのハッシュを保持することで、以後、認証状態を継続します。つまり、OAuth2による認証後は、INTER-Mediatorの通常の認証処理を行います。その後にOAuthCatcher.phpによってGoogleと通信して、認証の確認が行われ、その後に、「OAuth認証」ボタンを押したページにリダイレクトします。通常は、認証が成り立った後となるので、ページは自動的に開きます。

 authuserテーブルを参照してみてください。演習環境を利用しているのなら、サンプルの一覧ページでは左側の列に「Account Manager」と記述された部分のリンクより参照できます。図7-8-1ではリストの最後に見えるユーザーが、Googleアカウントで認証したことで作られたアカウントです。ここには表示していませんが、realnameフィールドには筆者の名前が入っているはずです。また、どのグループにも所属していません。パスワードのハッシュ値は自動生成され、認証の確認があるごとにパスワードは更新します。

図7-8-1 authuserに追加されたレコードを確認する

リダイレクトURLの先のプログラムについて

 OAuth2のプロバイダで認証をした後に、リダイレクトURLで指定したURLに移動します。そのリダイレクト先は、つまりは自分自身のアプリケーション側になります。そこで、認証が成功したか失敗したかを判定して、先の処理を行いますが、その仕組みについてはある程度INTER-Mediatorで実装されています。前にも出てきたレポジトリのsamples/Auth_Support/OAuthCatcher.phpにあるもので、さらに、INTER-Mediator内部のOAuthAuthクラスも利用しています。OAuthAuthクラスは固定的なもので、アプリケーションごとの事情に応じて処理を組み込む場合は、OAuthCatcher.phpを利用します。つまり、OAuthCatcher.phpファイルはどこか別のところに移動し、内容を書き換えて利用することを想定したものです。

 OAuthCatcher.phpはリスト7-8-4にコア部分を示します。なるべくカスタマイズがしやすいようにと考えてあります。まず、最初の$pathToIM変数は、このファイルからINTER-Mediatorのルートへの相対パスを記述します。このファイル自体は、INTER-Mediatorの外部になりますが、INTER-Mediator内部のクラスを利用するためにパスを通す方法で対処しています。もちろん、composerベースの手法でクラスを参照するのであれば、その方法でも構いません。

リスト7-8-4 OAuthCatcher.phpのコア部分
<?php
:
// The variable pathToIM has to point the INTER-Mediator directory.
$pathToIM = "../../";   // Modify this to match your directories.
:
$authObj = new INTERMediator\OAuthAuth($_COOKIE["_im_oauth_provider"] ?? "");
$authObj->debugMode = false; // or comment here
$authObj->setDoRedirect(true);
if (is_null($authObj)) {
    echo "Couldn't authenticate with parameters you supplied.";
    exit;
}
$jsCode = "";
if (!$authObj->isActive) {
    echo "Missing parameters for OAuth authentication.";
    exit;
}
$err = "No Error";
if ($authObj->afterAuth()) {
    $jsCode = $authObj->javaScriptCode();
    if ($authObj->debugMode) {
        $err = $authObj->errorMessages();
    }
    if ($authObj->isCreate()) {
        // In the case of newly logged-in, you can add any code for sending email or others.
    }
} else {
    $err = $authObj->errorMessages();
}
header("Content-Type: text/html; charset=UTF-8");
?>
// 以下、HTMLファイル

 最初にdebugModeプロパティの設定があります。これをtrueにすると、このページへのアクセスで処理が止まるので、OAuthCatcher.phpでの処理のデバッグができるようになります。この後のデータベース処理などがページ上に見えるようになるので、開発中は必要に応じて値をtrueにして認証をさせてみるということになります。

 続くsetDoRedirectメソッドでは、ここでの処理が終わった時に、認証をスタートさせたページにリダイレクトするかを設定できます。通常、trueとしておくことで、元のページに移動して、おそらくは認証が成り立っているので、ページの内容を参照することができます。この値をfalseにすればリダイレクトしません。認証を受け付けてから、後で手作業等でグループに所属させたいような場合には、リダイレクトをしないようにして、OAuthCatcher.phpに必要な案内等を記述します。

 isActiveプロパティは、プロバイダとのやり取りを行なって認証が成立したかどうかを取得します。プロバイダとのやり取りは、クラスのコンストラクタで行なっているので、実際のやり取りはもう少し前で行なっています。もちろん、falseだとプロバイダ側での認証が成立していないので、その先は何も行いません。認証が成立すると、afterAuth()メソッドを呼び出します。ここでさらにプロバイダとのやり取りを行なって、ユーザ情報を得て、ユーザ名を確定しています。前にも紹介した通り、authuserテーブルにキャッシュと言うべきユーザが作られています。しかしながら、すでにユーザが作られている場合には、一部のフィールドの更新はありますが、もちろん作成はしません。ユーザを作ったかどうかは、isCreate()メソッドで問い合わせることができます。

 最終的には、javaScriptCode()メソッドでJavaScriptコードを得て、HTMLの内部にこのコードを組み込みます。このコードには、認証を開始したページへのリダイレクトのためのコードが含まれています。いずれにしても、afterAuth()メソッドを呼び出すif文のブロックあたりに、独自の処理等を記述することになると思われます。

 ここまでのOAuth2対応において、GoogleやFacebookのAPIのURLを入力するような箇所がひとつもないことにお気づきかと思われます。これら「決まっているもの」はなるべくINTER-Mediator内部で記述しています。このところはAPIの仕様変更はあまり見られなくなりましたが、将来的にはAPIのURLが違うものになったり、あるいはOAuthそのものが変化する可能性はあります。その時はINTER-Mediator側の変更で対処しなければならなくなります。

 OAuthAuthクラスのプロパティとメソッドとプロパティは、リスト7-8-5にまとめておきます。

種別記述動作
プロパティisActiveプロバイダの設定がなされていればtrue
プロパティdebugModetrueならデバッグモード(リダイレクトはしない)
メソッドsetDoRedirect(value)引数をfalseにするとリダイレクトしない(既定値はリダイレクトを実施)
メソッドafterAuth()認証の処理を行い、成功すればtrueを返す
メソッドjavaScriptCode()認証が成功したときのクライアント側の処理プログラムを出力
メソッドerrorMessage()エラーメッセージをカンマで区切った文字列
メソッドgetUserInfo()afterAuthが成功した後に得られるアカウント情報の配列。キーはそれぞれ、username、realname、emailが指定されている
メソッドisCreate()afterAuthが成功した後新たにユーザーレコードを作ったらtrue、既存のレコードを更新したらfalse
表7-8-2 OAuthAuthクラスのプロパティとメソッド

このセクションのまとめ

 INTER-MediatorはOAuth2に対応しています。プロバイダから得られたコードをparams.phpファイルに記述するだけで、認証ができるようになります。実際のシステム化においては、場合によってはリダイレクトURLの先の処理にプログラムが必要かもしれませんが、設定のみでOAuth2認証対応している点は知っておいて損はないでしょう。