API Gateway カスタムオーソライザーを使って、Firebase で認証する

組み合わせただけの話なのですが、個人用メモ。

ちょっと前に「AWSによるサーバーレスアーキテクチャ」を読んだり、手元で色々試してました。本では、認証に Auth0 というサービスを使っているんですが、本が書かれた頃から Auth0 の仕様が大きく変わっています。サンプルを直すのがつらそうだったのと、その章で説明したい内容はあくまでカスタムオーソライザーの設定で、正直、認証サービスはなんでもよさそうだったので、馴染み深い Firebase Auth を代わりに使ってみました。

AWSによるサーバーレスアーキテクチャ

AWSによるサーバーレスアーキテクチャ

Firebase の ID トークンの作成と確認

Firebase はクライアントサイドで認証が完結しますが、バックエンドの API サーバーでログインしているユーザーを知りたい場合があります。Firebase ではクライアントで ID トークンの発行をおこない、サーバーでこれを検証することで実現できます。手順は以下のようなかんじです。

  1. ログイン後に ID トークンを作成する(クライアント)
  2. API リクエストにトークンを付与する(クライアント)
  3. トークンを検証してユーザーの情報を取得する(サーバー)

各手順で使う Firebase の APIID トークンを確認する  |  Firebase の説明がわかりやすいです。1, 2 を簡単にコードで示すと以下のようになります。

firebase.auth().currentUser.getIdToken(true)
  .then((idToken) => {
    return fetch('https://XXX.amazonaws.com/dev/get-profile', {
      mode: 'cors',
      headers: {
        'Authorization': 'Bearer ' + idToken,
      }
    });
  }).then((response) => {
    return response.text();
  }).then((body) => {
     console.log(body);
  }).catch((error) => {
     console.error(error);
  });

3 は次で示します。

カスタムオーソライザーに設定する Lambda のコード

トークンの検証とポリシーの生成をおこなう Lambda 関数を用意します。あとはこの Lambda 関数を API Gateway のリクエストの認証に設定すれば終わりです。

const admin = require('firebase-admin');
const serviceAccount = require('./XXX.json'); // Firebase の管理画面からインストールできる鍵ファイル

admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
  databaseURL: 'https://XXX.firebaseio.com'
});

const generatePolicy = (principalId, effect, resource) => {
  const 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;
};

exports.handler = function(event, context, callback){
  if (!event.authorizationToken) {
    callback('Could not find authToken');
    return;
  }
  const token = event.authorizationToken.split(' ')[1];
  // ID トークンの検証
  admin.auth().verifyIdToken(token)
    .then((decodedToken) => {
      const policy = generatePolicy('user', 'Allow', event.methodArn);
      callback(null, policy);
    }).catch((error) => {
      console.log('Failed idToken verification: ', error);
      callback('Authorization Failed');
    });
};

つらかったこと、いまいちなこと

あんまり本質じゃないですが、CORS まわりでけっこうハマりました。

  • カスタムオーソライザーでポリシーの生成にしくじっても(実行時エラーにはならないが、ポリシーの内容に問題がある場合)、API レスポンスが正常(200)で返ってくる
    • エンドポイントに紐付いた Lambda 関数は実行されない
    • Lambda 関数が実行できなかったらエラーを返すか、CloudWatch Logs にエラーを出力してほしい
    • (自分が気づいていないだけでどこかにエラーが出ているかも)
  • 「Lambda プロキシ統合の使用」を使うと、API Gateway で「CORS の有効化」をしていても、CORS 関係のヘッダーが付与されない。Lambda 関数でレスポンスを返す際に明示的に指定する必要がある
  • CloudWatch Logs が見づらいのと、複数の Lambda 関数で処理を実現している場合のデバッグで、各 Lambda 関数のログを調べるのが大変
    • (追記) これは S3 にログを集約して Athena で分析することになりそう