GoogleAPI for Node.js で Authorize までを共通化する

# はじめに

Node.js を使用して GoogleAPI にアクセスする場合、SheetAPI であろうと DriveAPI であろうと認可の部分までは実は大体一緒です。

https://developers.google.com/docs/api/quickstart/nodejs
https://developers.google.com/sheets/api/quickstart/nodejs
https://developers.google.com/drive/api/v3/quickstart/nodejs

あまりプロジェクト内で各サービスを行き来する事は無いのですが、どうせ共通なら一つのファイルにまとめて使いまわしたい、と思ってクラスを作りました。サンプルの内容をts化、クラス化して、``Promise`` 対応しています。
誰でも思いつく事だと思いますが、今後の自分のため + 同じような結論に行きついた方のためにまとめておきます。

# 認可までを行うクラス

``GoogleAuthorizer`` という名称のクラスにしました。
適当な名前で、以下のファイルを保存します。ここでは ``authorize.ts`` にしました。

```ts
import fs from "fs";
import readline from "readline";
import { google } from "googleapis";
import { OAuth2Client } from "googleapis-common";

export class GoogleAuthorizer {

  private credentialsBuffer: string | null = null;
  private tokenBuffer: string | null = null;

  constructor(
    private scopes: string[],
    private tokenPath: string,
    private credentialsPath: string
  ) { }

  public async getOAuth2Client(): Promise<OAuth2Client> {
    if (this.credentialsBuffer) {
      return await this.authorize(JSON.parse(this.credentialsBuffer));
    } else {
      return new Promise<OAuth2Client>((res, rej) => {
        fs.readFile(this.credentialsPath, async (err, content) => {
          if (err) {
            rej('Error loading client secret file:' + err);
            return;
          }
          // Authorize a client with credentials, then call the Google Drive API.
          this.credentialsBuffer = content.toString();
          try {
            const o = await this.authorize(JSON.parse(content.toString()));
            res(o);
          } catch (e) {
            rej(e);
          }
        });
      });
    }
  }

  private async authorize(credentials: any): Promise<OAuth2Client> {
    return new Promise<OAuth2Client>((res, rej) => {
      const { client_secret, client_id, redirect_uris } = credentials.installed;
      const oAuth2Client = new google.auth.OAuth2(
        client_id, client_secret, redirect_uris[0]);
      if (this.tokenBuffer) {
        oAuth2Client.setCredentials(JSON.parse(this.tokenBuffer));
        res(oAuth2Client);
        return;
      }
      // Check if we have previously stored a token.
      fs.readFile(this.tokenPath, async (err, token) => {
        if (err) {
          try {
            await this.getAccessToken(oAuth2Client);
            res(oAuth2Client);
          } catch (e) {
            rej(e)
          }
          return;
        }
        this.tokenBuffer = token.toString();
        oAuth2Client.setCredentials(JSON.parse(token.toString()));
        res(oAuth2Client);
      });
    });
  }

  private getAccessToken(oAuth2Client: OAuth2Client): Promise<OAuth2Client> {
    return new Promise<OAuth2Client>((res, rej) => {
      const authUrl = oAuth2Client.generateAuthUrl({
        access_type: 'offline',
        scope: this.scopes,
      });
      console.log('Authorize this app by visiting this url:', authUrl);
      const rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout,
      });
      rl.question('Enter the code from that page here: ', (code) => {
        rl.close();
        oAuth2Client.getToken(code, (err, token) => {
          if (err || !token) {
            rej('Error retrieving access token' + err);
            return;
          }
          oAuth2Client.setCredentials(token);
          // Store the token to disk for later program executions
          fs.writeFile(this.tokenPath, JSON.stringify(token), (err) => {
            if (err) {
              rej(err);
              return;
            }
            console.log('Token stored to', this.tokenPath);
          });
          res(oAuth2Client);
        });
      });
    });
  }
}

```

あまり気にしなくても良いかもしれませんが、ファイルIOがあまり発生するのもアレかなと思って、一度ファイルから読んだ情報はバッファに入れて再利用しています。

# 使用方法

先程保存した ``authorize.ts`` を読み込み、コンストラクタに以下の引数を指定して、まずはインスタンスを作成します。
その後、``getOAuth2Client`` を ``await`` 付きで呼び出せば、必要な ``OAuth2Client`` が取得できます。

| 引数   | 内容                        |
|----    | ---                        |
| 1      | 使用するスコープ(配列)       |
| 2      | トークンファイルのパス       |
| 2      | 認証ファイルのパス           |

```ts
import { GoogleAuthorizer } from "./authorize";

class Main {

  constructor() {
    this.init();
  }

  async init() {
    const SCOPES = ['https://www.googleapis.com/auth/drive.metadata.readonly'];
    const TOKEN_PATH = "token.json";
    const CREDENTIALS_PATH = "credentials.json";

    const a = new GoogleAuthorizer(SCOPES, TOKEN_PATH, CREDENTIALS_PATH);
    try {
      const auth = await a.getOAuth2Client();
      console.log(auth);
    } catch(e) {
      console.error(e);
    }
  }

}
new Main();

```

# おまけ

冒頭で紹介した Google の Node.js サンプルページでは、**とりあえず認証情報作るボタン**がありますが、これをクリックすると QuickStart というプロジェクトが作成され、その中に適切な権限が与えられた認証情報が作成されて、認証ファイルがDLされます。
既存のプロジェクトに手動で追加する場合に困ったのですが、以下に方法が書いてありました。

https://cloud.google.com/bigquery/docs/authentication/end-user-installed?hl=ja#bigquery-enduser-installed-packages-nodejs

![manual.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321330/6c5d80f2-35b8-4ce0-7cf6-e0f9a1d4aa95.png)

BigQueryのページですが、ここはサービス間で共通です。