Box のクライアント資格情報許可によるサーバー認証を試してみる

まとめ記事ができました

このスクラップについて
Box の Business プランを契約したので管理コンソールが使えるようになった。
契約したといっても 2 週間は試用期間なのでこの間に検証が終われば費用は発生しない。
このスクラップでは Business プラン以上でしか試すことができないサーバー認証を試してみる。
サーバー認証には JWT と クライアント資格情報許可の 2 つの方法があり、JWT の方がより安全といわれるが手順が若干面倒なので最初はお手軽なクライアント資格情報許可から試してみようと思う。

関連スクラップ
Box の Business プランを契約する手続きについては下記のスクラップにまとめた。
Box の Individual プラン(無料)で試したことについては下記のスクラップにまとめた。

ドキュメントを見直す

準備
- カスタムアプリケーションの作成
- 2 要素認証の有効化
- アプリケーションの承認

カスタムアプリケーションの作成
開発者コンソールにアクセスする。
アプリの新規作成ボタンを押す。
カスタムアプリを選ぶ。
こんなに複雑だったっけ?
アプリ名は My First App、目的は自動化とした。
次へボタンを押す。
認証方法はサーバー認証(クライアント資格情報許可)とした。
アプリの作成ボタンを押す。
無事にアプリが作成された。

2 要素認証の有効化
カスタムアプリページの構成タブでクライアントシークレットを取得ボタンを押すとアラートが表示される。
Settings ボタンを押す。
アカウント ID ってマスクした方がいいのかな?
2 段階認証の設定ボタンを押す。
認証アプリを選んだ状態で次へボタンを押す。
QR コードが表示されるのでスマホの認証システムアプリなどで読み込む。
認証コードを入力して送信ボタンを押す。
電話番号を入力して送信ボタンを押す。
電話番号自体は保存されたようだがバックアップコードは表示されなかった。
気になるが閉じるボタンを押す。
2 段階認証の設定自体は終わったようだ。

ここまでは復習
ここまでは無料の Individual プランでも試せた。
これからが Business プラン以上で試しか試せないので本番になる。
気合いを入れていこう。

アプリケーションの承認
関連ドキュメントは下記になるのかな?

Enterprise 設定の確認
まずは下記を確認することから始めてみよう。
- デフォルトで公開アプリを無効にする
- デフォルトで未公開アプリを無効にする
- アプリトークンを使用する場合に管理者の承認を要求する
まずは管理コンソールにアクセスする。
2 段階認証を有効にしたので認証コードが求められるようになった。
ナビゲーションでアプリを選ぶ。
カスタムアプリマネージャタブを選ぶ。
アプリの設定ボタンを押す。
すべて無効になっていることが確認できた。

カスタムアプリの承認
下記のドキュメントを読みながら進めていく。

自動と手動
承認の手続きには自動と手動の 2 つがある。
先ほどは気が付かなかったが開発者コンソールのカスタムアプリのページに承認タブがある。
承認タブは Individual プランではなかった
確認して送信ボタンがあり、多分このボタンを押すと管理者宛てにメールが送信される。
メールにリンクが記載されていてリンク先にアクセスすれば承認が完了するのかな?

まずは自動でやってみる
確認して送信ボタンを押す。
アプリの説明を入力してから送信ボタンを押す。
リストが表示されたのでメールが送信されたようだ。

メールを確認
下記の件名のメールを受信した。
新しいカスタムアプリの承認リクエストがあります
本文に含まれるアプリの詳細を確認ボタンを押す。

アプリの承認
承認ボタンを押す。
承認ボタンを押す。
承認が成功した。

承認の確認
管理コンソール > アプリ > カスタムアプリマネージャ
開発者コンソール > マイアプリ > 承認

手動承認やスコープ変更について
クライアント ID を手動で登録することで実現できる。
これは別のアカウントでも大丈夫なのかな?
せっかく Individual プランの別アカウントも持っているので試してみよう。
承認後にスコープを変更した場合は再認証が必要になる。
これも後から試してみたい。

カスタムアプリの件数
Business プランではアプリを 1 件までしか登録できないはず。
これも別アプリを作成して試してみたい。

まずは準備が終わった
無事にサーバー認証を試してみる準備が終わった。
次からは実際にサーバー認証を試してみたい。

その前に
下記の細々としたことを確かめていきたい。
- 別アカウントのカスタムアプリを登録できる?
- 承認後にスコープを変更した場合は再認証が必要になる?
- Business プランでアプリを 2 つ以上登録できる?
- 手動によるカスタムアプリの登録

Business プランでアプリを 2 つ登録
もう 1 つアプリを作って同じ要領で承認してみた。
結論から言うとできてしまった。
それでは契約時に価格表にあったエンタープライズアプリの統合とは何なのだろう?

新しく作成したアプリを削除してみる
開発者コンソールのカスタムアプリの一般設定タブに含まれるアプリを削除ボタンを押す。
2 段階認証の認証コードを入力する。
再び削除ボタンを押す。
確認モーダルに含まれる削除ボタンを押す。
アプリが削除される。
不思議なことに管理コンソールのカスタムアプリマネージャのページには残っている。
仕方がないので手動で削除しておく。
削除されなかった。
問い合わせが必要になってしまった。
裏側のことはよくわからないけど Box のデータでは承認はカスタムアプリを参照しているのではなくカスタムアプリのクライアント ID を格納しているのかな?

カスタムアプリ削除前に承認を削除してみる
結果は同じで問い合わせが必要だった。

カスタムアプリの手動登録
前と同じ要領でカスタムアプリを作成する。
作成したらクライアント ID をコピーする。
管理コンソールのカスタムアプリマネージャのページにあるアプリの追加ボタンを押す。
クライアント ID を入力してから次へボタンを押す。
承認ボタンを押す。
無事に登録された。

別アカウントのカスタムアプリ承認
Individual プランで契約しているアカウントのカスタムアプリを登録しようとしてみる。
結論だけ言うとできた。

承認後のスコープ変更
カスタムアプリの構成タブのアプリケーションセクションで書き込みにチェックを入れてから変更を保存ボタンを押す。

カスタムアプリ側では一見すると特に変化はない
ただし詳細ページでは書き込みがスコープに含まれていないことを確認できる。
再認証してみる。
アプリのスコープが増えたことが確認できた。

ほぼ期待通りだった
カスタムアプリを 2 つ以上承認できる点を除いては。
Business プランのエンタープライズアプリの統合 1 件とはどういうことなのだろう。

チャットで聞いてみる
なぜか無料試用を勧められて終わった。
もう 1 回聞いてみよう。

サポートページで検索してみる
チャットはあまり役に立たなかったけどサポートページの方には有用な情報がありそう。

今日はここまで
結局エンタープライズアプリ統合がわからなかった。
いずれわかる時が来るだろう。
一旦置いておいて次回は本題のサーバー認証を試してみよう。

今日はここから
今日こそサーバー認証を試してみる。

ワークスペースの作成
少し時間が空いたので復習を兼ねて Kotlin で Box API Java SDK を使う準備をしてみる。
まずは IntelliJ IDEA を起動して下記の内容でプロジェクトを作成する。

jvmToolchain の jdkVersion 修正
build.gradle.kts を開いて jvmToolchain の jdkVersion を 11 → 8 に修正する。
kotlin {
jvmToolchain(8)
}

main() 関数の実行
Ctrl + R か ▶︎ ボタンを使って main() 関数を実行する。
Hello World!

Box API Java SDK の追加
build.gradle.kts を開いて dependencies に Box API Java SDK を追加する。
dependencies {
testImplementation(kotlin("test"))
implementation("com.box:box-java-sdk:4.0.1") // この行を追加しました。
}

Gradle の更新
IntelliJ IDEA の Gradle ツールウィンドウを開いて左上の更新 🔃 ボタンを押す。

アクセス情報の取得
アクセスするには 3 つの情報が必要となる。
- クライアント ID
- クライアントシークレット
- エンタープライズ ID
クライアント ID とクライアントシークレットは開発者コンソールのカスタムアプリの構成タブから取得できる。
エンタープライズ ID はおそらく接続先の Box アカウント(多くの場合はカスタムアプリを作成した組織と同じ)のことで管理コンソールのアカウントと請求ページから取得できる

環境変数の設定
IntelliJ IDEA のメニュー > Run > Edit Configurations... を選んで環境変数を設定する。
- BOX_CLIENT_ID
- BOX_CLIENT_SECRET
- BOX_ENTERPRISE_ID

環境変数の読み込みテスト
fun main(args: Array<String>) {
val clientId = System.getenv("BOX_CLIENT_ID")
val clientSecret = System.getenv("BOX_CLIENT_SECRET")
val enterpriseID = System.getenv("BOX_ENTERPRISE_ID")
println("%s %s %s".format(clientId, clientSecret, enterpriseID))
}
xxxx yyyy zzzz
Java に比べて Kotlin が快適過ぎる。

動作確認用フォルダの作成
ルートフォルダ一覧の動作確認のために It works! と言う名前でフォルダを作成しておく。

動作確認
It works! と表示されなかった。
start
end
よくわからないけどサービスアカウントという概念について知る必要がありそう。

ユーザータイプ
Box Enterprise では下記 4 つのユーザータイプがある。
- 管理者(Admin)
- 管理対象ユーザー(Managed user)
- サービスアカウント(Service account)
- App User(App user)
たくさんあってわかりにくいが下記の表がわかりやすかった。
管理者権限 管理者以外の権限 従来のユーザー 管理者 管理対象ユーザー Platform のみ サービスアカウント App User

もしも Buisness Plus 以上だったら
管理コンソールのコンテンツページから動作を確認できたかも知れない。

しばらくドキュメントを読もう
少し手をとめてサービスアカウント周りのドキュメントを熟読しよう。

サービスアカウントには独自のフォルダツリーが用意される
だからフォルダー(フォルダー名:It works!)が見えなかったのか、納得した。

コラボレーションとは何か?
ドキュメントで出てきたので気になった。
既存のコンテンツでのコラボレーションにサービスアカウントを追加するには、他のユーザーの場合と同様、割り当てられたメールアドレスを使用してサービスアカウントを招待します。APIを使用してコラボレーションを追加する場合は、すでにコンテンツへのアクセス権があり、コラボレータを招待するための適切なコラボレーション権限を持っているユーザーのアクセストークンを使用する必要があります。また、サービスアカウントのユーザーIDも使用します。このユーザーIDは、サービスアカウントのアクセストークンを使用して現在のユーザーを取得エンドポイントを呼び出したときに返されます。
下記のドキュメントを読む限りだとファイル共有機能のような感じがする。

サービスアカウントをコラボレーターに追加してみる
共有を押す。
サービスアカウントのメールアドレスを入力する。
サービスアカウントのメールアドレスは開発者コンソールのカスタムアプリの一般設定に表示されているもので良いのかな?
入力したら送信ボタンを押す。
フォルダーの色が変わった。

この状態で再実行してみる
IntelliJ IDEA で Ctrl + R で実行してみる。
start
[204677117397] It works!
end
It works! ディレクトリが表示された!
まさか成功するとは思わなかった。

ユーザーの代理で操作
クライアント資格情報許可を使用するサーバー側認証では、エンドユーザーによる操作が必要ありません。また、適切な権限が付与されていれば、この認証方法を使用して、企業内の任意のユーザーの代理で操作することができます。IDの確認には、アプリケーションのクライアントIDとクライアントシークレットを使用します。
Box API Java SDK の GitHub ページのドキュメントによると下記のようにすれば良いのかな?
api.asUser("USER-ID");

ユーザー一覧の取得
ユーザーの代理で操作するにはユーザー ID が必要だがどうやって取得すれば良いかわからない。
API を使って取得しようとしてみる。
import com.box.sdk.BoxCCGAPIConnection
import com.box.sdk.BoxUser
fun main(args: Array<String>) {
val clientID = System.getenv("BOX_CLIENT_ID")
val clientSecret = System.getenv("BOX_CLIENT_SECRET")
val enterpriseID = System.getenv("BOX_ENTERPRISE_ID")
val api = BoxCCGAPIConnection.applicationServiceAccountConnection(clientID, clientSecret, enterpriseID)
val users = BoxUser.getAllEnterpriseUsers(api)
println("start")
for (user in users) {
println("%s".format(user.name))
}
println("end")
}
start
end
何も表示されなかった。
権限が不足しているのかな?

スコープの追加
開発者コンソールからカスタムアプリの構成タブでユーザーを管理するスコープを追加する。
忘れずに変更ボタンを押す。

アプリの再承認
管理コンソールでアプリを再承認する。
再承認ボタンを押す。

ユーザー ID の確認方法
ガイドに説明があった。
ページ右上のアカウントアイコンを押してアカウント設定を選ぶ。
アカウントの詳細セクションに表示されている。
上の画像で黒塗りされている部分に表示されるアカウント ID がユーザー ID と等しいようだ。

今日はここまで
無事にサーバー認証を行うことができた。
コラボレーション(ファイル共有)を使って動作確認もできた。
ユーザー ID の一覧は開発者トークンを利用すればできるようなので次回試してみたい。
それが終わったらユーザーの代理としてアクセスすることを検証してみよう。

今日はここから
まずはユーザー ID の一覧を試してみよう。

開発者トークンの取得
開発者コンソールのカスタムアプリの構成タブで開発者トークンを生成ボタンを押す。
開発者トークンは環境変数に BOX_DEVELOPER_TOKEN
として設定しておく。

ユーザー一覧取得の再挑戦
ソースコードに変更を加える。
import com.box.sdk.BoxAPIConnection
import com.box.sdk.BoxUser
fun main(args: Array<String>) {
val developerToken = System.getenv("BOX_DEVELOPER_TOKEN")
val api = BoxAPIConnection(developerToken)
val users = BoxUser.getAllEnterpriseUsers(api)
println("start")
for (user in users) {
println("%s: %s".format(user.name, user.id))
}
println("end")
}
Ctrl + R を押して実行する。
start
薄田 達哉: 12345678901
end
成功した、嬉しい。
一応アカウント設定ページのアカウント詳細セクションに表示されるアカウント ID を確認した所、同じ値だった。

代理アクセスの設定
開発者コンソールからカスタムアプリで as-user ヘッダーを許可する必要がありそう。
許可したら今度は管理コンソールで再承認する必要がありそう。
まずは全く設定しないでやってみてどうなるかを確認してみようと思う。

環境変数の追加
先ほど確認したユーザー ID を BOX_USER_ID
として環境変数に追加する。

コーディング
import com.box.sdk.BoxCCGAPIConnection
import com.box.sdk.BoxFolder
fun main(args: Array<String>) {
val clientID = System.getenv("BOX_CLIENT_ID")
val clientSecret = System.getenv("BOX_CLIENT_SECRET")
val enterpriseID = System.getenv("BOX_ENTERPRISE_ID")
val userID = System.getenv("BOX_USER_ID")
val api = BoxCCGAPIConnection.applicationServiceAccountConnection(clientID, clientSecret, enterpriseID)
api.asUser(userID)
val rootFolder = BoxFolder.getRootFolder(api)
println("start")
for (itemInfo in rootFolder) {
println("[%s] %s".format(itemInfo.id, itemInfo.name))
}
println("end")
}

動作確認
期待通り権限不足で実行が失敗した。
com.box.sdk.BoxAPIResponseException: The API returned an error code [403 | 3m617nhdbm0movmx.097f6bd405b5d7706dddc7374cf2d33c3] access_denied_insufficient_permissions - Access denied - insufficient permission

代理アクセスはクライアント資格情報許可ではできないかも
as-user ヘッダーの許可と再認証を行なってから再度実行してみたが結果は変わらなかった。
なんでだろうと思って調べたらサーバー認証にクライアント資格情報許可ではなくて JWT を使う必要がありそう。
as-userヘッダーを利用すると、JWTアプリケーションは別のユーザーの代理になることができます。

後から試してみたところクライアント資格情報許可でも代理アクセスができました。
権限の設定と再承認に少しコツが必要になります。
詳細は下記のスクラップに記載しております。

新しいスクラップを作った
JWT となるとテーマが変わるので新しいスクラップを作った。

まとめたらクローズする
このスクラップを通じて学んだことをまとめたらクローズしようと思う。

まとめ
今回のスクラップ作成を通じて学んだことは下記の通り。
- サーバ認証を試すには Box Business プラン以上が必要になる。
- もしかしたら Box Business Starter でも大丈夫かも知れない。
- カスタムアプリ承認には手動と自動の 2 つがある。
- カスタムアプリの権限を変更したら再認証が必要となる。
- 手動承認を使うと別アカウントのカスタムアプリも登録できる。
- Business プランでもサーバー認証のカスタムアプリを 2 つ以上登録できる。
- 承認したカスタムアプリは問い合わせないと削除できない。
- SDK から接続するにはクライアント ID・クライアントシークレット・エンタープライズ ID の 3 点が必要となる。
- エンタープライズ ID は管理コンソールのアカウントと請求ページから確認できる。
- サービスアカウントには独自のルートフォルダーが与えられる。
- サービスアカウントは API 経由でのみアクセスできる管理者のようなもの。
- おそらく App ユーザーにも独自のルートフォルダーが与えられる。
- Business Plus 以上だったら管理コンソールでサービスアカウントのルートフォルダーを一覧できそう。
- ユーザーからサービスアカウントにフォルダを共有(コラボレーション)することでサービスアカウントはユーザーのファイルにアクセスできる。
- サービスアカウントをコラボレーションに招待するとすぐに承認される。
- ユーザー ID は API を使って一覧できるが自分のだけであればアカウント設定ページからも確認できる。
不明な点は下記の通り。
- Box 価格体系ページにあるエンタープライズアプリの統合というのはいまだに何なのか謎である。

おわりに
サーバー認証を使ってファイルにアクセスできることを確認できて良かった。
サーバー認証を使った場合の初期設定はどうなるのだろう?
ユーザーさんの Box 管理者にカスタムアプリのクライアント ID を教える、カスタムアプリを承認してもらう、エンタープライズ ID を教えてもらうなどのステップが必要となりそう。
ユーザーのファイルにアクセスする必要があればユーザー ID の一覧も必要だが、これはカスタムアプリ側で取得できるのでユーザーさんの管理者にやってもらう必要は無さそう。
管理者は大変だけど OAuth 2.0 ユーザー認証のようにエンドユーザーさんに操作してもらう必要がないのでその点は楽なのかも知れない。
実際にサンプルアプリを作ってみるとイメージが湧くかも知れないので機会を作ってやってみようと思う。