JWT を使った 2-legged-OAuth で Box Content API の認証・認可を行う
はじめに
こんにちは。フロントエンドエンジニアの安部です。
プリズムの煌めき、浴びましたか? 私は浴びました。
さて、今回は2-legged OAuthと呼ばれる認可プロセスを使って、コンテンツプラットフォームであるBox(box.com)のWeb APIであるContent APIを使用してみます。
Boxとは?
クラウドプラットフォームのひとつで、書類や画像などのファイルを管理・共有・コラボレーションするサービスです(Box Japan公式サイト)。
エンタープライズ用Dropboxと考えるとわかりやすいですかね。
Content APIとは?
BoxのコンテンツやユーザをRESTfulなAPIでWebインターフェースと同様にリソース操作が出来るWeb APIです。
Content API公式ドキュメント
最終的なミッション
BoxにあるEnterprise環境下のコンテンツ(ファイル・フォルダ)の情報をWebアプリからエンドユーザが取得する
これを達成するのに必要なことは
- 2-legged OAuthを理解する
- JWT(JSON Web Token)を使った認証を理解する
- BoxのApp Auth / App Userを理解する
の3点です。
環境
Box
Enterprise環境(どのプランでもAPIは使用可能)
開発者アカウントでSMSによる2段階認証ができる
Webアプリ
バックエンド側:PHP
前提知識(分かる人は読み飛ばして下さい)
2-legged OAuthとは
アプリケーションを介してプラットフォームにアクセスする上で必要になるのは認証と認可です(認証と認可の違いについてはこちら)。
OAuthは認可の手段として広く普及していますが、その認可にはパターンがあります。
3-legged OAuthはサービスプロバイダ(box.com)・コンシューマ(Webアプリ)・エンドユーザの三者間における認可パターンです。
エンドユーザがコンシューマを通じてサービスプロバイダのコンテンツへのアクセスを求める場合、エンドユーザはサービスプロバイダで認証(ログイン)し、サービスプロバイダから認可コードを受け取ります。
この認可コードをコンシューマに引き渡し、コンシューマはこれをサービスプロバイダに渡します。
サービスプロバイダはこの認可コードの正当性を判断し、コンテンツへのアクセスを許可するアクセストークンを発行し、コンシューマに返却します。
コンシューマはサービスプロバイダにアクセストークンを渡してコンテンツにアクセスします。
この方式ではエンドユーザがサービスプロバイダのアカウントを持っていることが前提になっており、エンドユーザはWebアプリのほかにサービスプロバイダでもログインが必要となります。
そこで使用されるのが2-legged OAuthです。
この方式はサービスプロバイダ・コンシューマの二者間での認可となります。
コンシューマ側に登録された認証情報をサービスプロバイダに暗号化してリクエストし、サービスプロバイダで認証ができた場合アクセストークンを発行します。
エンドユーザはWebアプリのログイン情報のみでアプリを利用することができます。
前者の認可パターンではアクセスするコンテンツはエンドユーザの所有するものですが、後者ではコンシューマの保持するコンテンツにアクセスします。
JWT(JSON Web Token)とは
JSONに署名をつけたり、暗号化する技術として生まれた表現方法のひとつで、OAuthなどの認可プロセスでの認証情報の受け渡しに使われます。
JWTは3つの要素から成り立ちます。
- header部: 暗号化の方法やトークン方式を含んだJSON
- claim部: APIキーやリクエストの一意性を示すハッシュやそのトークンの有効期限などから成るJSON
- signature部: header部, claim部のJSONをそれぞれURLセーフな形にBase64エンコードし、更にそのエンコードされたものに鍵ペアの秘密鍵を使って署名したもの
この3つの要素をBase64エンコードし、その文字列をピリオドで連結したものがJWTとなります。
受け取った側ではデコードしたのちheader部で指定された鍵ペアの公開鍵を使って、規定された暗号化方法で復号します。
この復号に成功した場合にclaim部をリクエストとして処理します。
また、この時点でトークンの有効期限が切れていた場合や一意性がない場合は処理を行いません。
JWTは様々な言語でライブラリを使って扱うことができます。
以下のブログ記事が参考になるかと思います(ライブラリへのリンクも含まれます)。
App Auth / App Userとは
App Auth(アプリ認証)
2-legged OAuthを使ったアプリケーションの認証方式です。
エンドユーザが認証する必要なしに、Boxのアカウントを複数もつユーザでも煩わしいことなくアプリケーションを利用できます。
また、Boxの管理者はユーザーを複数発行したり、アカウントの管理に注力する必要がなくなります。
App Users(アプリユーザ)
アプリケーションに属するユーザをAPIを利用して発行することが出来る機能・またそのユーザを指します。エンドユーザと異なり、通常のログインからコンテンツを扱うものではありません。
ただし、管理側からは通常のユーザとして扱われ、コンテンツのアクセス制限をかけることができます。
つまり、アプリケーションの利用者を仮想的にアプリユーザとして扱い、権限の設定などを可能としています。
準備
Boxアプリケーションの作成と認証の準備をしていきます。
開発者アカウントの取得
Box Developersから開発者アカウントを取得します。
取得したら2段階認証を有効にしておきます。公式ドキュメントのTwo-Factor Authentication Setupの項を参考にしてください。
セキュリティのタブからログイン認証を設定します。
+81 からの国際コードのついた電話番号ですので、最初のゼロを除いて入力します。
Boxアプリケーションの作成
Webアプリに対応したBoxのアプリケーションを作成して、APIキーを発行します。
公式のHow To Get An API Keyも参考にしてください。
Boxアプリケーションを作成後、編集画面で「ユーザータイプ」をアプリユーザーに変更します。
Enterpriseアカウントで開発者アカウントを作成している場合、範囲の項目が自動的にチェックされていますので、そのままで大丈夫です。
この画面のうち、後で必要になるものは
- バックエンドパラメータのAPIキー
- OAuth2パラメータのclient_id
- OAuth2パラメータのclient_secret
の3つですので、控えておいて下さい(あとからの確認もできます)。
RSAキーペアの作成・登録
鍵ペアによる認証を行うため、RSAキーペアを作成します。今回はOpenSSLツールキットを使用します。
公開鍵をBox側に登録し、秘密鍵は一旦ローカルで保持し、Webアプリに移管します。
公式ドキュメントのApp Authのページでスクリーンショットを交えて紹介されているので、そちらも合わせて読んで下さい。
WindowsユーザーはOpenSSLコマンドを使うのににCygwin packageをインストールしておくといいでしょう。
秘密鍵の生成
$ openssl genrsa -aes256 -out private_key.pem 2048
公開鍵の生成
$ openssl rsa -pubout -in private_key.pem -out public_key.pem
パスフレーズを設定して控えておきます。
キーペアが作れたらBoxアプリケーションに公開鍵を登録します。
公開鍵ファイルは -----BEGIN PUBLIC KEY----- のようなヘッダ・フッタも含めてコピーして貼り付けます。
登録が完了するとIDが発行されるので、こちらを控えておきます。
Enterpriseからアプリケーションを登録
Enterpriseの管理コンソールからアプリケーションを登録します。
右上の設定アイコンから「ビジネス設定」を選択し、アプリタブを開きます。
カスタムアプリケーションの項目から「新しいアプリケーションを承認」で先ほど控えたアプリケーションのAPIキーを登録します。
アクセスレベルが設定にありますが、「このアプリのアプリユーザーのみ」を選択します(多分この設定しか選択できないはずです…)。
後ほど必要になりますので、enterpriseのIDを取得します。
アカウント情報タブからIDを控えておきます。
やっと準備が整いました。
トークンの発行
まずはローカル環境でアクセストークンのテスト発行が出来るようにします。
公式のApp Authのドキュメントを参考に、アクセストークンをリクエストします。
ritou/php-Akita_JOSEのライブラリを使います。
src/Akita/JOSE/以下のファイルを実行ファイルと同階層に設置してください。
実行ファイルではJWS.phpを読み込んでおきます。
また、RSAキーペアの秘密鍵もプロジェクトファイルに入れておきます。
ライブラリリポジトリのREADMEにあるとおりの使い方で問題ないのですが、1点変更の必要があります。 JWS.phpの__construct
でheader部のkey/valueをセットしているんですが、Boxではこのheader部に公開鍵のIDをセットする必要があります。
一旦$this->setHeaderItem('kid', '(公開鍵のID)');
のような形で追加しておきます。
秘密鍵へのパスを変更し、必要なデータセットを作成します。
$data = array( 'iss'=>'(APIキー)', 'sub'=>'(enterprise_id)', 'box_sub_type'=>'enterprise', 'aud'=>'https://api.box.com/oauth2/token', 'jti'=>'(一意のハッシュ値を生成)', 'exp'=>time()+60 // 有効期限をtimestampで指定します );
body部(claim)はこのような形で作成します。
秘密鍵を使って署名します。
実際のPOSTリクエストのbodyは以下のようになります。
$body = array( 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', 'client_id' => '(client_id)', 'client_secret' => '(client_secret)', 'assertion' => $token // JWTの文字列を渡します );
cURLを使ってトークンを発行します。
$url = 'https://api.box.com/oauth2/token'; $curl = curl_init($url); $options = array( CURLOPT_HTTPHEADER => array( 'Content-Type: application/x-www-form-urlencoded' ), CURLOPT_POST => true, CURLOPT_POSTFIELDS => http_build_query($body), ); curl_setopt_array($curl, $options); $result = curl_exec($curl); echo $result;
ローカルのApacheやPHPビルトインサーバからこの実行ファイルにアクセスするとトークンが表示されているはずです。
{ "access_token": "d8Za3bDNIvKlW1Og5yMR1lf8yBPBmdzU", "expires_in":3796, "restricted_to":[], "token_type":"bearer" }
このaccess_tokenはexpires_inで示されるように30秒程度で失効します。
ただ、jti(リクエストが一意であることを示す文字列)を発行しているので、失効前にもアクセストークンを更に発行することができます。
トークンを使ってリクエストを発行する
Content APIにアクセスする際はこのaccess_tokenをリクエストヘッダに入れて送信します。
今回はChrome拡張のPOSTMANを使ってリクエストを発行してみます。
リクエストが返却されたでしょうか?(されているはずです!)
この例では/usersにリクエストを飛ばしています。
APIリファレンスを見ながらとあるフォルダの情報をリクエストしてみます。
おかしいですね。ボディが空で返却されます。
レスポンスヘッダに以下の記述があります。
WWW-Authenticate →Bearer realm="Service", error="insufficient_scope", error_description="The request requires higher privileges than provided by the access token."
正常にトークンが発行されているのにリクエストの権限が不正だと弾かれます。
Boxの最大のハマりポイントがここです。
このトークンはenterprise tokenと呼ばれるトークンなのです。
Boxではファイルやフォルダはユーザやグループに権限を与えることで閲覧が可能になります。
この時点のトークンはユーザのアクセス権限についての認可がなく、enterprise環境についての認可です。
先述したApp Usersを思い出して下さい。
このenterprise tokenを使ってApp Userを発行し、そのユーザに適切な権限を与え、その上でユーザについての認可を受ける必要があるのです。
ではApp Userを作成します。
App Usersのドキュメントを参考にユーザを発行してください。
アプリユーザを発行するとBoxのWebインターフェース上で確認できるようになります。
このユーザに先ほどリクエストを送ったフォルダの権限を与えておきます。
発行したユーザのIDを取得するには最初に叩いたGET /usersでユーザを確認できます。
このユーザIDをトークン発行時のJWTbody内のsubに設定し、box_sub_typeをuserに変更します。
これでuser tokenを手に入れることが出来るようになりました。
このユーザの権限に合わせたリクエストが受け付けられます。
user tokenを発行し、POSTMANで先ほどの/folders/xxxxxxにリクエストを送ります。
レスポンスボディにfolderのオブジェクトが返却されました。
このfolderからApp Userの権限を削除するとレスポンスボディはエラーのオブジェクトとなり、レスポンスヘッダのエラーはなくなります。
実装する際に
Webアプリ上で実装する場合は先に発行したApp Userに適切な権限を与えておきます。
このユーザのIDをアプリ内で保持し、必要なフォルダやファイルのIDをサーバへのリクエストに含め、サーバはApp Userのuser tokenを発行し、リクエストを送信するようにします。
client_idやAPI_KEYは環境変数などで保持しておき、外に漏れないようにします。
また、バックエンド側のAPIはセッションやCookieなどを使い、Webアプリからのリクエストのみを受け付けるといいでしょう。
最後に
ドキュメント上ではenterprise tokenとuser tokenがどういうものなのか説明されているんですが、順序だった解説ではないので私はだいぶ苦労しました…
BoxのEnterpriseを使う機会は少ないかもしれませんが、誰かの役に立ちましたら光栄です。