Qiita OAuth API を使ってコマンドを叩くだけでQiita APIトークンを取得する

# [qiita-cli](https://github.com/increments/qiita-cli)

## 導入

Qiita公式がCLI上からQiitaに記事を投稿できる[qiita-cli](https://github.com/increments/qiita-cli)というツールを公開しています。
[qiita-cli](https://github.com/increments/qiita-cli)について詳しくはこちらを参照してください

* [自分のエディタで記事投稿ができる、Qiita CLIの使い方](https://github.com/increments/qiita-cli)
* [qiita-cli](https://github.com/increments/qiita-cli)

[qiita-cli](https://github.com/increments/qiita-cli)のインストール後の初期設定のために以下のコマンドを実行します

```
npx qiita login
```

実行後`APIトークン`の入力を求められます
`APIトークン`は[こちら](https://qiita.com/settings/tokens/new)より新しく発行するか発行済みの値を入力して設定します
 

## 所感と課題

...`APIトークン`の入力面倒くさくないですか?
`APIトークン`の値とか失念しているでしょうし、新しく`APIトークン`をつくるのは面倒くさいですし...
このような初期設定の面倒臭さは[qiita-cli](https://github.com/increments/qiita-cli)のユーザー初心者にとってライブラリを使用する上での敷居の高さにつながっているように思います

## 他の事例

上記のような点を課題と感じたきっかけとして他のCLIツールでは `API Key` の入力を求められることなく利用可能になるように作られているものが多く存在あります。
例えばGoogle App Scriptをローカルで開発するツールである[clasp](https://github.com/google/clasp)を使用した場合のインストール後の初期設定のコマンドは以下の通りになります。

```
clasp login
```

上記のコマンドを実行するブラウザがたちあがり以下のような画面が表示されます


![clasp-login1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72596/7a839cae-c5f2-d5d5-b161-4afb1749ed05.png)

「次へ」を押すと以下の画面が表示されます

![clasp-login-permissions.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72596/5edfc49a-1b47-6e2a-e610-0593d9d86427.png)

認可してもいい権限すべてをチェックして以下のように「続行」を押します

![clasp-login-permissions-to-next.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72596/790d2d0e-2c1e-28f6-112b-602f97aee69a.png)

「続行」を押した後はOAuth認可が完了し、これでclaspの各種コマンドが実行できるようになります
[clasp](https://github.com/google/clasp) だけでなくfirebase-toolsなど、他のCLIツールにおいても初期設定時には同様の推移をたどることで初期設定をすることができます
[qiita-cli](https://github.com/increments/qiita-cli)においても同様なでも実現できたらいいなぁ...


## [clasp](https://github.com/google/clasp)の事例を参考にする

[qiita-cli](https://github.com/increments/qiita-cli)でも[clasp](https://github.com/google/clasp)と同様のインターフェースを実現させるために[clasp](https://github.com/google/clasp)ではどのように実現しているか参考にする。
とりあえず[clasp](https://github.com/google/clasp)にいて該当のソースコードが↓であるので見てみます。

[auth.ts](https://github.com/google/clasp/blob/master/src/auth.ts)

詳しくはソースコードを見てください。
ソースコードより以下のことをやっていることが読み取れます

1. CLIよりローカルサーバーを立ち上げる
2. CLIよりGoogle OAuthの認可画面を開く
3. 認可した時にローカルに戻ってくる (`http://localhost`)
4. 戻ってきたときにOAuth tokenの情報を取得して、その情報をマシンに記録する

以上のことを行っており、Google OAuth APIを使用しているがローカルの中だけで完結していることが読み取れます。
(また[clasp](https://github.com/google/clasp)にはGoogle OAuthに関する情報がすべてべた書きされている。事前にOAuth認可の戻り先のURLに `http://localhost` を指定していると思われる)
なお[clasp](https://github.com/google/clasp)の実装内容を ~~パク~~ 参考にしてCLIの中だけでGoogle APIを使用することを実装して実現できたことを確認しました。
以下は確かめてみて実現できたプロジェクトです。(開発中につき解説などについては後日もう少し整理できてから行います)

[google-photos-cli](https://github.com/TakuKobayashi/google-photos-cli)

またこのプロジェクトでは以下のようにOAuth認可の戻り先のURLに `http://localhost` としてあらかじめ指定することでlocalhostに戻せることができることを確認しました。(ちなみにGoogleのOAuth APIの場合は `http://localhost` と指定することでポート番号の区別なく(どのポート番号に)戻ることができます)

![google-oauth-api-callback.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72596/ed75a0de-f771-59a6-5de2-a61244f3a198.png)

[qiita-cli](https://github.com/increments/qiita-cli)においても[clasp](https://github.com/google/clasp)と同様のフローでのログインをするためには以下のことが実現可能である必要があります

1. Qiita OAuth APIを利用できること
2. Qiita OAuth APIの戻り先のURLにローカルホスト(`http://localhost`)を指定することができること




# Qiita Oauth API

Qiita APIには認証認可(OAuth)の機能も存在します
導入方法についてはこちらのドキュメントを参考に導入していきます

[Qiita API v2#認証認可](https://qiita.com/api/v2/docs#%E8%AA%8D%E8%A8%BC%E8%AA%8D%E5%8F%AF)

とりあえずこのQiita Oauth APIを使ってCLIの中だけでQiita APIトークンを取得することを試みていきたいと思います。

## Qiita OAuth APIの登録

[ユーザーの管理画面](https://qiita.com/settings/applications) を選択し「アプリケーションを登録する」を選択してQiita OAuth APIを使用するための登録を行います

![qiita-application-list.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72596/4e9ab6e8-c068-f104-1232-fda1455b816f.png)

各種情報の入力します、この時「リダイレクトのURL」の部分には `http://localhost:ポート番号` を指定して登録します。
※ Qiita OAuth APIではlocalhostのURLを指定することができましたが、ポート番号を指定する必要があります

![qiita-regist-token.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72596/cec2a18b-f247-7eb4-f23a-faa7bdc1db7d.png)

無事に登録が完了すると `Client Id` と`Client Secret` の値が取得できるので実際に実装するときに使用するのでこの値を控えておきます。

## CLIからQiita OAuth APIを介して実際にQiita API tokenを取得する

実際にNode.jsを使ってCLIを実行してローカルの中だけでQiita OAuth APIを介してQiita API Tokenを取得する処理を以下に記述します。

```typescript
const qiitaOauthClientTokens = {
  clientId: 'QiitaOAuthAPIClientId',
  clientSecret: 'QiitaOAuthAPIClient Secret'
};

// OAuth認可を行う時に受け取るサーバのポート番号
const serverPortNumber = 59116;

async function startLocalServer(): Promise<Server> {
  return new Promise<Server>((resolve) => {
    const server = createServer();
    enableDestroy(server);
    server.listen(serverPortNumber, () => resolve(server));
  });
}

async function recieveOauthCallbackCode(server: Server): Promise<string> {
  return new Promise<string>((resolve, reject) => {
    server.on(
      'request',
      (
        request: ReadonlyDeep<IncomingMessage>,
        response: ReadonlyDeep<ServerResponse>
      ) => {
        const urlParts = new URL(request.url ?? '', 'http://localhost')
          .searchParams;
        const code = urlParts.get('code');
        const error = urlParts.get('error');
        if (code) {
          resolve(code);
        } else {
          reject(error);
        }
        response.setHeader('Content-Type', 'text/plain;charset=utf-8');
        // OAuth認可から戻ってきたときにブラウザ画面に表示させる内容
        response.end('codeの取得が完了しました。ブラウザを閉じてください');
      }
    );
  });
}

export async function oauthLogin(): Promise<string> {
  const server = await startLocalServer();
  // OAuth認可を行う時にローカル確認を行うためのstateを指定する必要があるのでてきとうな値を作る
  const authorizeState = crypto.randomBytes(12).toString('hex');
  const permissionScope = [
    'read_qiita',
    'write_qiita',
    'read_qiita_team',
    'write_qiita_team',
  ].join('+');
  const qiitaAuthorizeUrl = `https://qiita.com/api/v2/oauth/authorize?client_id=${qiitaOauthClientTokens.clientId}&scope=${permissionScope}&state=${authorizeState}`;
  await open(qiitaAuthorizeUrl);
  const authCode = await recieveOauthCallbackCode(server).finally(() => {
    server.destroy();
  });
  const accessTokenResponse = await getAccessToken(
    qiitaOauthClientTokens.clientId,
    qiitaOauthClientTokens.clientSecret,
    authCode
  );
  return accessTokenResponse.data.token;
}
```

※上記処理を実行するために使用しているライブラリは以下の通りです。事前に`yarn add`や`npm install --save`をして導入しておいてください

* [open](https://www.npmjs.com/package/open)
* [server-destroy](https://www.npmjs.com/package/server-destroy)

上記の処理を記述後以下のように`function`を呼び出します。

```typescript
const token = await oauthLogin();
```

このようにCLIの中で呼び出すことで途中で以下のようにブラウザが立ち上がり「許可する」をおすと `token` にQiita API tokenを取得できます

![qiita-oauth-confirm.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72596/b154792d-0c29-d026-1bda-60f0d162f1aa.png)

あとはこの `token` をローカルファイルに記録するなりして活用することでQiita APIを実行することができます。

# 実は...

Qiita 公式が[qiita-cli](https://github.com/increments/qiita-cli)を公開するより前に私も非公式で[qiita-cli](https://github.com/antyuntyuntyun/qiita-cli?tab=readme-ov-file)というプロジェクトがありその開発(のお手伝い)をしていました。
こちらがForkして継続して開発していた私のプロジェクトとなります。
※なお Qiita OAuth API を利用するための `Client Id` と`Client Secret` の値は [clasp](https://github.com/google/clasp) 同様にソースコードにべた書きしていますが、ライブラリとして配布する場合はべた書きで公開状態でも問題ないと思われます。結局はローカル内で完結する話なので。

* [qiita-cli](https://github.com/TakuKobayashi/qiita-cli)

元々開発していた方が紹介している記事

[Qiita記事管理CLI(qiita-cli)をTypeScriptでつくってみた](https://qiita.com/antyuntyuntyun/items/278579fd60ecd85f6e4c)

その中で今回のQiita OAuth APIを使ってCLIの中だけでログインすることができるのかどうかを検証していました。￥・
検証の結果実現できたのですが私の開発意欲の低下に伴い、放置となっ￥てしまいました
そして気が付いたらQiita公式が[qiita-cli](https://github.com/increments/qiita-cli)を作って公開したという状況となっていました。
役割を終えた形となりました。(無事、供養されると幸いです…)

公式の[qiita-cli](https://github.com/increments/qiita-cli)にてこのQiita OAuth APIを使ってのログイン機能の実装の導入のお手伝いをするかどうかについては依頼が来たらやろうかな...と考えています