default eye-catch image.

OAuth2 Implicit Grant Flow とセキュリティ

Implicit Grant Flow を「認証」のための方法として使ってはならない、というが、ちょっと不勉強で理解が曖昧だったので、少し深く理解してみることにした。 参考にしたソースは下記記事 The problem with OAuth for Authentication. 単なる OAuth 2.0 を認証に使うと、車が通れるほどのどでかいセキュリティー・ホールができる OAuth2 implicit grant flow 自分が所有する情報に対してアクセスを認めることを認可と呼ぶ。 自分が所有する情報と自分の間に第三者が入らない場合 その鍵を自分が管理することに問題はない アクセスするための鍵を自分自身が管理し、安全が保障されている通信の中で直接鍵を利用できる。 自分が所有する情報と自分の間に第三者(Webシステムやアプリ)が入る場合 Webシステムやアプリに対して自分自身の情報に対するアクセス権を移譲する。 鍵をWebシステムやアプリに渡してしまうと、Webシステムやアプリの脆弱性により鍵そのものが危険にさらされてしまう。 代わりに合鍵(accessToken)を作成し、Webシステムやアプリは合鍵を使って自分自身の情報にアクセスするようにする。 以後、第三者は大元のアクセス権に触れずに、accessToken/refreshTokenを使う。 第三者システムを仮に実装してみると、accessToken/refreshTokenを該当システムのIDに紐付けて保存することで、そのシステム上のログインユーザにリソースオーナーへのアクセス権を付与できる implicit grant flow を認証に使う implicit grant flow を認証に使う、というのは、第三者システムが、リソースオーナーから取得したIDをそのまま第三者システムのIDとして使うことを指す。例えば、GoogleやYahooに保存されたID(emailなど)に対してアクセスを許可するだけで、第三者システムのログインそのものを許可してしまうようなものを指す。 だいたいどんなサービスを作るにしても、最初は知名度が低くて、ユーザ登録などしてくれないことがほとんど。 サービス提供者が考えるのは、GoogleやYahooなどのログインを自システムへのログインに代替できないか、ということ。 自分自身の情報へのアクセスを認めただけなのに、勝手に自分自身のログインであることの証明に使われてしまう、というのが問題。 問題は 第三者システムに悪意はなくて、ただ借りパクしたいだけなら被害はない。 第三者システムが悪い奴で、取得したaccessToken/refreshTokenを使って、本人になりすまして、別の第三者システムにログインしてしまったら...。 別の第三者システム的には、本人からのアクセスなのか、本人になりすましたシステムからのアクセスなのか、区別することができないから、CSRF対策を行って防げる攻撃ではない。

default eye-catch image.

RESTFul API 設計の掟

はじめに どうすれば、開発者に喜ばれるAPIを設計できるのか、RESTFulAPI設計のバイブル「Web API Design - Crafting Interfaces that developers love」をまとめてみます。 URLの設計方針 URLはリソースを表す URLはあくまでリソースを表す識別子とし名詞により表現すること リソースを複数形で表現すること。 リソースに対する操作を、動詞を使ったURLで表現しないこと 抽象的な名詞より具体的な名詞が良い より多くの意味を含もうとすると items といったボンヤリした単語になる。 items では、APIの利用者(開発者)にとって意味が分かりづらい。 例えば、以下のようなURLは適切。 /dogs/123 /dogs/123/age /dogs/123/color リソースに対する操作をHTTPメソッドで表現する リソースに対する操作をCRUDの4種類に分類すること CRUDを4種類のHTTPメソッドに対応させること。 操作HTTPメソッド CreatePOST ReadGET UpdatePUT DeleteDELETE リソースとHTTPメソッドの組み合わせは以下のような意味となる。 リソースPOSTcreateGETreadPUTupdateDELETEdelete /dogsdogを新規作成dog一覧を取得全てのdogを一括更新全てのdogを削除 /dogs/1234未定義ID=1234のdogを取得もしID=1234のdogがあれば更新もしID=1234のdogがあれば削除 リソース間の関連を簡潔に表現する 原理主義的に全てのリソースをURLに含めるとURLの階層が深くなりがちである。 /owners/954/dogs/123/red/runninng/park ... リソースとパラメータを分類することでリソースを表すURLの階層を浅く保つ パラメータはリスエストパラメータとして表現する /dogs?owner=954&color=&red&state=running&location=park エラーハンドリングをしっかりする API利用者にとってはAPIはブラックボックスとなる。API開発者は利用者に対してブラックボックス内部で発生した出来事を伝える義務がある。 さらにAPI開発者に対しても以下のようなメリットが生まれる TestFirstな開発を進めやすくなる プロダクトが世に出回ったとき発生したトラブルを追跡しやすくなる 以下の2つでエラーを表現する。 HTTPステータスコード HTTPレスポンス文字列 まず、以下の3つのステータスコードで済ませられないか検討する。 200 - OK 400 - Bad Request 500 - Internal Srver Error 必要であれば、以下から選んで上記に加える。 201 - Created 304 - Not Modified 404 - Not Found 401 - Unauthorized 403 - Forbidden 開発者向けメッセージ、利用者向けメッセージなどを含めるなど、レスポンス文字列を可能な限り充実させること。 バージョニング APIの更新による後方互換性を考慮し、URLにバージョンを含めること。例えば以下の通り。 api/v1/hoge/123 api/2016-06-12/hoge/123 api/hoge/123?v=1.0 旧APIをいつまで保守すべきかについて、以下のような方針を取ること。 少なくとも1個前は保守が必要 保守を停止することに対して開発者の反応を\"1サイクル\"見ること。 ここでいう\"1サイクル\"とは、開発対象による。モバイルアプリなら短いだろうしWebアプリなら長くなる。 リソース範囲を限定する方法 開発者はいつも全てのデータを必要としている訳ではない。以下の二つの戦略で取得したい範囲を限定できるべきである。 Partial Response戦略 あるデータについて常に全フィールドを返すのではなく、対応するデータのみ取得できる手段を提供すること。 リクエストパラメータに取得したいフィールドを付与することで実現すること。 Pagenation戦略 1回あたりの取得量を制限できる手段を提供すること。 例えば 100件のデータのうち、1回あたり20件を表示できるようにしたとき、page=3,limit=20 等をリクエストパラメータに付与できること。 複数のフォーマットをサポートする json、xml、csvなど複数のフォーマットをサポートする 以下のようなURL上の表現方法がある クエリパラメータに付与 : /api/v1/dogs/123?alt=json 拡張子 : /api/v1/dogs/123.json jsonフォーマットが最も普及しているためjsonをデフォルトとする jsonフォーマットを採用する場合、属性はJavaScriptの書式(CamelCase,オブジェクトタイプによる大文字小文字制御など)に従うこと 例外的な扱い HTTP Status Code を利用できない環境 HTTP Status Codeとして常に200=OKを返す レスポンスに StatusCode を表す属性を付与する PUT、DELETE等のHTTPメソッドが利用できない環境 全てをGETリクエストとする 動詞部分をクエリパラメータとする GET /api/v1/dogs?method=put&location=park など 認証 RESTfulAPIの認証で一般的なOAuth2.0を採用すること OAuth2.0と似て非なる認証を採用するのはN.G.なぜなら セキュリティ的に危険だから 開発者が慣れていないから 何度も呼び出さないといけないAPIにしない 開発者がどのようにAPIを使うか想像してAPIを設計すること 呼出回数を軽減することを心がけるkとお 例えば、ほとんどのケースである条件で絞り込んだ結果が求められるのに、必ずAPI経由で絞込みを行わせるなどは避ける 使われ方を想定したデフォルトを設定する

default eye-catch image.

Authenticate code grant flow で FitbitAPI からデータを取得する例

Fitbit APIからデータを取得するためには OAuth2 をパスする必要がある。FitbitAPI は以下の2つのflowをサポートしている。 Authenticate grant flow implicit grant flow ドキュメントでは、サーバサイドコードからFitbitAPIにアクセスする場合、Authenticate grant flow が推奨されている。 このエントリでは、djchen/oauth2 というoauth2クライアントを利用して、Authenticate grant flow をパスするサンプルを作成してみる。 composerによるoauth2クライアント(djchen/oauth2)の導入手順 dev.fitbit.comへのサンプルアプリ登録 FitbitのOAuth2認証をパスするためのサンプル実装 Fitbitからサンプルデータの取得 composerによるoauth2クライアントの導入手順 phpスクリプトをブラウザから叩ける環境を用意する。VagrantでもVPSでも何でも。 ~/wwwが DocumentRoot となるように httpd を構成する。 $ pwd /home/ikuty/www 次に、composerを使ってOAuth2クライアントをインストールする。 $ composer require league/oauth2-client djchen/oauth2-fitbit dev.fitbit.comへのサンプルアプリ登録 これから作るサンプルアプリを dev.fitbit.com に登録する必要がある。 例えば以下のような登録を行う。 Application Name: Test Application Description: This is my first test. Application Web Site: http://hoge.com:8001 Organization: Personal Organization Web Site: https://ikuty.com/ OAuth2.0 Application Type: Personal Callback URL: http://hoge.com:8001/test.php Default Data Access: readonly すると、以下を発行してもらえる。 OAUth 2.0 Client ID Client Secret OAuth 2.0: Authorization URI OAUth 2.0: Access/Refresh Token Request URI FitbitのOAuth2認証をパスするためのサンプル実装 サンプル実装、といっても本家のサンプルをそのまま流用しただけだが…。 以下の Authenticate grant flow の流れの通りとなっている。 ClientID,ClientSecretを使用して認証コードを取得する 認証コード を accessToken に変換する accessToken の有効期限に達したら refreshToken を使って accessToken を更新する $ pwd /home/ikuty/www $ vi test.php 中身は以下 <?php require_once \'./vendor/autoload.php\'; use djchenOAuth2ClientProviderFitbit; use LeagueOAuth2ClientTokenAccessToken; $provider = new Fitbit([ \'clientId\' => \'{client_id}\', // 登録時に取得したclientId \'clientSecret\' => \'{clientSecret}\', //登録時に取得したclientSecret \'redirectUri\' => \'http://hoge.com:8002/test.php\' ]); session_start(); if(!isset($_GET[\'code\'])){ $authorizationUrl = $provider->getAuthorizationUrl(); $_SESSION[\'oauth2state\'] = $provider->getState(); header(\'Location: \'.$authorizationUrl); exit; } elseif ( empty($_GET[\'state\']) || ($_GET[\'state\'] != $_SESSION[\'oauth2state\'])){ unset($_SESSION[\'oauth2state\']); exit(\'Invalid state\'); } else { try { $forceToAuth = false; $needToRewrite = false; $lines = file(\'token.txt\',FILE_IGNORE_NEW_LINES); if (($lines == false) || (count($lines) == 0) || $forceToAuth) { echo \'authorization_code->\'; //ここで 認証コード と accessToken を交換する $accessToken = $provider->getAccessToken(\'authorization_code\',[\'code\'=>$_GET[\'code\']]); $needToRewrite = true; } else { echo \'existing AccessToken->\'; $_accessToken = $lines[0]; $_refreshToken = $lines[1]; $_expiredToken = $lines[2]; $accessToken = new AccessToken([\'access_token\'=>$_accessToken, \'refresh_token\'=>$_refreshToken, \'expires_in\'=>$_expiredToken]); // accessToken の有効期限に達したら refreshToken を使って新しい accessTokenを要求する if ($accessToken->hasExpired()) { echo \'refresh AccessToken->\'; $refreshToken = $accessToken->getRefreshToken(); $accessToken = $provider->getAccessToken(\'refresh_token\',[\'refresh_token\'=>$refreshToken]); $needToRewrite = true; } } if ($needToRewrite) { $file = fopen(\"token.txt\",\"wb\"); fputs($file, $accessToken->getToken()); fputs($file, \"n\"); fputs($file, $accessToken->getRefreshToken()); fputs($file, \"n\"); fputs($file, $accessToken->getExpires()); fputs($file, \"n\"); fputs($file, $accessToken->getResourceOwnerId()); fputs($file, \"n\"); fclose($file); } } catch (Exception $e){ } } 最初、「$accessToken->getToken()を保存しておいて次回利用時に使いまわす」具体的な方法が分からなかった。oauth2-client の AccessTokenクラスの実装を見ると、コンストラクタにアクセストークン等を渡してあげればインスタンス化できることがわかった。 アクセストークンの有効期限に達すると、hasExpired()メソッドがtrueを返すようになる。その場合、リフレッシュトークンを使って新しいアクセストークンを要求する。DBに保存しておいたアクセストークンを新しい値で上書きする。 Fitbitからサンプルデータの取得 RESTfulなAPIを指定することでデータが得られる。例えばユーザのプロフィールを取得するには以下の通りとする。厳密にAPIにパラメータを全て埋め込むタイプではなくログイン情報等のセッション情報も用いられるタイプ。以下では、user-idとして\"-\"を渡すとセッションにあるユーザIDが使われるようだ。 $request = $provider->getAuthenticatedRequest( \'GET\', Fitbit::BASE_FITBIT_API_URL . \'/1/user/-/profile.json\', $accessToken, [\'headers\' => [\'Accept-Language\' => \'ja_JP\'],[\'Accept-Locale\' => \'ja_JP\']] ); $response = $provider->getResponse($request); var_dump($response);