oclifの簡単な使い方とコマンドラインからOAuth2.0の認証をする方法


### oclif

コマンドラインツールを作ろうと思い立ち、たまたまoclifというものに出会ったので使い方メモ
oclifを使うと、TypeScript/JavaScripで自作のCLIがかなり簡単に作れる。

oclif (https://oclif.io) は、heroku cli をつくったチームがリリースした、CLIツール作成のフレームワーク。
コマンドに渡されるパラメータやフラグの処理、ヘルプの表示など、ベースとして必要なものがスキャフォールドできるようになっている。
Mac, Windows, Linux 用にインストーラーにパッケージングして配布できるなど、CLIとして必要なものを一通り取り揃えている。

### OAuthのアクセストークンをコマンドラインから取り出す

この記事では、コマンドラインからOAuth 2.0（Authorization Code Grant）のアクセストークンを取り出す方法も、Box APIを例にして、合わせて書く。
ちなみにBox社が提供するBox cliもoclifを利用して作られている。

通常、Auth2.0はブラウザ上で認可フローが行われることが前提となっている。
通常の動作は、以下のとおり。
1. ブラウザをユーザーに向け開きログイン画面を表示
2. ユーザーがログインする
3. ブラウザ上でリダイレクトされ、アプリケーションに認可コードが渡される
4. アプリケーションは認可コードを受け取り、アクセストークンに交換、以降アクセストークンがあればリソースにアクセス可能

1〜3は必ずブラウザで実行する必要があるが、4以降は必ずしもブラウザでなくてもよく、コマンドラインツールからも利用可能。
OAuthのアクセストークンがコマンドラインで使えると、便利にAPIを呼び出せるようになるので、その方法をメモしておきたい。
ちょっとごちゃまぜ感があるが、半分個人的な備忘録なので許してほしい。

### oclifのインストール
nodeとnpmは入ってる前提ですすめる。
必ずしもそうしなくていいが、とりあえずoclifをグローバルにインストールしておく。

```sh
npm i -g oclif
```

### oclifでCLIプロジェクトを生成
これだけで、cliのプロジェクトの基本的なテンプレートが生成できてしまう。とても簡単。
`mycli`の部分は好きな名前に変える
色々聞かれるので、適当に答える

```
oclif generate mycli 
```



### 生成したコマンドの実行

```sh
cd mycli
./bin/dev hello world
```

生成された、サンプルのコマンドが実行される。
`./bin/dev` は、リリースした後は `mycli` に置き変わる。
`./bin/dev` は `node-ts`を使っているので、TSファイルのコンパイルなしで実行できる。
コンパイルするときは、`npm run build` 
コンパイル済みのjsファイルを実行するときは、`./bin/dev`の代わりに、`./bin/run`を使う

### macos用のインストーラーパッケージを作成

macos以外にも、WindowsやLinux用も作れるけど、ここではmacosのやり方だけを書く。

`pakcage.json` を開き、以下の値を追加。
他のアプリと被らない名前を付ける。

```json:package.json
  ...
  "oclif":{
    "macos": {
      "identifier": "com.example.mycli"
    }
  }
```

以下を実行 （macos用）

```sh
oclif pack macos
```

distフォルダにxx.pkgというインストーラーが生成されるので、これでインストールが行える
インストールすると、以下のコマンドが実行できるようになる。

```sh
mycli hello world
```

### OAuthでログインするコマンドを追加する

以下の手順で、OAuthのログインのためのコマンドを追加する。ここではloginという名前で作る。

```sh
# プロジェクトルートで実行
oclif generate command login
```
`src/commands/login.ts` が生成される。

### 必要なライブラリを追加する

```sh
npm i express box-node-sdk cli-ux
```
`cli-ux`は、oclifのサイドプロジェクト。URLをブラウザで開く、ハイパーリンクを表示、処理をウェイト、アクションの終了待ちの表現、などいろいろcliで便利なUXを提供してくれる。

### BOXでアプリ設定を作成

Boxからマイアプリ（開発者コンソール）を開く → アプリの新規作成をクリック → カスタムアプリ → ユーザー認証（OAuth 2.0)を選択 → 作成
作ったアプリの構成タブを開き、クライアントIDとクライアントシークレットをコピーしておく。
リダイレクトURIに、`http://localhost:3000/redirect` を入れる。


### login.tsを書き換える

OAuthの取り出しかたは、この記事を参考にした。
https://thecodebarbarian.com/oauth-in-nodejs-cli-apps.html

ちなみにBox Node SDKが現時点でTypescriptのサポートが甘く、エラーが出たためファイル先頭2行を書いている。

ポイントは、コマンドを実行すると、localhostでサーバーが立ち上がることと、Promiseのハンドラの第一引数resolveを外だしにしてコード取得を待ち合わせしていること。


```typescript:login.ts
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
import {Command} from '@oclif/core'
import cli from 'cli-ux'

const express = require('express')
const BoxNodeSDK = require('box-node-sdk')

const clientID = 'xxxxxxxxx' // BOXマイアプリからコピーしたクライアントID
const clientSecret = 'xxxxxxxxx' // BOXマイアプリからコピーしたクライアントシークレット

// box sdk
const sdk = new BoxNodeSDK({clientID, clientSecret})

// Boxの認証用URLを生成
const loginUrl = sdk.getAuthorizeURL({
  // eslint-disable-next-line camelcase
  client_id: clientID,
  // eslint-disable-next-line camelcase
  response_type: 'code',
})

export default class Login extends Command {
  public async run(): Promise<void> {
    // Boxがリダイレクトするlocalhostを一時的に立ち上げるため、サーバーを用意
    // Boxのアプリ設定で、http://localhost:3000/redirect にリダイレクトするようになっているので、
    // このコマンドを実行し、ブラウザでログインし、リダイレクトされるまでの間だけローカルにサーバーを立ち上げる。
    const app = express()

    // Promiseのハンドラ内部に渡される第一引数 resolveを外だしにして、後から使えるようにする。
    // 取り出したresolveを実行すると、pの処理が終了する。
    let resolve: any
    const p = new Promise(_resolve => {
      resolve = _resolve
    })

    // ローカスのサーバーで/redirectが呼び出されたときの処理
    app.get('/redirect', function (req: any, res: any) {
      // 上で取り出したresolveを呼び出す
      resolve(req.query.code)
      res.end('')
    })
    // サーバーを立ち上げる
    const server = await app.listen(3000)

    // Boxの認証画面をブラウザで開く。
    cli.open(loginUrl)

    // redirectの処理で、認可コードを取りだしてresolveが呼び出されるのを待つ
    const code = await p
    // 取り出した認可コードを表示
    this.log(`the code is ${code}`)
    // 不要になったので待たずにサーバーを停止
    server.close(() => {
      this.log('server stopped')
    })
    // 認可コードからアクセストークン類を取り出す（ここから通常のBOX APIの利用方法）
    const tokenInfo = await sdk.getTokensAuthorizationCodeGrant(code)
    // アクセストークンを渡して、box api clientを作成
    const client = sdk.getBasicClient(tokenInfo.accessToken)
    // ユーザー情報を取り出す
    const user = await client.users.get(client.CURRENT_USER_ID)
    this.log(user)
  }
}


```


### loginの実行

```
./bin/dev login
```

インストーラーパッケージにしてからインストールしても、同じように動作する

```
mycli login
```

注意1：最近のBOXのバグ？で、既にBOXにログイン済みのブラウザで開くと認証画面が表示されないことがあるので、ログアウトしてやり直す。
注意2：BOXのEnterpriseの設定でカスタムアプリを許可していない場合、認証画面は開くことができない。