Firebase Authenticationを使ってAPI Gatewayに認証機能を付与する

# 概要
Firebase Authenticationに登録されているユーザーだけがAPIを使えるようにするため、
エンドポイントであるAPI Gatewayにオーソライザーを設定します。

<img width="500" src="https://user-images.githubusercontent.com/84354821/163387752-414532dd-a501-47a6-b4cd-4fdf93b026e3.png">

処理の流れは以下の通りです。
1. Firebase Authentication SDK を使用し IDトークン を取得する
2. 取得したIDトークンをヘッダーに付与し、APIGatewayのエンドポイントへリクエストを送る
2. API Gatewayの オーソライザー が Firebase Admin SDK を使用しIDトークンを検証する
2. 認証できた場合、Lambda関数を実行する


# 対象読者
Firebase Authenticationの機能が分かる方
API GatewayとLambdaでAPIを作成する方法が分かる方


# APIの作成

テスト用のシンプルなAPIをAPI GatewayとLambdaで作成しておきます。

例として、Lambda関数は以下の自動生成されるコードをそのまま使用します。
```js:index.js
exports.handler = async (event) => {
    // TODO implement
    const response = {
        statusCode: 200,
        body: JSON.stringify('Hello from Lambda!'),
    };
    return response;
};
```
API Gatewayを新規に作成した後、`GET`メソッドを付与しLambda関数と紐付け、APIを作成します。

# オーソライザーの作成

作成したAPI Gatewayに付与するオーソライザーを実装します。
* Firebaseプロジェクトの秘密鍵を取得
* Lambda Layerを作成
* 認証用のLambda関数の実装
* オーソライザーの設定

の順で解説していきます。


## Firebaseプロジェクトの秘密鍵を取得

Firebaseプロジェクトを開き、サイドメニューの歯車アイコンから[プロジェクトの設定]を開きます。

[サービスアカウント]を開き、[新しい秘密鍵の生成]をクリックして秘密鍵(jsonファイル)をダウンロードします。
<img width="500" src="https://user-images.githubusercontent.com/84354821/163401852-9cfee356-c387-4f10-bc12-e96ab296fb01.png">
Firebaseプロジェクトの秘密鍵が取得できました。


## Lambda Layerを作成

Lambda関数内で`Firebase Admin SDK`を使用するためにLayerを作成していきます。

PC上に**firebase_admin**フォルダを作成します。
```
$ mkdir firebase_admin
```
**firebase_admin**フォルダに移動し`firebase-admin`をインストールします。
```
$ cd firebase_admin
$ npm install firebase-admin
```
**firebase_admin**フォルダを**firebase_admin.zip**という名前でzip化します。
```
$ cd ..
$ zip -r firbase_admin.zip firebase_admin
```

AWSマネジメントコンソール画面でLambdaのレイヤーを選択します。
新規Layerをランタイム Node.js 14.xで、**firebase_admin.zip**をS3経由でアップロードし作成します。


## 認証用のLambda関数を作成

新規Lambda関数をランタイム Node.js 14.xで作成します。


### レイヤーを追加

[Layers]を選択し、[レイヤーの追加]から先ほど作成したレイヤーを追加します。
<img width="600" alt="スクリーンショット 2022-04-15 16 07 38" src="https://user-images.githubusercontent.com/84354821/163537145-0b66bd1a-b632-454b-b35c-2a589b5cad2d.png">
<img width="600" alt="スクリーンショット 2022-04-15 16 12 45" src="https://user-images.githubusercontent.com/84354821/163537687-12834e41-ef83-438f-8f1d-bf9bcbcbe5af.png">


### 秘密鍵を追加

Lambda関数のルートディレクトリ直下に、
Firebaseプロジェクトの秘密鍵のjsonファイルを`admin.json`としてアップロードします。
<img width="300" alt="スクリーンショット 2022-04-15 16 27 44" src="https://user-images.githubusercontent.com/84354821/163539346-ab33b59f-7a5b-499c-ab1d-54f9bfea11e4.png">


### Lambda関数の実装


Lambda関数本体のコードは以下の通りです。
```javascript:index.js
//SDKを初期化
const admin = require('firebase-admin');

const serviceAccount = require('./admin.json');

admin.initializeApp({
    credential: admin.credential.cert(serviceAccount)
})

//IDトークンの検証
exports.handler = async function(event, context, callback) {
    const uid = await admin.auth().verifyIdToken(event.authorizationToken)
        .then((decodedToken) => {
            return decodedToken.uid;
        })
        .catch((error) => {
            console.log(error);
            return null;
        })

    if (uid) {
        callback(null, generatePolicy('user', 'Allow', event.methodArn));
    } else {
        callback(null, generatePolicy('user', 'Deny', event.methodArn));
    } 
};

//IAMポリシーを生成する関数を定義
var generatePolicy = function(principalId, effect, resource) {
    var authResponse = {};

    authResponse.principalId = principalId;
    if (effect && resource) {
        var policyDocument = {};
        policyDocument.Version = '2012-10-17'; 
        policyDocument.Statement = [];
        var statementOne = {};
        statementOne.Action = 'execute-api:Invoke'; 
        statementOne.Effect = effect;
        statementOne.Resource = resource;
        policyDocument.Statement[0] = statementOne;
        authResponse.policyDocument = policyDocument;
    }

    return authResponse;
}
```
以下、このコードについてパートごとに解説します。

#### SDKを初期化

`serviceAccount`には`admin.json`を読み込みます。
```javascript
const admin = require('firebase-admin');

const serviceAccount = require('./admin.json');

admin.initializeApp({
    credential: admin.credential.cert(serviceAccount)
})
```


#### IAMポリシーを生成する関数を定義

以下の関数で**IAMポリシー**を生成しAPI Gatewayに返却します。

このIAMポリシーの`effect`が`'Allow'`の場合のみ、APIのLambda関数の実行を許可します。

`principalId`は便宜的なユーザーの識別値で、`methodArn`は、受信するメソッドリクエストの ARN です。

```javascript
var generatePolicy = function(principalId, effect, resource) {
    var authResponse = {};

    authResponse.principalId = principalId;
    if (effect && resource) {
        var policyDocument = {};
        policyDocument.Version = '2012-10-17'; 
        policyDocument.Statement = [];
        var statementOne = {};
        statementOne.Action = 'execute-api:Invoke'; 
        statementOne.Effect = effect;
        statementOne.Resource = resource;
        policyDocument.Statement[0] = statementOne;
        authResponse.policyDocument = policyDocument;
    }

    return authResponse;
}
```


#### IDトークンの検証

`firebase-admin`の`verifyIdToken`関数で**IDトークン**の検証を行います。
検証に成功した場合は、アカウント毎に設定されている**uid**を取得できます。

**uid**がある場合はLambda関数の実行を許可する**IAMポリシー**を、
ない場合は実行を拒否する**IAMポリシー**を生成し返却します。

```javascript
exports.handler = async function(event, context, callback) {
    const uid = await admin.auth().verifyIdToken(event.authorizationToken)
        .then((decodedToken) => {
            return decodedToken.uid;
        })
        .catch((error) => {
            console.log(error);
            return null;
        })

    if (uid) {
        callback(null, generatePolicy('user', 'Allow', event.methodArn));
    } else {
        callback(null, generatePolicy('user', 'Deny', event.methodArn));
    } 
};
```


## オーソライザーを設定

作成した認証用のLambda関数をAPI Gatewayのオーソライザーに設定します。


### オーソライザーの作成

認証を適用したいAPI Gatewayを選択後、
[オーソライザー] →[新しいオーソライザーの作成]を選択し、 以下の通り作成します。
<img width="400" alt="スクリーンショット 2022-04-15 18 16 03" src="https://user-images.githubusercontent.com/84354821/163553136-28494190-30e9-4906-a53b-fdc0c9414866.png">

* 名前：任意
* タイプ：Lambda
* Lambda関数：作成した認証用のLambda関数
* Lambda呼び出しロール：空白のまま
* Lambdaイベントペイロード：トークン
* トークンのソース：Authorization
* トークンの検証：空白のまま

トークンのソースは`Authorization`とすることで、
後で設定するCORSを有効化した際にデフォルトで許可されるヘッダーになります。（他の値だと手動で設定する必要があります。）

認可のキャッシュを有効にすると`methodArn`もキャッシュされるため、
TTL内に別のリソースでオーソライザーを呼び出す場合は食い違いによるエラーが発生します。
そのためオーソライザーを使い回したい場合は`methodArn`をワイルドカードで指定するか、認可のキャッシュを無効にします。

[【AWS】API Gateway Lambdaオーソライザー「User is not authorized to access this resource」エラーの原因と対応](https://qiita.com/hassoubeat/items/0add09325fbe664e263e)


### メソッドリクエストへオーソライザーを紐付け

[リソース]→[/ の GET]→[メソッドリクエスト]を選択します。

[設定]の[認可]のプルダウンから作成したオーソライザーを選択し、横のチェックマークをクリックします。
<img width="600" src="https://user-images.githubusercontent.com/84354821/163558284-a5ec41ad-5936-4870-9a82-2034a9d7e9ac.png">


### APIのデプロイ

[アクション]から[CORSの有効化]を選択すると設定画面が表示されるため、
そのまま[CORSを有効にして既存のCORSを置換]をクリックします。
<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2050533/fcbce9ca-1897-d5b1-c421-fdbbe74e62c3.png">

[アクション]→[APIのデプロイ]から、APIをデプロイします。
<img width="300px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2050533/73218fb1-89df-7b99-59bc-43626007b650.png">


# リクエストの送信

クライアント側で以下の例のような処理を適宜実装してください。
`'Hello from Lambda!'`がコンソールに出力されれば成功です。

Firebase認証を行い、IDトークンを取得する情報は多くあるため詳細は省きます。
```javascript:index.js
firebase.auth().onAuthStateChanged(async user => {
    if (user) {
        const authToken = await user.getIdToken()

        const headers = {
            'Authorization': authToken
        }

        //API Gatewayのエンドポイントを設定
        const url = "https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/"

        response = axios.get(url, {headers: headers})
        console.log(JSON.parse(response.data.body))
    }
})
```


# 最後に

Firebase Authenticationを使ってログイン・認証処理を実装している場合は、
それを利用し簡単にAPI Gatewayに認証機能をつけることができました。

１つオーソライザーを作れば他のリソースやメソッドでも使い回せるので便利ですね。

それでは最後までご覧いただきありがとうございました。 コードの不備やご意見等あればコメントいただけますと幸いです。