【Node.js】Box APIでOAuth認証を実装する（アクセストークン自動更新付き）

# はじめに
Box API を OAuth2 認証経由で利用するまでの手順と、TypeScript によるトークン管理・API 呼び出しの実装方法を紹介します。

# 1. アプリ登録（Box Developer Console）
1. `https://app.box.com/developers/console`にアクセス  
2. アプリを作成
   - 「**Custom App (OAuth 2.0 with JWT or OAuth 2.0)**」を選択  
3. 必要なスコープ（権限）を設定（例: `Read and write all files and folders`）  
4. 管理者の承認をリクエスト（企業アカウントで必要）

## 2. クライアント情報を取得

- `Client ID` と `Client Secret` をメモ  
- リダイレクト URI に以下を設定  
  ```
  https://oauth.pstmn.io/v1/callback
  ```
:::note warn
企業内で **Self-signed certificate** を使用している場合、SDK の使用は困難なため、API による実装を選択します。
:::

# 3. 認可コードの取得（ブラウザ）

以下の URL にアクセス：
```
https://account.box.com/api/oauth2/authorize?client_id={Client ID}&response_type=code
```

アクセス後、次のような URL にリダイレクトされます：
```
https://oauth.pstmn.io/v1/callback?state=&code=xxx
```

この `code` を控えておきます。

# 4. リフレッシュトークンの取得（Postman）
#### リクエスト設定

- **Method**: POST  
- **URL**: `https://api.box.com/oauth2/token`  
- **Body**: `x-www-form-urlencoded`

| Key            | Value                                |
|----------------|--------------------------------------|
| `client_id`    | {Client ID}                          |
| `client_secret`| {Client Secret}                      |
| `code`         | 前ステップで取得した `code`         |
| `redirect_uri` | `https://oauth.pstmn.io/v1/callback` |
| `grant_type`   | `authorization_code`                 |

### レスポンス例
```json
{
  "access_token": "...",
  "refresh_token": "...",
  "expires_in": 3599,
  "token_type": "Bearer"
}
```
- `refresh_token` はファイル等に安全に保存して管理します。

# 5. TypeScript で Box API を使うクラスを実装

#### refresh_token を管理するストレージ
```json:storage.json
{
  "BOX_REFRESH_TOKEN": "xxx"
}
```

#### トークン管理付きの Box クラス実装

以下に、あなたの `Box.ts` クラスを**機能ごとに分割して説明**します。各部分の役割や考え方も補足しています。

#### インポート

```typescript:Box.ts
import fs from "fs";
import credentials from "@utils/constants/credentials";
import Postman from "@utils/Postman"; // 自作のHTTPユーティリティ
import Logger from "@utils/Logger";   // 自作のログユーティリティ
```

- `fs`: `refresh_token` を保存しているファイル（例：`storage.json`）の読み書きに使用。
- `credentials`: `client_id` や `client_secret` など、Box API 認証に必要な情報を保持。
- `Postman`: HTTP リクエストを簡潔に記述するためのラッパー。
- `Logger`: エラー発生時のログ出力用。

#### クラスの定義と初期状態

```typescript:Box.ts
class Box {
  private expiredAt = 0;
  private accessToken = "";
```

- `expiredAt`: アクセストークンの有効期限（`Date.now()` 形式のタイムスタンプ）。
- `accessToken`: 現在のアクセストークン（API 呼び出し時に使用）。

```typescript:Box.ts
private async checkToken(): Promise<void> {
  if (Date.now() >= this.expiredAt) {
    await this.refreshAccessToken();
  }
}
```

- アクセストークンが期限切れであれば、`refreshAccessToken` を呼び出して更新します。
- **毎回の API 呼び出し前にチェックすることで、トークン切れによる失敗を防止**します。

#### refresh_token の取得／保存

```typescript:Box.ts
private getRefreshToken(): string {
  const path = "storage.json";
  const storage = JSON.parse(fs.readFileSync(path, "utf-8"));
  return storage.BOX_REFRESH_TOKEN;
}

private setRefreshToken(token: string): void {
  const path = "storage.json";
  const storage = JSON.parse(fs.readFileSync(path, "utf-8"));
  storage.BOX_REFRESH_TOKEN = token;
  fs.writeFileSync(path, JSON.stringify(storage, null, 2));
}
```

- `getRefreshToken`: ローカルファイルから `refresh_token` を読み込みます。
- `setRefreshToken`: 取得した新しい `refresh_token` をファイルに保存します。

:::note warn
Box の `refresh_token` は毎回変わるため、毎回更新して保存する必要があります。
:::

#### アクセストークンのリフレッシュ処理

```typescript:Box.ts
private async refreshAccessToken(): Promise<void> {
  const url = `https://api.box.com/oauth2/token`;
  const body = {
    client_id: credentials.BOX_CLIENT_ID,
    client_secret: credentials.BOX_CLIENT_SECRET,
    refresh_token: this.getRefreshToken(),
    grant_type: "refresh_token",
  };

  try {
    const res = await Postman.postWithEncodedString(url, body);

    if (res.status === 200) {
      this.accessToken = res.data.access_token;
      this.expiredAt = Date.now() + res.data.expires_in * 1000;
      this.setRefreshToken(res.data.refresh_token);
    } else {
      Logger.error(`Token refresh failed: ${res.status} ${JSON.stringify(res.data)}`);
      throw new Error("Token refresh failed");
    }
  } catch (error) {
    Logger.error(`Error refreshing token: ${error}`);
    throw error;
  }
}
```

- `refresh_token` を使って新しい `access_token` を取得。
- レスポンスが 200 の場合：
  - アクセストークンを更新
  - 有効期限を再設定（現在時刻＋期限秒数）
  - `refresh_token` を保存
- エラー時はログを出力し、例外をスロー。

#### ファイルのメタデータ取得API

```typescript:Box.ts
public async getFileMeta(id: string): Promise<void> {
  await this.checkToken();

  const url = `https://api.box.com/2.0/files/${id}`;
  const res = await Postman.get(url, this.accessToken);
  console.log(JSON.stringify(res.data, null, 2));
}
```

- 指定された `fileId` をもとに Box API からメタデータを取得。
- **API を叩く前に `checkToken()` を必ず呼んで、アクセストークンの有効性を保証**。

#### クラスのインスタンスをエクスポート

```typescript:Box.ts
export default new Box();
```

- `Box` クラスのシングルトンインスタンスをエクスポート。
- 複数箇所で共通のトークン管理が可能になります。



#### まとめ：このクラスの設計思想

- **責務の分離**：
  - 認証処理、トークン管理、API 呼び出しをそれぞれ独立して実装。
- **堅牢性**：
  - 有効期限切れを事前に検知し、自動更新。
- **セキュリティと可搬性の両立**：
  - `refresh_token` をローカルファイルで保持し、SDK に依存せずに軽量実装。

## 6. 使用例

```typescript:sandbox.ts
import "module-alias/register";
import Box from "@class/manager/Box";

(async () => {
  await Box.getFileMeta("1234567890"); // 取得したいファイルID
  process.exit(0);
})();
```

## 補足：セキュリティ上の注意

- `client_secret` や `refresh_token` は `.env` や secure vault 等で安全に保管してください
- ログや Git にアクセストークンを記録しないように注意してください