Koa+TypeScript で Google Sign-In のバックエンド認証をやってみた

Koa+TypeScript で Google Sign-In のバックエンド認証をやってみました。

はじめに

Google Sign-In については、以下の記事を参照してください:

itexplorer.hateblo.jp

今回は、クライアント側でGoogle認証が通った後、バックエンドのAPIをたたく時に、バックエンド側で認証をチェックする部分の話です。

クライアント側の実装については、以下の記事を参照してください:

itexplorer.hateblo.jp

前提として、Koaアプリのベースは出来上がっているものとします。

まだ出来ていない場合は、以下の記事を参照してください:

itexplorer.hateblo.jp

Google Auth Library のインストール

まず、Google Auth Library をインストールします:

$ npm i -S google-auth-library

これには、TypeScriptの型定義ファイルが付いておらず、DefinitelyTyped にもあがっていないため、型定義ファイルを自分で用意する必要があります。

型定義ファイルについては、かなり試行錯誤したのですが、うまい方法が見つからず(後ろの「おまけ」を参照)、とりあえず src/types/google-auth-library/index.d.ts というファイルを作成し、以下の内容を入れました:

declare module "google-auth-library" {}

これは単にモジュールを解決するだけです。

認証処理の実装

クライアントから受け取ったIDトークンの認証処理は、以下のような middleware にしてみました:

import * as Koa from 'koa';
import * as GoogleAuth from 'google-auth-library';

const CLIENT_ID = '{Client ID}';

interface INext {
  (): Promise<any>;
}

class Authentication {

  private _ctx: Koa.Context;

  do = async (ctx: Koa.Context, next: INext) => {
    this._ctx = ctx;

    // IDトークンを取得
    const authHeader = this._ctx.req.headers['authorization'];
    console.log('req.headers.authorization: ', authHeader);
    const { isSuccess, idToken } = this._getIdToken(authHeader)
    if (!isSuccess) {
      return;
    }

    // IDトークンを検証
    let login: any;
    try {
      login = await this._verifyIdToken(idToken);
    } catch (e) {
      console.log('error in _verifyIdToken(): ', e.message)
      this._setResponseAsUnauthorized('Token invalid');
      return;
    }

    // ユーザー情報を取得
    const payload = login.getPayload();
    console.log('payload: ', payload)
    const userid = payload['sub'];
    console.log(`userid: ${userid}`);

    await next();
  };

  private _setResponseAsUnauthorized = (msg: string) => {
    this._ctx.body = { error: msg };
    this._ctx.status = 401;
  };

  private _getIdToken = (authHeader: string | string[]) => {
    const failedResult = { isSuccess: false, idToken: '' };

    if (typeof authHeader !== 'string') {
      this._setResponseAsUnauthorized('Token invalid');
      return failedResult;
    }

    if (!authHeader) {
      this._setResponseAsUnauthorized('Token required');
      return failedResult;
    }

    const authHeaderString: string = authHeader;
    const elems = authHeader.split(' ');
    if (elems.length !== 2) {
      this._setResponseAsUnauthorized('Token invalid');
      return failedResult;
    }

    return { isSuccess: true, idToken: elems[1] };
  };

  private _verifyIdToken = (idToken: string) => {
    const gAuth = new (GoogleAuth as any)();
    const client = new gAuth.OAuth2(CLIENT_ID, '', '');
    return new Promise((resolve, reject) => {
      client.verifyIdToken(
        idToken,
        [CLIENT_ID],
        (err: any, login: any) => {
          if (err) {
            reject(err);
            return;
          }
          resolve(login);
        });
    });
  };
}

const authentication = new Authentication();
export default authentication;

do() が認証処理の本体、_getIdToken() で Authorization ヘッダーからIDトークンを取得、_verifyIdToken() でIDトークンをGoogleのライブラリーで検証しています。

最後の _verifyIdToken() についてですが、GoogleAuth が型定義できなかったため、any で逃げています(残念)。

あと、client.verifyIdToken() はコールバック式なので、Promiseに直し、do() 側で await できるようにしています。

呼び出し側では、以下のようにして組み込めます:

import * as Koa from 'koa';
import authentication from './lib/authentication';

const app = new Koa();
app.use(authentication.do);
.....

おまけ:TypeScriptの型定義ファイルを生成

実は、NodeJS用の Google Auth Client はTypeScriptで書かれているんですよね。

でも、リポジトリーには型定義ファイルが含まれていません。

そのため、リポジトリーをクローンしてビルドします:

$ git clone https://github.com/google/google-auth-library-nodejs.git
$ cd google-auth-library-nodejs
$ npm install
$ npm run build

これで types ディレクトリーの中に型定義ファイルが生成されます。

これを、自分のプロジェクトにコピーします:

mkdir -p {project path}/src/types/google-auth-library
cp types/lib {project path}/src/types/google-auth-library/

npm run build で自分のプロジェクトをビルドしてみると、以下のようなエラーが発生します。

error TS2688: Cannot find type definition file for 'request'.
Try `npm install @types/request` if it exists or add a new declaration (.d.ts) file containing `declare module 'request';`

@types/request をインストールしてみます:

$ npm i -D @types/request

これでエラーが消えました。

ただし、この状態で import * from GoogleAuth from 'google-auth-library'; とやっても型定義ファイルを見つけてくれません。

公式ページの「Module Resolution · TypeScript」によると、ファイルを探す対象のディレクトリーと順番が書いてあります。

また、コンパイル時に tsc --traceResolution とやるとログが出るのですが、それを見ると、src/types も探してくれるようです。

そこで、src/types/index.d.ts に、前述のように declare module "google-auth-library" {} と書くと、見つけてくれるようになります。

GoogleAuth の型をマッチさせるのは、かなり試行錯誤したのですが、うまくいきませんでした。型定義ファイルは生成されているんですが。。。

なお、型定義を書く時には、コンパイルした結果を見ると、どのように参照されているかわかりやすいと思います。