認可サーバーが提供する2つのエンドポイント

川崎貴彦氏:認可サーバーは基本的に2つのエンドポイントを提供します。その2つは、認可エンドポイントとトークンエンドポイントです。他にもさまざまな仕様があり、他のエンドポイントを定義している仕様もありますが今日は触れません。

このRFC 6749の仕様は、この2つのエンドポイントがどう動作すべきかを書いています。クライアントアプリケーションは、この2つのエンドポイントのどちらか一方もしくは両方のエンドポイントとやり取りをしてアクセストークンをゲットするのが目標です。

認可エンドポイントとトークンエンドポイントのどちらを使うのか、もしくは両方使うのかというのはフローの種類によって決まっています。認可コードフローの場合は両方のエンドポイントを使う。インプリシットフローの場合は認可エンドポイントしか使いません。リソースオーナー・パスワード・クレデンシャルズフローはトークンエンドポイントしか使わない。クライアントクレデンシャルズフローも同様です。

リフレッシュトークンもトークンエンドポイントしか使わない。なのでインプリシットフローしかサポートしていないのであれば、認可エンドポイントだけを実装すればいい。認可コードフローをサポートしたければ両方のエンドポイントを実装する必要があります。

基本形となる「認可コードフロー」

この認可コードフローのイメージを見ていくと、登場人物としてはエンドユーザーとアプリケーションとサービスです。そして、認可サーバーを提供しているサービス、あとはそのサービスのリソースサーバーというものがあります。

まずアプリがユーザーに対して「サービスABCと連携しますか?」と聞いてくる。これに対してユーザーが「OK、連携していいよ」と言えば、このアプリからWebブラウザを経由して認可サーバーの認可エンドポイントに認可リクエストが投げられます。その認可リクエストを受けた認可エンドポイントは認可画面を生成してユーザのWebブラウザに返す。

その認可画面では「こんなアプリがこんな権限を求めています。承認しますか?」という画面がでます。そして、ユーザーは認可画面の内容を確認して、内容がよければ自分の認証情報を入れて「はい」ボタンを押す。そうすると、その情報が認可サーバーに伝わり、短命の認可コードが発行されます。

クライアントアプリがその認可コードを受け取ったら、それを同じ認可サーバーのトークンエンドポイントに提示して、その結果アクセストークンが発行される。ここまでが認可コードフローです。

アクセストークンが発行されたあとはアプリが、このアクセストークンを使って、リソースサーバのWeb APIにアクセスし、リソースを要求します。このあとはいくつか手順はありますが、リソースサーバーはそのアクセストークンが本当に正しいものかどうかを自分で直接調べてもいいし、認可サーバーに問い合わせてもかまいません。

ここでは認可サーバーに問い合わせる形式を図にしています。そのアクセストークンの情報を認可サーバーのイントロスぺクションエンドポイントというところに問い合わせる。その結果、イントロスぺクションエンドポイントはアクセストークンの情報を返してきます。そこでは、リソースサーバーはアクセストークンが有効かどうかを確認するんですね。

権限の有無や有効期限が切れていないかなどを調べたあとにOKが出れば、要求されたリソースをクライアントアプリケーションに返す。結果クライアントアプリはリソースを獲得する。

OAuth 2.0の仕様として、こういうフローがあります。この中でOAuth 2.0の仕様書で書かれている部分はどこかというと、この2、6、7、8のところです。とくにその認可画面とかは別に定義はありません。自由に作ってくださいといった感じです。

認可画面とかそこら辺の処理のところでユーザー認証が発生したり発生しなかったりしますが、そこはOAuthの仕様的にはどうでもよく、自由にやってくださいとなっています。それが、「OAuthはユーザー認証について定義していません」ということの理由です。次に、この2、6、7、8について少し細かく見ていきましょう。

2は認可リクエスト、6は認可レスポンス、7がトークンリクエスト、8がトークンレスポンスと呼ばれています。認可コードフローの認可リクエストは具体的にはこのHTTPのリクエストで、response_typeというパラメータがあったり、その下にclient_id、redirect_uri、scope、stateとあります。

このresponse_typeというのが重要なリクエストパラメータで、この値がcodeとなっていた場合は認可コードフローという意味です。そのリクエストを投げた結果、いろいろあったあとに認可レスポンスが返ってきますが、ここでcodeというパラメータで戻ってきます。これが発行された認可コードです。

この認可レスポンスはJSONで返ってくるのではなくてHTTPの302 Foundで返ってきます。Locationヘッダーにデータが入っている少し変わった形式です。これによってリダイレクトされてデータが渡ります。 これは非常にわかりにくい部分ではあるんですけど、これについて何が起こっているかというと、まず認可サーバーが認可レスポンスを返す。その結果でリダイレクションが発生して、結果はコールバックにデータが飛んできて認可レスポンスパラメータをゲットできます。これを細かく説明するとすごい時間が掛かってしまうので紹介だけに留めて、詳細はブログに書いてあるのでそちらを見てください。

こんな感じでクライアントアプリケーションは認可レスポンスに含まれる認可レスポンスパラメータ群をゲットします。

次に、認可コードを受け取ったあとにクライアントアプリは何をするか。トークンリクエストについてです。ここで重要なのがgrant_typeというパラメータがありまして、この値がauthorization_codeだと認可コードフローでのトークンリクエストだとわかります。

ここのgrant_typeがauthorization_codeの場合は下のcodeは必須パラメータです。これは認可エンドポイントで発行された認可コードを表しています。こういうリクエストを投げて返ってくるトークンレスポンスはJSONで、その中にaccess_tokenというキーが入っている。その値が発行されたアクセストークンです。これが、認可コードフローです。

利用を避けるべき「インプリシットフロー」

次にインプリシットフローの説明をします。OAuth 2.1の話やセキュリティベストカレントプラクティスなどいろいろあって、もうお聞きになっている方がいらっしゃると思いますが、インプリシットフローはもう使わないでくださいと言われているフローです。なので今後はこれを細かく勉強する必要はないんですけど、一応教養として知っておいたほうがいいということで説明をします。

これも最初にアプリが「サービスABCと連携しますか?」と聞いてくる。ここまでは認可コードフローと一緒です。認可コードフローではここで認可コードが発行されましたが、インプリシットフローではアクセストークンを直接発行するという流れになります。認可コード発行というワンクッションはありません。

ここまでがインプリシットフローです。アクセストークンをゲットしたあとはさっきと同じようにWeb APIコールをクライアントはできますという感じになります。このフローについて仕様書で定義されている部分は、ここの2と6の部分です。認可リクエストと認可レスポンスのフォーマットが仕様で決まっています。

これをよく見ていくと認可リクエストはHTTP GETのリクエストです。response_typeの値がtokenと書いてあった場合はインプリシットフローという意味になります。認可リクエストの結果、認可レスポンスが返ってきてそこでアクセストークンが発行されます。パラメータとしてここにaccess_tokenと書いてあるので、ここにアクセストークンが入っている状態です。

このレスポンスパラメータ群はフラグメント部に置かれ、フラグメント部というのは#で始まる部分以降のパラメータを指しています。これはクエリー部ではない、つまり「?」で始まっていない、という違いがあります。なぜこんな違いがあるのかというと、フラグメント部のパラメータ群というのはサーバーに送られないんですね。

それを参照できるのはWebブラウザやカスタムスキームハンドラだけ。なので、302でリダイレクトが起こってリダイレクトURIにWebブラウザをアクセスしに行くんだけど、アクセスしに行くときにここら辺のパラメータはサーバーに飛んでいきません。アクセストークンが飛んで行くのを避けようとしています。

何が起こるかというと、認可サーバーが認可レスポンスを返して……#になっていたりしますが、これでWebブラウザのLocationヘッダーが示す場所に遷移する。こんな感じでリクエストが飛んできます。これを受け取るのがコールバックのところ。

ただ、ここでは認可レスポンスのパラメータ群は見えない。#の後ろがフラグメント部なので、ここは飛んでこないので、認可レスポンスのパラメータ群を取得できません。このコールバックは、WebブラウザにJavaScriptを送り込みます。このJavaScriptはWebブラウザで動くアプリです。

そのアプリからだと、このURLのところにあるフラグメント部にアクセスできるので、そこからレスポンスパラメータをゲットできる仕組みになっています。インプリシットフローというのは基本的にWebブラウザの中で動くアプリが使うために用意されている感じです。ただ、最近はWebブラウザで動くアプリもインプリシットフローではなく認可コードフローをやりましょうという話になっています。

使ってはいけない「リソースオーナー・パスワード・クレデンシャルズフロー」

次にリソースオーナー・パスワード・クレデンシャルズフローの内容です。これも今は使ってはいけないと言われています。

もともとRFC 6749が決まって定義されたときも、このフローは定義するんだけども他に手段がない場合、もしくは移行手段、もともとユーザーIDとパスワードで全部APIコール、とかをOAuth形式に移行する場合の一時的なものとしてこれを使ってくださいという感じで用意されていました。

どういう内容かというと、アプリがユーザーに「サービスABCと連携しますか?」と聞きます。ユーザーが「はい」と答えると、アプリが「サービスABCにログインする際に使用するIDとパスワードを入力してください」という感じで要求してくるんですね。この画面を出しているのが認可サーバーではなくアプリというところがポイントです。

これで何が起こるかというと、そのアプリがユーザーに画面を表示して、ユーザーがIDとパスワードを入れようかなという感じになります。結果、トークンエンドポイントにトークンリクエストを投げる。その中にはユーザーが入力したIDとパスワードが含まれています。

ここで問題なのが、ユーザーが入力したIDとパスワードをこのアプリが取得できる点です。もし、このアプリが悪いアプリだった場合、このIDとパスワードを保存して悪用できてしまいます。以上がリソースオーナー・パスワード・クレデンシャルズフローの使用がおすすめできない理由です。

いずれにせよトークンリクエストを投げられたあとは、認可サーバーはアクセストークンをクライアントアプリに発行する。アクセストークンをもらったアプリはAPIコールが可能になります。仕様書で定義されているのはトークンリクエストとトークンレスポンスの中です。

これをHTTP的に見ていくと、アプリがトークンエンドポイントに直接アクセスしにいく。grant_typeの値がpasswordとなっていた場合、リソースオーナー・パスワード・クレデンシャルズフロー。なのでこのフローの場合は、usernameとpasswordを直接リクエストの中に埋め込んでしまっている状態です。なのでアプリは「そのIDとパスワードを教えてください」という感じでユーザーに聞いてきます。

この結果、戻ってくるJSONの中にaccess_tokenというのがあって、これが発行されたアクセストークンです。

ユーザーが関与しない「クライアント・クレデンシャルズフロー」

次はクライアント・クレデンシャルズフロー。これはユーザーが絡まないフローです。要はクライアントに対してアクセストークンを発行する。クライアントアプリがユーザーに関係なくトークンエンドポイントにトークンリクエストを投げる。このリクエストの中にクライアントIDとクライアント・シークレットが含まれています。

他のクライアント認証方式を使うのであれば、普通はクライアントIDとクライアント・シークレットが含まれていて、トークンエンドポイントではそれを確認してアクセストークンを発行するという流れです。これでクライアントアプリはAPIコールができる。ただ、ここで発行されるアクセストークンはユーザーが絡んでいないので、ユーザーに紐づいたアクセストークンではありません。クライアントが誰ですよ、という情報しかないアクセストークンです。

ユーザーに関係ないAPI……例えば天気の情報を返すAPIだけだったらこれでもいい、と。ただ、もし天気のAPIがユーザーの住んでる地域に紐づいた天気を返すAPIだったらユーザーに紐づいたアクセストークンを返さないといけないですけど。

トークンリクエストとトークンレスポンスをHTTPレベルで見ていくとこんな感じですね。grant_typeのところにclient_credentialsと書いてあれば、これはクライアント・クレデンシャルズフローだとわかります。ここではクライアントは誰ですかという認証は確実に行わないといけないので、何らかの方法でクライアントのクレデンシャルズを含めます。

ここではAuthorizationヘッダーのところに含め、その結果アクセストークンが発行されるというところです。

アクセストークン再発行のための「リフレッシュトークンフロー」

あとはリフレッシュトークンフローというのもあります。アクセストークンの再発行を受けたので、認可処理をもう1回やらずに、そういうのをスキップしてトークンの再発行を受けることです。この場合は、まずリフレッシュトークンというものを予めクライアントアプリが持っていないといけない。過去の認可リクエストの結果、アクセストークンと一緒にリフレッシュトークンも発行されていることが前提です。

そのリフレッシュトークンを持っていれば、このクライアントアプリはトークンエンドポイントにリフレッシュトークンを持ってアクセスして、アクセストークンが発行される。アクセストークンが発行されればクライアントアプリケーションはWeb APIコールができます。

これもHTTPレベルで見ていくと、grant_typeがrefresh tokenとなっているので、これを見るとリフレッシュトークンフローなんだなということがわかります。リフレッシュトークンフローの場合は、refresh_tokenというパラメータは必須。発行されてあるリフレッシュトークンを指定すると、アクセストークンが再発行されます。

アクセストークンが再発行された結果、リフレッシュトークンも発行されることがありますが、これは発行しても発行しなくてもどちらでも可能です。また、アクセストークンの再発行リクエストにて指定されたものとは別のリフレッシュトークンを新しく発行してもいいし、リフレッシュトークンを一度使ったけどそのまま使い続ける選択もできます。どちらにするかは認可サーバーの実装次第です。Authleteの実装では、どちらでも選択可能になっています。