1
Help us understand the problem. What are the problem?

posted at

updated at

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

概要

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

処理の流れは以下の通りです。

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

対象読者

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

APIの作成

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

例として、Lambda関数は以下の自動生成されるコードをそのまま使用します。

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ファイル)をダウンロードします。

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]を選択し、[レイヤーの追加]から先ほど作成したレイヤーを追加します。
スクリーンショット 2022-04-15 16 07 38
スクリーンショット 2022-04-15 16 12 45

秘密鍵を追加

Lambda関数のルートディレクトリ直下に、
Firebaseプロジェクトの秘密鍵のjsonファイルをadmin.jsonとしてアップロードします。
スクリーンショット 2022-04-15 16 27 44

Lambda関数の実装

Lambda関数本体のコードは以下の通りです。

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を読み込みます。

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 です。

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-adminverifyIdToken関数でIDトークンの検証を行います。
検証に成功した場合は、アカウント毎に設定されているuidを取得できます。

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

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を選択後、
[オーソライザー] →[新しいオーソライザーの作成]を選択し、 以下の通り作成します。
スクリーンショット 2022-04-15 18 16 03

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

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

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

【AWS】API Gateway Lambdaオーソライザー「User is not authorized to access this resource」エラーの原因と対応

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

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

[設定]の[認可]のプルダウンから作成したオーソライザーを選択し、横のチェックマークをクリックします。

APIのデプロイ

[アクション]から[CORSの有効化]を選択すると設定画面が表示されるため、
そのまま[CORSを有効にして既存のCORSを置換]をクリックします。

[アクション]→[APIのデプロイ]から、APIをデプロイします。

リクエストの送信

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

Firebase認証を行い、IDトークンを取得する情報は多くあるため詳細は省きます。

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に認証機能をつけることができました。

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

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

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
1
Help us understand the problem. What are the problem?