Koa+TypeScript で Google Sign-In のバックエンド認証をやってみた
Koa+TypeScript で Google Sign-In のバックエンド認証をやってみました。
はじめに
Google Sign-In については、以下の記事を参照してください:
今回は、クライアント側でGoogle認証が通った後、バックエンドのAPIをたたく時に、バックエンド側で認証をチェックする部分の話です。
クライアント側の実装については、以下の記事を参照してください:
前提として、Koaアプリのベースは出来上がっているものとします。
まだ出来ていない場合は、以下の記事を参照してください:
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
の型をマッチさせるのは、かなり試行錯誤したのですが、うまくいきませんでした。型定義ファイルは生成されているんですが。。。
なお、型定義を書く時には、コンパイルした結果を見ると、どのように参照されているかわかりやすいと思います。