こんにちは。
今、こんな感じのアーキテクチャでサービスを作ろうとしています(簡略化してます)
IDaaSとして過去の資産が使えるFirebase Authenticationを利用し、認証済みのユーザーのみAPI Gatewayから先のAPIにアクセスできるというアーキテクチャです。
API GatewayのJWT AuthorizerをGoとかPythonで作ってる記事はありましたが、Node.js & Typescriptでの日本語記事が無かったので、書いておきます。
Goの実装例
Javascriptの実装例
環境
全部Typescriptでやります。
Lambdaの実行環境は、Node.js16.x
処理の流れ
- クライアント(今回はExpoですがWebでもOK)でFirebase Authenticationでログインする
-
auth.currentUser.getIdToken()
でJWTをクライアント側で保持しておく - クライアントからなんらかのAPIにリクエストするときに、2で保持しているJWTをヘッダーに付与してリクエストする
- API GatewayはAuthorizer(Lambda)を通して、受け取ったJWTが有効なものかLambdaを呼び出して検証する
- 有効なJWTであれば、先のAWSサービス(今回はApp Runnerを使いますが割愛します)をCallする。そうでなければ403を返す(本来は401を返すべきですが、API Gatewayのデフォが403?)
説明すること、しないこと
API Gatewayから先は今回関係ないので割愛し、Mockを設定してます。
また、今回Typescriptで実装するにあたり、AWS SAMを使用しています。つい最近TSがベータ版ながらサポートされたとのことでありがたく使わせていただいてます。
また、生のLambda(?)でnpm packageを使うためにはLayerという機能を使わないといけないのですが、SAMであればよしなにやってくれるので、下記のような操作が不要になりDXが向上するのでおすすめです。
今回はSAMの使い方は説明しません、とにかく firebase-admin
が使えるLambda関数をデプロイできればServelessFrameworkでも、生でもOKです。
SAMについてはこちらが参考になりました。
JWT Authorizer コード例
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
としました。クライアントからのリクエストはここで指定したヘッダーに付与します。
対象のエンドポイントに先ほど作成したオーソライザーを指定します。
デプロイするのを忘れずに!
デプロイしたAPIにリクエストして確認。
ヨシ!
あとは
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/