OAuth 2.0とは

川崎貴彦氏:株式会社Authleteの川崎です。本日は「OAuthとOpenID Connectの入門編」ということでオンライン勉強会を開催しますので、よろしくお願いします。最初にOAuth 2.0の概要の説明からです。

ブログに書いてある内容と一緒なんですが、まずユーザーのデータがあります。このユーザーのデータを管理するのが、リソースサーバーです。このユーザーのデータを利用したいクライアントアプリケーションがあり、リソースサーバー側にはそのデータをやり取りする口をAPIと呼びます。このAPIが用意されていれば、クライアントアプリケーションはリソースサーバーに対して「あのユーザーのデータをください」と要求を出します。

そうするとリソースサーバーは「はい、どうぞ」と、ユーザーのデータを渡す。こんな仕組みです。しかし、そのアプリが悪いアプリだったとき「ユーザーのデータをください」と言ってきて、リソースサーバーはそのまま「はい、どうぞ」とデータを渡してしまうと、悪意のあるアプリにもデータが渡ることになってしまいます。

なので何らかのAPIを守る仕組みが必要です。APIを守る仕組みのベストプラクティスは、予めクライアントアプリにアクセストークンというものを持たせておきます。このアクセストークンは、このクライアントアプリがこのユーザーのデータにアクセスできる権利を持っている証です。

クライアントアプリは、データを要求するときにこのアクセストークンを持ってデータをアクセスしに行きます。そうすると、そのリソースサーバーはそのリクエストの中からアクセストークンを取り出す。そして、このアクセストークンが本当にクライアントアプリがアクセスする権限を持っているのかを調べ、OKであれば改めてデータをクライアントアプリに渡します。

この仕組みが機能するためには、予めクライアントにアクセストークンを渡しておく必要があります。その結果、アクセストークンを発行する係が必要になります。このアクセストークンを発行する係のことを認可サーバーと呼び、これは重要なキーワードです。

簡単に、この認可サーバーとクライアントアプリの関係を説明します。認可サーバーがアクセストークンを生成して、これをクライアントアプリに発行する。こういう関係です。整理すると登場人物は3つ。認可サーバーとクライアントアプリとリソースサーバーです。

リソースサーバーはユーザーのデータを管理していてAPIを公開しています。認可サーバーがアクセストークンを生成してクライアントアプリに発行。クライアントアプリはそのアクセストークンを持ってリソースサーバーのAPIにアクセスして「ユーザーのデータをください」と言います。リソースサーバーはそのアクセストークンをリクエストの中から取り出して検証。よければデータを返すという仕組みです。

今の説明では、いきなり認可サーバーがアクセストークンを発行するという流れでしたが、実際はアクセストークンを発行する前にまずクライアントアプリケーションが認可サーバーに対して「アクセストークンをください」とお願いを行なう。すると認可サーバーはユーザーに対して「クライアントアプリに権限を与えますか?」と質問をします。

もしユーザー側がOKを出せば、そこで初めて認可サーバーはアクセストークンを生成して、これをクライアントアプリに発行。この一連のアクセストークンの要求と応答を標準化したものがRFC 6749のOAuth 2.0と言われているものです。これがOAuth 2.0の一番簡単な説明です。

OAuthの認可の流れ

具体的にユーザーにアクセストークンの発行を確認する方法は、認可サーバーは認可画面・同意画面というのを生成してユーザに見せます。この認可画面はこんなアプリがこんな権限を求めています。権限を承認しますか? ということを聞きます。

この認可画面は誰に権限を与えるのか? どんな権限を与えるのか? そのユーザーは誰か?(ユーザー認証) を確認するためのフィールドです。この3つをまとめて、認可というかたちになります。

認可の処理のときにはユーザー認証がステップの1つとして入ります。認証と認可という言葉がごっちゃになって混乱を招くことが多いんですけど、実はこういう関係になっています。

このRFC 6749というのは4つの認可フローを定義しています。

認可コードフロー、インプリシットフロー、リソースオーナー・パスワード・クレデンシャルズフロー、クライアント・クレデンシャルズフローの4つが、アクセストークンの発行手順です。他にもアクセストークンを再発行するためのリフレッシュトークンフローというのも仕様書では定義されています。

この中で一番重要なのが認可コードフローです。説明するとアプリケーションと認可サーバーがあって、ユーザーがいます。アプリケーションがあるサービスと連携をしたいときに、ユーザーに対して例えば「Facebookと連携しますか?」と聞いてきます。

そうするとユーザーがこれに対して「はい」と応答すれば、このアプリは認可サーバーの認可エンドポイントに認可リクエストを送ります。その認可エンドポイントは認可画面をユーザーに返す。ユーザーにはこの認可画面に「このアプリがこんな権限を求めています」という表示が出ます。ユーザーにしてみれば自分でアプリに許可を出したのでログインIDとパスワードを入力して「はい」を押します。

これはユーザーの権限を与えることを許可を出せばそのことは認可サーバーに伝わり、そこでまだユーザー認証が終わっていなければ認証をしてアプリに対して認可コードを発行します。これは一時的なトークンなんですけど、アプリはこの認可コードが発行されたらすぐに認可サーバーのトークンエンドポイントというところに行って、認可コードをアクセストークンと交換してアクセストークンを取得する。これがだいたいのOAuthの流れです。

OpenID Connectの概要

次にOpenID Connectの概要を説明します。これは会話形式でイメージだけ掴んでいただければなと思います。

ある人が訪ねて来ました。「こんにちは! 鈴木一朗です!」と。鈴木一朗さんは有名な名前なので来た人にしてみれば「本当ですか? 証明してください」となります。そうすると鈴木さんが「これが私の名刺です!」と株式会社〇〇の鈴木一朗ですと書いてあるものを渡す。でも受け取った人からしてみれば、この名刺では本人である証明にならない。名刺というのは誰でも偽造が可能ですから。

後日、鈴木一朗さんは会社の署名入りの名刺を持ってきました。これを受け取った側は「ちょっと確認しますのでお待ちください」と、名刺を発行した会社に問い合わせをします。

「何かご用でしょうか?」と。それで「御社が発行したと思われる名刺に署名が付いていますので、この署名が本当に御社が発行したものかを確認したいので署名検証への公開鍵をください」ということを言います。すると、公開鍵なので誰に見せてもセキュリティ上問題ありません。それで公開鍵をもらい、初めて署名検証ができます。

「公開鍵をもらった結果、確かにあなたが提示した名刺は株式会社〇〇さんが発行した名刺であることが確認できました」。なのでこの株式会社の名において、この名刺に載っている情報、名前とか住所とか電話番号というのは保証されていることがわかります。

今のこの会話の中で、発行者の署名付き名刺という概念が出てきました。この概念に相当するもののことをOpenID ConnectではIDトークンと言っています。署名付きのIDトークンが発行され、そこにユーザーの情報も含まれている。

IDトークンを発行する係がいて、これがOpenIDプロバイダーと言われています。よくIdPと略されるのはこのOpenIDプロバイダーのことです。このOpenIDプロバイダーとクライアントアプリケーションの関係を簡単に説明すると、OpenIDプロバイダーがIDトークンを生成してクライアントアプリに発行するかたちになっています。

ここでOpenIDプロバイダーはいきなりIDトークンを生成して発行をしているんですけど、発行をする前にユーザーに確認を取りに行きます。クライアントアプリが「IDトークンをください」というと、OpenIDプロバイダーはユーザーに「IDトークンを発行しますか?発行する場合は本人識別情報を提示してください」というお願いをします。

ユーザーも「発行してください」。そして「これは私の本人確認情報です」。それはログインID・パスワードかもしれないし、他のユーザー認証の方法かもしれません。何かしらを提示し、初めてOpenIDプロバイダーはIDトークンを生成してクライアントアプリにIDトークンを発行します。

ここを標準化したものがOpenID Connectという仕様になります。ここまででOAuthとOpenID Connectの概念図みたいなものを紹介しました。実は先ほど紹介したOAuth 2.0と今紹介したOpenID Connectの流れがすごく似ています。

これが似ているのは実はわざとなんです。というのもOpenID Connectというのは、これはWebサイトからの抜粋なんですけど、「OpenID ConnectはOAuth 2.0の上に作られたアイデンティティレイヤーです」という話です。だからOAuthのフローの中にOpenID Connectを組み込んでいることが、似ている原因になります。

これによって何が起こるかというと、サーバー側はOpenIDプロバイダー兼認可サーバーという実装になることができます。こういうサーバーがあった場合はクライアントは何ができるかというと、IDトークンとアクセストークンを同時にリクエストすることが可能になります。リクエストされた場合は、サーバーはユーザーに確認に行きます。

「クライアントがIDトークンとアクセストークンの発行をお願いしています。発行を許可しますか?」と「発行する場合は本人確認情報を提示してください」。ユーザーが許可を出して本人確認情報として正しいものを提示すればOpenIDプロバイダーはIDトークンとアクセストークンの2つを作って、これをまとめてクライアントアプリに発行します。

ここまでがOpenID Connectの簡単な説明になります。

JWSとは?

次にJWS、JWE、JWTという技術キーワードの説明です。この技術がOAuthとOpenID Connectの中で非常によく使われているので、これをちゃんと理解しておかないと仕様書を読むことが、なかなか難しくなります。この仕様なんですけど、実はRFCになっていまして7515から7519まで一連の仕様です。

RFC 7515がJWSで、これがJSON Web Signature。7516がJSON Web Encryption、7517がJSON Web Keyです。JWAがJSON Web Algorithmsで、JWTがJSON Web Tokenで、この辺りの仕様はよくできていて、さまざまなところに使われています。ちなみに2014年に賞をもらっているほどです。

JWSがどんなかたちをしているのかというと、訳のわからない文字列になっています。見た目では何が書いてあるのかわからない。ただ、この中にピリオドが2つあり、ヘッダーの部分、ペイロードの部分、署名の部分と、3つにわかれています。

この3つに分かれた部分をピリオドでつないだ形式になっている。この形式というのは実はRFC 7515の仕様のセクションの7.1で定義されている仕様です。JWS Compact Serializationというフォーマットになっていて、この仕様書をよく見ていくと次のように書かれています。

仕様書を見るとピリオドが2つあって、3つの部分でヘッダーとペイロード、署名があります。よく見るとBASE64URLと、書いてあります。どういう意味かというと、各部分はBASE64URLという方式でエンコードされています。逆に言うとBASE64URLでデコードすれば元の情報が得られるということです。なのでちょっとやってみましょう。

これがJWS形式のIDトークンで、一番初めのヘッダーの部分です。これをBASE64URLでデコードするとJSONが出現して、このペイロードの部分もデコードすると明らかにJSONの形式のものが出てきます。最後の署名の部分はデコードすると、バイナリデータしか出てこないので、何が書いてあるかはわかりません。

ヘッダーの部分に何が書かれているかというと、JSONでkidが〇〇、algが〇〇と書かれています。JWSのヘッダーの部分はJSONで、これは仕様書で決まっているんですね。この中でalgというプロパティが、署名アルゴリズムを表す必須のパラメータです。

この値として今はRS256という値が入っていて、これはアルゴリズムの種類です。このRS256が何を表すかというと、RFC 7518のJWAという仕様書に表が書いてあります。その表を参照するとRS256というのはこんな感じのアルゴリズムを指していることがわかります。

このalg以外のヘッダーに入りそうなプロパティというのはRFC 7515に列挙されて、kidなどが入っています。

今はヘッダーの説明をしたが、今度はこのJWSのペイロードの部分。仕様書を見ると任意のバイト列と書いてあります。JWSの仕様書自体はペイロードの部分がJSONであることは要求していません。任意のバイナリデータを突っ込んでもいいが、さっきのIDトークンのペイロードを見たら明らかに何かのルールに基づいたJSONが入っているとわかります。

これはJWTという仕様から来ています。ペイロード部分はJSONでないといけないですよと制限を付けている仕様がJWTで、このJWTという仕様がJSONを要求していています。さらにIDトークンとはJWTの一種です。OpenID ConnectはこのJWTを拡張してIDトークンというものを定義しています。話を整理しますと、JWS、JSON Web Signatureという仕様があり、これの発展系というか拡張したものがJWTです。それをさらに発展させたものがIDトークンと呼ばれているものです。ただ、こういう関係なんですけど、これだけでは関係性としては片手落ちで、実はJWEというものもがあります。JSON Web Encryptionといい、JWTはこのJWEの一種でもあるんですね。

なのでJWTはJWSかJWEのどちらかです。

JWEとは?

JWEも見た目はわけのわからない文字列です。今度はピリオドが4つ入っていて、5つのパートに分かれています。この5つをJWEの仕様書で見ると次のように書いています。仕様書の中にピリオドが4つあり、そこから5つの部分に分かれている。それぞれヘッダー、暗号化されたキー、初期ベクター、暗号文、認証タグです。

ここもBASE64URLと書いてあるのでBASE64URLでエンコードされているんだなというのがわかります。仕様書を見ると2番目のフィールドにEncrypted Keyと書いてあり、これを日本語に訳すと「暗号化されたキー」です。

「暗号なら暗号キーでEncryption Keyじゃない?」と、「なんで暗号化されたキーなの?」と疑問をもつと思います。なぜかというと、JWEが、共有鍵を非対称鍵で暗号化するという2段階の暗号処理をしてキーを暗号化しているから、Encrypted Keyという表現になっています。この2段階の暗号処理というのは、実はJWEだけではなくて暗号の世界ではよく使われる手法です。

なので一般論としてこの2段階暗号の話をします。

暗号化する側と復号化する側がいます。まず、対称鍵のキーのペアを作る。暗号化する側が公開鍵を持っていて、復号化する側は秘密鍵を持っている。そこから何をするかというと、暗号化したい平文があり、このときに暗号化する側はランダムに共有鍵を生成します。この共有鍵で平文を暗号化して暗号文を作り、この公開鍵を使ってこの共有鍵を暗号化する。そして、この2つを復号化する側に送信するというプロセスです。

その受け取った側は秘密鍵を使って暗号化された共有鍵を復号する。そして共有鍵が取り出せたので暗号文を復号して平文に、暗号化前の元の文章を取得できます。なぜ、こんな面倒くさいことをやっているかというと、公開鍵はいくらでも公開していいので自由に配布できるという利点があります。

ただ、公開鍵暗号の処理には少し時間が必要です。なのでその暗号化された文章がすごい大きな場合、長時間必要になってしまう。一方で共有鍵形式の暗号は暗号処理のスピードは速いです。しかし、暗号化する側と復号する側で鍵を共有しないといけないので、相手に鍵を渡す手間が生じます。その手順は大変だし鍵を漏らさないようにして渡す必要があります。

その両方の長所を組み合わせたのが2段階の暗号方式です。

これはJWEの図ですが、ここでこのヘッダー部分、ここをBASE64URLでデコードすると、中にalgとencというものが入っています。このalgは共有鍵の暗号アルゴリズム、encは平文の暗号アルゴリズムです。暗号を2つ使うので2つのアルゴリズムの名前がヘッダーの中に入っています。

今はこのalgはRSA-OAEPとかが書いているんですけど、これが何のアルゴリズムを表しているかというのはJWAという仕様書に書いています。そこから抜粋したテーブルなんですけど、こういうふうにアルゴリズムが列挙されています。この中でdirというのだけ少し特殊なアルゴリズムで、これは共有鍵自体を直接本文暗号化に使うという特殊な処理なので後で説明します。

あとは平文暗号化のアルゴリズムで、6個ぐらい列挙されています。

さっき少し言った共有鍵暗号アルゴリズムの1つのdirは、Direct use of a shared symmetric as the CEK、Content Encryption Keyとして直接共有鍵暗号の方式を使う暗号アルゴリズムです。2段階暗号処理ではなく直接共有鍵暗号方式を行い、RFC 7518を見るとdirの説明はこう書いているんですね。

directly performing symmetric key encryption……共有鍵暗号方式の暗号を直接実行するアルゴリズムと書いてあります。ただ、その使う共有鍵はどうやって決めるの? といったルールはRFC 7518自体に書かれていません。なのでdirを使う場合は別途ルールを定める必要があります。

それについてOpenID Connect Coreの10.2.Encryptionというセクションでは、共有鍵はクライアントシークレットを元にして決めたものです。と書いてあります。クライアントシークレットをUTF-8で表現。それのバイト配列のSHA-2のハッシュを取ったやつみたいな感じで、そういったルールが書いてあります。これが共有鍵暗号です。

JWTとは?

JWSとJWEの説明が終わったので次にJWTの説明に入ります。これはJSON Web Tokenです。よく話題になりますが、この読み方は仕様書の最初のほうに「The suggested pronunciation of JWT is the same as the English word "jot"」 。発音はjot(ジョット)と呼んでくれたらありがたいみたいなことが仕様書に書いてあります。

ジェイダブリュティと呼んでも問題はありませんが、込み入った話をするときには短くジョットという人が多いです。JWTとは何かというと、JSON形式で表現されたクレームの集合をJWSもしくはJWEに埋め込んだものです。まずは、これについて説明していきます。

JWS形式のJWTは、まずそのJSON形式で表現されたクレームの集合があります。クレームと言っているのはJSONのプロパティのキー・バリューの組のことです。例えばissやsubというクレームがあります。

まずはこうやってJSONで表現されたデータがあって、これをBASE64URLでエンコードする。それをペイロードにして、あとは署名とヘッダーを付けてピリオド。これがJWS形式のJWTです。

次はJWE形式のJWT。JSONの元のデータがあって、これをまず暗号化します。暗号化したものをBASE64URLでエンコードして、その他のフィールドをくっつけてピリオドで連結して1つの塊にする。これがJWE形式のJWTです。署名をしたければJWSを使うし、暗号化したければJWEを使う。当然署名も暗号化も両方やりたいという話になって、これにも方法があります。データをJWSにしてからJWEでくるむか、もしくは逆にJWEで暗号化してからJWSでくるむかです。

どちらかをやれば署名も暗号化も両立可能。こういうどちらかにどちらかが入っているようなかたちのことをNested JWTと呼びます。

Nested JWTの説明をするんですけど、まずは、JWSがJWEに含まれるパターンのNested JWTの説明から。JSON形式で表現されたクレームの集合があり、これをBASE64URLでエンコードしてペイロードを作ってヘッダーとペイロードの署名を作ります。この完成したJWSを暗号化。そしてこれをBASE64URLでエンコードして暗号文として、これの周りにフィールドを作ってJWE形式にします。こういうNested JWTです。

OpenID Connect Coreのセクション2のところにID Tokenという章があります。ここには、ID TokenというのはMUST be signed using JWS and optionally both signed and then encrypted using JWS and JWE respectively……と書いてある。

後ろのほうにit MUST be signed then encrypted, with the result being a Nested JWT, as defined in JWTとあります。IDトークンというのは署名はJWSです。JWEで暗号化する場合は署名してから暗号化という順番にやってくださいという話が仕様書に書いてあります。

IDトークンにはRFC 7519より少し強い制限が掛かって、まず署名が必須です。RFC 7519では署名は必須ではありません。またIDトークンは署名が必須で、暗号化は任意。任意だけど、署名してから暗号化するという順番が決まっています。これがIDトークンの縛りですね。

次にJWTのクレームの話なんですけど、クレームはさっき言ったとようにクレーム名とクレーム値、JSONのキーバリューのことをクレームと呼んでいます。RFC 7519ではJWTのクレームの例としてissとかexpとか長いですけどこのhttpで始まるこれもクレームの名前の1つです。

ただ、明らかにissとかexpとかはルールがありそうだなとなります。ここら辺はRegistered Claim Namesで事前に登録されているクレーム名として、issはissuer。そのJWTを発行した人を表すのがiss。

あとはsubject(sub)、これは主体識別子です。識別子なのでidentifierという言葉をよく使うことがあるんですけど、subjectというのはこの文脈ではよく使われる専門用語で主体識別子を表します。

その他でJWTのexpとかだと有効期限の終了時刻とかそういう感じの情報が入っていたりして、これがJWTです。この中で重要なのはsubですね。その識別子は誰なんですか?  みたいな感じです。

IDトークンについて

IDトークンはJWTの一種です。IDトークンに含まれるクレームは、大きく分けるとエンドユーザーの認証に関するもの、クレーム,エンドユーザーの属性に関するもの、その他のものにわけられます。OpenID ConnectのCoreの仕様書を見にいくと「こういうクレームをIDトークンに入れてください」。と、書いています。

issクレームとかはJWTの仕様書にも入っていたやつですけど、他にもauth_timeみたいなIDトークンで新たに追加されたクレームとかがあります。

他にはユーザーの属性に関するStandard Claimsというのも仕様書内で定義されています。名前や性別、電話番号など個人情報属性を表すStandard Claimsが仕様書で定義されています。

クレームの種類によっては多言語対応が可能なものがあるんですね。例えばfamily_name、家族の名前といったときに表現方法はいくつもあり、アルファベットでKawasakiだったり漢字で川崎と書いたり、カタカナでカワサキと書いたりする。これを区別したい場合、クレーム名の末尾に#言語タグというのを付けて区別できます。

例えばそのfamily_nameだけども、漢字の川崎というのでfamily_nameを表現したい場合は、このfamily_nameの後ろにハッシュを付けて#ja-Hani-JPとする。これが言語タグで、言語タグ自体の仕様書はRFC 5646に定義されています。

あと残るのは特殊なクレームで、OpenID Connect Coreで定義されているクレームがat_hashです。

これはIDトークンと同時にアクセストークンが発行された場合、アクセストークンのハッシュ値をIDトークンの中に埋め込むルールがあります。あとは同様にc_hashというのがあり、これはIDトークンと認可コードが同時に発行される場合、その認可コードのハッシュ値をIDトークンに埋め込むルールです。

他にもFinancial-grade APIのほうで特殊なクレームで、s_hashというものがあります。内容として、認可リクエストにstateパラメータが含まれていた場合、そのstateパラメータのハッシュ値を取ってs_hashというクレーム名でIDトークンに埋め込みなさいという感じのルールです。

ここまででJWT系の技術の説明は終わりましたので、次はOAuth 2.0のフローについてより細かく技術的に見ていきます。