API GatewayのJWT検証にFirebase Authenticationを使う

こんにちは。

今、こんな感じのアーキテクチャでサービスを作ろうとしています(簡略化してます)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256853/366ebcd4-1ef1-e018-10ef-6eabaeee1b71.png)


IDaaSとして過去の資産が使えるFirebase Authenticationを利用し、認証済みのユーザーのみAPI Gatewayから先のAPIにアクセスできるというアーキテクチャです。

API GatewayのJWT　AuthorizerをGoとかPythonで作ってる記事はありましたが、Node.js & Typescriptでの日本語記事が無かったので、書いておきます。

#### Goの実装例
https://kaminashi-developer.hatenablog.jp/entry/2021/02/24/142735

#### Javascriptの実装例
https://qiita.com/kousaku-maron/items/23d88b26c8a1cd0b1140

# 環境
全部Typescriptでやります。
Lambdaの実行環境は、Node.js16.x

# 処理の流れ
1. クライアント(今回はExpoですがWebでもOK)でFirebase Authenticationでログインする
1. `auth.currentUser.getIdToken()`でJWTをクライアント側で保持しておく
1. クライアントからなんらかのAPIにリクエストするときに、2で保持しているJWTをヘッダーに付与してリクエストする
1. API GatewayはAuthorizer(Lambda)を通して、受け取ったJWTが有効なものかLambdaを呼び出して検証する
1. 有効なJWTであれば、先のAWSサービス(今回はApp Runnerを使いますが割愛します)をCallする。そうでなければ403を返す(本来は401を返すべきですが、API Gatewayのデフォが403?)

# 説明すること、しないこと
この部分を説明します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256853/655fcfd4-0cb0-5a1d-5351-64b6ed10bd9b.png)

API Gatewayから先は今回関係ないので割愛し、Mockを設定してます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256853/29a93fb7-d391-f1dd-062d-2079f8a3388d.png)

また、今回Typescriptで実装するにあたり、AWS SAMを使用しています。つい最近TSがベータ版ながらサポートされたとのことでありがたく使わせていただいてます。

https://dev.classmethod.jp/articles/typescript-native-support-in-the-aws-sam-cli/

また、生のLambda(?)でnpm packageを使うためにはLayerという機能を使わないといけないのですが、SAMであればよしなにやってくれるので、下記のような操作が不要になりDXが向上するのでおすすめです。

https://qiita.com/Annoske/items/611606283ab031753661

今回はSAMの使い方は説明しません、とにかく `firebase-admin`が使えるLambda関数をデプロイできればServelessFrameworkでも、生でもOKです。

SAMについてはこちらが参考になりました。

https://dev.classmethod.jp/articles/re-introducation-2022-aws-sam/

# JWT Authorizer コード例
```typescript
import { APIGatewayAuthorizerResult, APIGatewayAuthorizerEvent } from 'aws-lambda'
import { auth } from 'firebase-admin'
import { cert, initializeApp } from 'firebase-admin/app'

// Lambdaの環境変数(FIREBASE_DATABASE_ENDPOINT)にFirebaseProjectのdatabaseurlを登録しておく。
const firebaseDatabaseEndpoint = process.env.FIREBASE_DATABASE_ENDPOINT

// FirebaseAdminSDKを使うための秘密鍵ファイルを指定する
// eslint-disable-next-line @typescript-eslint/no-var-requires
const serviceAccount = require('./serviceAccount')

initializeApp({
  credential: cert(serviceAccount),
  databaseURL: firebaseDatabaseEndpoint
})

/** ポリシーを生成 */
const generatePolicy = (
  principal: string,
  effect: 'Allow' | 'Deny',
  resource: string
): APIGatewayAuthorizerResult => {
  return {
    principalId: principal,
    policyDocument: {
      Version: '2012-10-17',
      Statement: [
        {
          Action: 'execute-api:Invoke',
          Effect: effect,
          Resource: resource,
        },
      ],
    },
  }
}

export const lambdaHandler = async (event: APIGatewayAuthorizerEvent): Promise<APIGatewayAuthorizerResult> => {
  if (event.type !== 'TOKEN') {
    console.log(`expected authorization type is TOKEN, got ${event.type}`)
    return generatePolicy(null, 'Deny', event.methodArn)
  }

  // JWTを取得
  const token = event.authorizationToken

  if (!token) {
    console.log('authorization token must not bet null')
    return generatePolicy(null, 'Deny', event.methodArn)
  }

  // JWTを検証
  try {
    const result = await auth().verifyIdToken(token)
    return result.uid
      ? generatePolicy(result.uid, 'Allow', event.methodArn)
      : generatePolicy(null, 'Deny', event.methodArn)
  } catch (error) {
    console.log('authorization token is invalid')
    return generatePolicy(null, 'Deny', event.methodArn)
  }

}

```

# 作成したJWT Authorizerをアタッチする
SAMでできるっぽいですが、まだ勉強不足のためGUIでアタッチしました。

## オーソライザーを作成
API Gatewayで、オーソライザーを作成します。
先ほど作った関数を指定します。トークンのソースはスタンダードに`Authorization`としました。クライアントからのリクエストはここで指定したヘッダーに付与します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256853/55dcbcdb-32c7-a8d6-6e1a-8e54a01ee3fb.png)

対象のエンドポイントに先ほど作成したオーソライザーを指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256853/3df4ae91-b712-8f31-9bee-067572e6894d.png)

デプロイするのを忘れずに！

デプロイしたAPIにリクエストして確認。
> 正しいJWT
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256853/cc834443-07f6-7937-d5ae-bb74571c0a51.png)
> 誤ったJWT
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256853/2e9e6f13-88f5-6648-08f6-cda1056d6ed1.png)

ヨシ！

# あとは
AWS Serviceに繋ぐ前にLambdaの関数を挟んでいるので、JWTの検証だけでなく、ユーザーごとのリクエストのログを取ったりできそうですね。

# 参考
> https://dev.classmethod.jp/articles/lambda-authorizer-verify-token-from-auth0/
> https://blog.devgenius.io/secure-your-apis-with-firebase-aws-api-gateway-199b1eda1da3
> https://blog.ymcloud.jp/entry/firebase-auth-aws-integration/