OAuth 2.0 仕様は、Web対応アプリケーションとAPIのネットワーク全体で認可の決定を伝達するのに役立つ委任プロトコルを定義しています。OAuthは、ユーザー認証のメカニズムを提供するなど、さまざまなアプリケーションで使用されています。このため、多くの開発者やAPIプロバイダーは、OAuth自体が認証プロトコルであると誤って結論付け、誤ってそのように使用しています。もう一度明確に言いましょう。
混乱の多くは、OAuthが認証プロトコルの内部で使用されているという事実から生じています。開発者はOAuthコンポーネントを見てOAuthフローと対話し、OAuthを使用するだけでユーザー認証を実行できると想定します。これは真実ではないだけでなく、サービスプロバイダー、開発者、エンドユーザーにとって危険です。
この記事は、OAuth 2.0をベースとして認証およびID APIを構築する方法について、潜在的なIDプロバイダーを支援することを目的としています。基本的に、「OAuth 2.0があり、認証とIDが必要」と言っている場合は、このまま読み進めてください。
アプリケーションにアクセスするユーザーのコンテキストにおける認証とは、アプリケーションに現在のユーザーが誰であるか、およびユーザーが存在するかどうかを伝えます。完全な認証プロトコルでは、おそらく、一意の識別子、メールアドレス、アプリケーションが「おはようございます」と言うときにユーザーを何と呼ぶかなど、このユーザーに関する多くの属性も通知されます。認証はすべてユーザーとそのアプリケーションでの存在に関するものであり、インターネット規模の認証プロトコルは、ネットワークとセキュリティの境界を越えてこれを実行できる必要があります。
ただし、OAuthはアプリケーションにこれらの情報を一切伝えません。OAuthはユーザーについて何も言いませんし、ユーザーが自分の存在をどのように証明したか、あるいはまだそこにいるかどうかさえも言いません。OAuthクライアントに関する限り、トークンを要求し、トークンを取得し、最終的にそのトークンを使用して一部のAPIにアクセスしました。誰がアプリケーションを承認したか、あるいはユーザーがそこにいたのかどうかさえも知りません。実際、OAuthの要点の多くは、ユーザーが存在しないクライアントとアクセスされているリソース間の接続で使用するために、この委任されたアクセス権を与えることです。これはクライアント認証には最適ですが、ユーザーがそこにいるかどうか(そして誰であるか)を把握することが要点である認証には本当に適していません。
このトピックをさらに混乱させる要因として、OAuthプロセスには通常、いくつかの種類の認証が含まれています。リソース所有者は承認手順で承認サーバーに対して認証を行い、クライアントはトークンエンドポイントで承認サーバーに対して認証を行い、その他の場合もあります。OAuthプロトコル内にこれらの認証イベントが存在しても、OAuthプロトコル自体が認証を確実に伝達できるということにはなりません。
しかし、実際には、OAuthと共に使用して、この委任および承認プロトコルの上に認証およびIDプロトコルを作成できるものがいくつかあります。これらのほぼすべての場合において、OAuthのコア機能はそのまま残り、実際に起こっているのは、ユーザーがログインしようとしているアプリケーションに自分のIDへのアクセスを委任していることです。その後、クライアントアプリケーションはID APIのコンシューマーになり、クライアントを最初に承認したユーザーを把握します。このように承認の上に認証を構築することの主な利点の1つは、エンドユーザーの同意を管理できることです。これは、インターネット規模のクロスドメインIDフェデレーションにおいて非常に重要です。もう1つの重要な利点は、ユーザーが自分のIDと並行して他の保護されたAPIへのアクセスを同時に委任できるため、アプリケーション開発者とエンドユーザーの両方にとって管理がはるかに簡単になることです。1回の呼び出しで、アプリケーションはユーザーがログインしているかどうか、アプリがユーザーを何と呼ぶべきか、印刷用の写真をダウンロードするか、メッセージストリームに更新を投稿するかを把握できます。このシンプルさは非常に魅力的ですが、両方を同時に行うことで、多くの開発者は2つの機能を混同しています。
物事を明確にするために、チョコレートとファッジの比喩で問題を考えることが役立つかもしれません。最初から、これら2つのものの性質はまったく異なります。チョコレートは材料であり、ファッジはお菓子です。チョコレートは多くの異なるものを作るために使用でき、単独で使用することもできます。ファッジは多くの異なるものから作ることができ、そのうちの1つはチョコレートかもしれませんが、ファッジを作るには複数の材料が必要であり、チョコレートは含まれない場合もあります。そのため、チョコレートはファッジに等しいと言うのは正しくなく、チョコレートはチョコレートファッジに等しいと言うのは確かに言い過ぎです。
このメタファーでは、OAuthはチョコレートです。これは、多くの異なるものの基礎となる汎用性の高い材料であり、単独で大きな効果を発揮することもできます。認証はファッジのようなものです。機能させるには、少なくともいくつかの材料を適切な方法で組み合わせる必要があり、OAuthはこれらの材料の1つ(おそらく主要な材料)になる可能性がありますが、必ずしも含まれる必要はありません。何をどのように組み合わせるかを指示するレシピが必要であり、それをどのように達成できるかを指示する多くの異なるレシピがあります。
そして実際、Facebook Connect、Sign In With Twitter、OpenID Connect(Googleのサインインシステムなどを支えている)など、特定のプロバイダーでこれを行うためのよく知られたレシピがいくつかあります。これらのレシピはそれぞれ、共通のプロファイルAPIなど、多くの項目をOAuthに追加して認証プロトコルを作成します。OAuthなしで認証プロトコルを構築できますか?もちろん、チョコレートを含まないファッジの種類がたくさんあるように、多くの種類があります。しかし、今日ここで話しているのは、特にOAuth 2.0上に構築された認証、何が問題になる可能性があるか、そしてそれをどのように安全でおいしいものにすることができるかということです。
OAuthを使用して認証プロトコルを構築することは非常に可能ですが、IDプロバイダー側またはIDコンシューマー側のいずれかで、これを行う際に多くの人がつまずく傾向があるものがいくつかあります。この記事で説明されているプラクティスは、潜在的なIDプロバイダーに一般的なリスクを知らせ、OAuthベースの認証システムを使用する際にコンシューマーが回避できる一般的な間違いを知らせることを目的としています。
認証は通常アクセストークンの発行前に発生するため、任意のタイプのアクセストークンを受け取れば認証が発生したという証拠と見なしたくなります。ただし、アクセストークンを単に所有しているだけでは、クライアントには何もわかりません。OAuthでは、トークンはクライアントに対して不透明になるように設計されていますが、ユーザー認証のコンテキストでは、クライアントはトークンからいくつかの情報を導き出すことができる必要があります。
この問題は、クライアントがOAuthアクセストークンの意図されたオーディエンスではないという事実から生じています。代わりに、それはそのトークンの承認されたプレゼンターであり、オーディエンスは実際には保護されたリソースです。保護されたリソースは、一般的にトークンだけではユーザーがまだ存在するかどうかを判断できる立場にありません。OAuthプロトコルの性質と設計上、ユーザーはクライアントと保護されたリソース間の接続では利用できないためです。これに対抗するには、クライアント自体に向けられたアーティファクトが必要です。これは、アクセストークンを二重に使用し、クライアントが解析して理解できる形式を定義することによって行うことができます。ただし、一般的なOAuthはアクセストークン自体の特定の形式または構造を定義していないため、OpenID ConnectのIDトークンやFacebook Connectの署名付きレスポンスなどのプロトコルは、アクセストークンと共に認証情報をクライアントに直接伝えるセカンダリトークンを提供します。これにより、通常のOAuthと同様に、プライマリアクセストークンはクライアントに対して不透明なままになります。
アクセストークンは一連のユーザー属性と交換できるため、有効なアクセストークンを所有しているだけでユーザーが認証されていることを証明するのに十分だと考えがちです。この仮定は、承認サーバーでユーザーが認証されたコンテキストでトークンが新しく作成された場合など、いくつかのケースでは当てはまります。ただし、OAuthでアクセストークンを取得する方法はそれだけではありません。リフレッシュトークンとアサーションを使用すると、ユーザーが存在しなくてもアクセストークンを取得でき、場合によってはユーザーが認証しなくてもアクセス許可が付与される場合があります。
さらに、アクセストークンは、一般的にユーザーが存在しなくなってからも長い間使用できます。OAuthは委任プロトコルであるため、これはその設計の基本です。これは、クライアントが認証がまだ有効であることを確認したい場合、OAuthで保護されたリソースであるID APIは、ユーザーがそこにいるかどうかを判断する方法がないことが多いため、トークンをユーザーの属性と交換するだけでは不十分であることを意味します。
クライアントがトークンエンドポイントからのリターンコール以外のソースからアクセストークンを受け入れると、追加の(そして非常に危険な)脅威が発生します。これは、暗黙的フロー(トークンがURLハッシュのパラメーターとして直接渡される)を使用し、OAuthのstate
パラメーターを適切に使用しないクライアントで発生する可能性があります。この問題は、アプリケーションの異なる部分がコンポーネント間でアクセストークンを渡してアクセスを「共有」する場合にも発生する可能性があります。これは、アクセストークンが外部の当事者によってアプリケーションに挿入される可能性があり(そしてアプリケーションの外部にリークする可能性がある)、問題となります。クライアントアプリケーションが何らかのメカニズムを通じてアクセストークンを検証しない場合、有効なトークンと攻撃トークンを区別する方法がありません。
これは、認可コードフローを使用し、認可サーバーのトークンエンドポイントからのみトークンを受け入れ、攻撃者が推測できないstate
値を使用することで軽減できます。
アクセストークンを一連の属性と交換して現在のユーザーを取得する際のもう1つの問題は、ほとんどのOAuth APIが返された情報に対してオーディエンス制限のメカニズムを提供していないことです。言い換えれば、単純なクライアントに別のクライアントからの(有効な)トークンを渡し、単純なクライアントがこれを「ログイン」イベントとして扱うことは非常に可能です。結局のところ、トークンは有効であり、APIへの呼び出しは有効なユーザー情報を返します。問題は、もちろん、ユーザーが自分が存在することを証明するためには何もしておらず、この場合、単純なクライアントを認可さえしていないことです。
この問題は、クライアントが認識して検証できる識別子と共に認証情報をクライアントに伝達することで軽減できます。これにより、クライアントは自身に対する認証と別のアプリケーションに対する認証を区別できます。また、OAuthで保護されたAPIなどの二次的なメカニズムではなく、OAuthプロセス中に認証情報を直接クライアントに渡すことによっても軽減されます。これにより、クライアントはプロセス後半で不明な信頼できない情報が挿入されるのを防ぐことができます。
攻撃者がクライアントからの呼び出しの1つを傍受または乗っ取ることができた場合、クライアントが何か問題があったことを知ることなく、返されたユーザー情報のコンテンツを変更できます。これにより、攻撃者は正しい呼び出しシーケンスでユーザー識別子を交換するだけで、単純なクライアントでユーザーになりすますことができます。これは、認証プロトコルプロセス中に(OAuthトークンと共に)IDプロバイダーから直接認証情報を取得し、検証可能な署名で認証情報を保護することで軽減できます。
OAuthベースのID APIの最大の問題の1つは、完全に標準に準拠したOAuthメカニズムを使用している場合でも、異なるプロバイダーが必然的に実際のID APIの詳細を異なる方法で実装することです。たとえば、ユーザーの識別子は、あるプロバイダーではuser_id
フィールドにありますが、別のプロバイダーではsubject
フィールドにあります。これらは意味的には同等ですが、処理するには2つの別々のコードパスが必要になります。言い換えれば、各プロバイダーで認可は同じ方法で行われるかもしれませんが、認証情報の伝達は異なる可能性があります。この問題は、OAuthの上に構築された標準の*認証プロトコル*をプロバイダーが使用することで軽減できます。そのため、ID情報の出所にかかわらず、同じ方法で送信されます。
この問題は、ここで説明する認証情報を伝達するためのメカニズムがOAuthの範囲外であるために発生します。OAuthは、特定のトークン形式を定義しておらず、アクセストークンに対して共通のスコープセットを定義しておらず、保護されたリソースがアクセストークンを検証する方法についてはまったく対処していません。
OpenID Connectは、2014年初頭に公開されたオープンスタンダードであり、OAuth 2.0を使用してユーザー認証を実行するための相互運用可能な方法を定義しています。本質的に、それは広く公開された*チョコレートファッジのレシピ*であり、多くの専門家によって試され、テストされています。潜在的なIDプロバイダーごとに異なるプロトコルを構築する代わりに、アプリケーションは1つのプロトコルを使用して、連携したい任意の数のプロバイダーと通信できます。オープンスタンダードであるため、OpenID Connectは制限や知的財産の問題なしに誰でも実装できます。
OpenID ConnectはOAuth 2.0上に直接構築されており、ほとんどの場合、OAuthインフラストラクチャと共に(またはその上に)デプロイされます。OpenID Connectは、JSON Object Signing And Encryption(JOSE)仕様スイートを使用して、署名および暗号化された情報をさまざまな場所に伝達します。実際、JOSE機能を備えたOAuth 2.0のデプロイメントは、完全に準拠したOpenID Connectシステムを定義するための長い道のりを歩んでおり、2つの間の差は比較的小さいです。しかし、その差は大きな違いを生み、OpenID ConnectはOAuthベースにいくつかの重要なコンポーネントを追加することで、上記の多くの落とし穴を回避できます。
OpenID Connect IDトークンは、通常のOAuthアクセストークンと共にクライアントアプリケーションに付与される署名付きのJSON Webトークン(JWT)です。IDトークンには、ユーザーの識別子(sub
)、トークンを発行したIDプロバイダーの識別子(iss
)、およびこのトークンが作成されたクライアントの識別子(aud
)など、認証セッションに関する一連のクレームが含まれています。さらに、IDトークンには、トークンの有効な(通常は短い)有効期間に関する情報と、ユーザーがプライマリ認証メカニズムを提示されてからの時間など、クライアントに伝達される認証コンテキストに関する情報が含まれています。IDトークンの形式はクライアントによって認識されているため、外部サービスに依存することなく、トークンのコンテンツを直接解析してこの情報を取得できます。さらに、アクセストークンに加えて(代わりにではなく)発行されるため、通常のOAuthで定義されているように、アクセストークンはクライアントに対して不透明なままにすることができます。最後に、トークン自体がIDプロバイダーの秘密鍵によって署名され、最初にトークンを取得するために使用されたTLSトランスポート保護に加えて、トークン内のクレームに追加の保護層が追加され、なりすまし攻撃のクラスを防ぎます。このIDトークンにいくつかの簡単なチェックを適用することにより、クライアントは多数の一般的な攻撃から身を守ることができます。
IDトークンは認可サーバーによって署名されているため、認可コード(c_hash
)とアクセストークン(at_hash
)にデタッチされた署名を追加する場所も提供します。これらのハッシュは、認可コードとアクセストークンのコンテンツをクライアントに対して不透明なままにして、クライアントが検証できるため、さまざまなインジェクション攻撃を防ぐことができます。
IDトークンには認証イベントを処理するために必要なすべての情報が含まれているため、クライアントはアクセストークンを使用する必要はありません。ただし、OAuthとの互換性を提供し、IDと他のAPIアクセスを並行して認可するという一般的な傾向に合わせるために、OpenID Connectは常にOAuthアクセストークンと共にIDトークンを発行します。
IDトークンのクレームに加えて、OpenID Connectは、現在のユーザーに関するクレームを含む標準の保護されたリソースを定義します。ここで説明したように、ここでのクレームは認証プロセスの一部ではありませんが、認証プロトコルをアプリケーション開発者にとってより価値のあるものにするバンドルされたID属性を提供します。結局のところ、「おはようございます、9XE3-JI34-00132A」ではなく「おはようございます、Jane Doe」と言う方が望ましいです。OpenID Connectは、これらの属性のサブセットにマップする標準化されたOAuthスコープのセットを定義します:profile
、email
、phone
、およびaddress
。これにより、プレーンOAuth認可リクエストはリクエストに必要な情報を伝えることができます。OpenID Connectは、IDトークンの発行とUserInfoエンドポイントへのアクセストークンによるアクセスを切り替える特別なopenid
スコープを定義します。OpenID Connectスコープは、OpenID Connect以外の他のOAuthスコープと競合することなく使用でき、発行されたアクセストークンは、潜在的にいくつかの異なる保護されたリソースをターゲットにすることができます。これにより、OpenID Connect IDシステムはOAuth認可システムとスムーズに共存できます。
OAuth 2.0は、さまざまなデプロイメントを可能にするように記述されていますが、設計上、これらのデプロイメントがどのように設定されるか、またはコンポーネントが互いについてどのように認識するかは指定していません。これは、1つの認可サーバーが特定のAPIを保護し、2つが緊密に結合されている通常のOAuthの世界では問題ありません。OpenID Connectでは、共通の保護されたAPIが、クライアントとプロバイダーのさまざまな場所にデプロイされ、それらはすべて動作するために互いについて知る必要があります。各クライアントがあらかじめ各プロバイダーについて知る必要があるのはスケーラブルではなく、各プロバイダーが各潜在的なクライアントについて知る必要があるのはさらにスケーラブルではありません。
これに対抗するために、OpenID Connectは、クライアントが特定のIDプロバイダーと対話する方法に関する情報を簡単に取得できるようにするディスカバリプロトコルを定義しています。トランザクションの反対側では、OpenID Connectは、クライアントを新しいIDプロバイダーに紹介できるようにするクライアント登録プロトコルを定義しています。これらの2つのメカニズムと共通のID APIを使用することにより、OpenID Connectはインターネット規模で機能し、当事者があらかじめ互いについて知る必要はありません。
この堅牢な認証機能をすべて備えていても、OpenID Connectは(設計上)依然としてプレーンOAuth 2.0と互換性があるため、開発者の労力を最小限に抑えてOAuthシステムの上にデプロイするのは非常に良い選択です。実際、サービスがすでにOAuthとJSON Object Signing and Encryption(JOSE)仕様(JWTを含む)を使用している場合、そのサービスはすでにOpenID Connectをサポートするための準備が整っています。
優れたクライアントアプリケーションの構築を促進するために、OpenID Connectワーキンググループは、認可コードフローを使用して基本的なOpenID Connectクライアントを構築し、暗黙的なOpenID Connectクライアント。これらのドキュメントはどちらも、開発者が基本的なOAuth 2.0クライアントを構築し、OpenID Connectに必要な少数のコンポーネントを追加する手順を説明しています。
コア仕様はかなり単純ですが、すべてのユースケースが基本メカニズムで適切に処理できるわけではありません。より高度なセキュリティデプロイメントを含む高度なユースケースをサポートするために、OpenID Connectは、標準のOAuthを超える多くのオプションの高度な機能も定義しています。これには、次のものが含まれます(とりわけ)。