Lean Baseball

No Engineering, No Baseball.

App Engine上にあるAPIの認証をCloud Endpoints + Firebaseでいい感じにしてみた

つい最近, 学生に見られてしまった42歳*1(外資コンサル企業マネージャー)です.

今, プライベート(いわゆる個人開発)でWebサービスを開発しようとしているのですが,

  • Webサービスの性質上, 何らかの認証機能が必須
  • ↑が必須なのはわかるけど, 自分で開発(コードを書く)のは正直つらい(かつ自前で書くほどの要件でも無さそう)
  • クラウド(今回はGoogle Cloud)で運用するのは決まってるので, 既存のクラウドサービスでどうにかならないか?

といういい感じにサボる効率的なソリューションを探した結果,

ワイ「どうやらCloud EndpointsとFirebaseでどうにかなるらしい🤔」

とわかり, 実際にどうにかなりそうなものができたので, 自分が忘れないように(&似たような状況の方のお役にたてるように)メモとして残したいと思います.

TL;DR

Google Cloudでサクッと認証をやりたいときは「Identity-Aware Proxy (IAP) 」「Cloud Endpoints」「Firebase Authentication」を使うとひとまずいい感じにできます

少なくとも, メールとかの一般的な認証は.*2

おしながき

やったこと

今回やったこと(やりたかったこと)を言語化すると,

  • BackendのAPI(RESTful API)はApp Engine(Standard)にある. これはすでに動いている.
  • 認証の方式は, 「メールアドレスとパスワード」という非常にシンプルな方式.
  • 何かしらの認証をして, Token(有効期限付き)を取得&APIコールはそれを使い回す.
  • APIは認証する所としないところがある.
    • ルート / については認証不要とする. 外形監視のHealth Checkのパスとして使うため(認証でガードしてしまっては困ってしまう).
    • 上記以外のパスはすべて認証済みのTokenを必須とする.

この条件のもと, いろいろ考えて試行錯誤と実験をした結果, 以下のような構成(概念です, 後で触れますが実際の構成は少し変わります)でまとまりました.

f:id:shinyorke:20211219112445p:plain
やったこと(概念)

  1. User(実態としてはフロントエンドのアプリケーションを想定)はFirebase Authenticationを使って認証を行い, Tokenを取得する
  2. Userは, 1.で得たトークンをAPI Requestに埋め込んで必要なAPIリソースにアクセスする
  3. API ProxyはTokenをチェック(Firebase Authenticationとの突合)を行い, 通して良いアクセスかどうかをチェックする.
  4. ちゃんとしたTokenならApp Engineに通す, NGなら401 Errorで弾く

若干のハマりどころはありましたが, この構成でどうにか動きました.

実際の構成

この先は実際どうやって作ったか?という話です.

実際のアーキテクチャはこんな感じになります.

f:id:shinyorke:20211219112312p:plain
アーキテクチャ

登場するサービスは,

  • 認証基盤としてのFirebase Authentication. これは先程の絵と同じ.
  • API Proxy(もしくはAPI Gateway)としてのExtensible Service Proxy V2(ESPv2). Cloud Endpointsを使って構成する. ホストするのはCloud Run.
    • 先程の概念図では分けて書いてました(理解を整理するため).
    • 実際動くものとしては, 「Cloud EndpointsのESPv2イメージを元にコンテナとして動かします, 環境はCloud Runでね」って感じ.
    • ホスト環境はCloud Run, 上モノはCloud Endpointsという雑な理解で大丈夫です.
  • API本体はApp Engineでホスト. これも先程の絵と同じ.

で, 実際の作業手順としては,

  1. App Engineのへのアクセスを「Identity-Aware Proxy (IAP) 」で保護. 上の図で言う所の「4. 優勝」とある線の部分のアクセスを制御.
  2. 認証用のESPv2を構築し, App Engineまでのパスを通す(この時点ではAPIアクセスに認証を入れない)
  3. Firebase Authenticationでユーザー作成, ESPv2の認証基盤として紐付けして, APIアクセス認証するようにする

という感じになります.

IAPでApp Engineのアクセスを保護

普通にデプロイしたApp EngineのアプリケーションはURLを知ってる人なら全世界に見える状態です.

このままだと外から認証を掛ける意味がないので, Identity-Aware Proxy (IAP) でアクセスを保護します.

cloud.google.com

これがちゃんと終わると, 許可したユーザーのみのアクセスとなります(ブラウザのシークレットモードなどで確認すると良いでしょう).

なお, 個人的なオススメとしては, 自分のGmailアカウントや組織などを予め表示許可を与える事だったりします.

デバッグしたり, なにかの確認作業をしたりという所で「自分だけ見たい」って事はあると思うので.

認証用のESPv2をCloud Runにデプロイ

App EngineのIAP保護が終わったら, API Proxyの役割を果たすESPv2を構築してCloud Runにデプロイします.

これは, Google Cloudにまとまってる手順をそのままやるだけです.

cloud.google.com

ちょっとややこしい手順ですが, 基本そのまま真似をするといけます.

この時点でのわたしがやったopenapi-appengine.yamlの中身はこんな感じになりました.

swagger: "2.0"
info:
  title: Cloud Endpoints + App Engine
  description: Sample API on Cloud Endpoints with an App Engine backend
  version: 1.0.0
host: ${Cloud RunのURL}
schemes:
  - https
produces:
  - application/json
x-google-backend:
  address: ${App EngineのURL}
  jwt_audience: ${App EngineのIAPクライアントID}
  protocol: h2
paths:
  /:
    get:
      summary: Health check
      operationId: index
      responses:
        "200":
          description: A successful response
          schema:
            type: string
  /predict:
    post:
      summary: predict a home run
      operationId: predict
      responses:
        "200":
          description: A successful response
          schema:
            type: string

ポイントとしては,

  • yamlの記述内容はApp Engineにあるアプリの定義 なのか, API Proxyの定義そのもの なのかを分けて考えると理解・記述がしやすい
  • 公式の説明では端折ってますが, 「使いたいAPIのパス」はこの時点でpathsに定義が必要
    • 公式のやつをそのままやると, / しかpath通らない
    • なぜなら, 「Proxyの設定」なので, ここで使うべきパスを許可する必要があるため

招待はOpen API(swagger)なので, 予め定義を用意すると楽かもしれないです.*3

ここまでうまくいくと,

  • API ProxyのURL(Cloud RunのURL)経由でApp Engineのアプリにアクセスできる
  • どのパスも, 認証いらずで通過する
  • App EngineのURLはIAPで許可した者のみ通す

といった状態になります.

Firebase Authenticationを設定

この手順は以下のサイトが参考になりました.

blog.sora-riku.com

ここまで行けたら優勝まであと一歩です.

Firebase Authenticationを有効化してユーザーを作ります(これはGUIでポチポチしたら終わる).

認証とアクセスはフロントエンドのアプリからやりたいところですが...ひとまずAPIの検証がしたい!感じであればcurlコマンドでどうにかします.

curl 'https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=${自分のAPI Key}' \
-H 'Content-Type: application/json' \
--data-binary '{"email":"${your email address}","password":"${your password}","returnSecureToken":true}'

これでレスポンスに含まれるidTokenを控えておきます.

先ほどCloud RunにデプロイしたAPI Proxyに「Firebase Authenticationを認証基盤にしていい感じにしてくれ」という設定をします.

swagger: "2.0"
info:
  title: Cloud Endpoints + App Engine
  description: Sample API on Cloud Endpoints with an App Engine backend
  version: 1.0.0
host: ${Cloud RunのURL}
schemes:
  - https
produces:
  - application/json
x-google-backend:
  address: ${App EngineのURL}
  jwt_audience: ${App EngineのIAPクライアントID}
  protocol: h2
securityDefinitions:
  firebase:
    authorizationUrl: ""
    flow: "implicit"
    type: "oauth2"
    x-google-issuer: https://securetoken.google.com/${自分のプロジェクトのID}
    x-google-jwks_uri: https://www.googleapis.com/service_accounts/v1/metadata/x509/securetoken@system.gserviceaccount.com
    x-google-audiences: ${自分のプロジェクトのID}
paths:
  /:
    get:
      summary: Health check
      operationId: index
      responses:
        "200":
          description: A successful response
          schema:
            type: string
  /predict:
    post:
      summary: predict a home run
      operationId: predict
      security:  # 認証が必要やでっていう設定
        - firebase: []
      responses:
        "200":
          description: A successful response
          schema:
            type: string

最初にデプロイしたyamlとの差分は,

  • securityDefinitions で「認証基盤はFirebase Authenticationだよ」と定義
  • / 以外のパス, ここでは/predict というパスに「認証が必要だよ」と定義

です, ここまでできたらCloud Runに再デプロイします.

こんな感じで動作したら成功です.

$ # indexはTokenいらない
$ curl --location --request GET 'https://example.a.run.app'
{"status":"ok"}
$
$ # predictでToken無しだと怒られる
$ curl --location --request POST 'https://example.a.run.app/predict' \
--header 'Content-Type: application/json' \
--data-raw '{
    "throw": "R",
    "pitch_speed_mph": 100,
    "pitch_type": "FF"
}'
{"message":"Jwt is missing","code":401}
$ # Tokenをつけると動くよ
$ curl --location --request POST 'https://example.a.run.app/predict' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ${控えておいたidToken}' \
--data-raw '{
    "throw": "R",
    "pitch_speed_mph": 100,
    "pitch_type": "FF"
}'
{"result":"HOME_RUN"}

これでOAuth2ベースのトークン認証がほぼ開発ゼロでできるようになりました, おめでとうございます🙌🏻

こんなときどうする🤔

というわけで, 「App Engine上のAPI認証をCloud Endpoints + Firebaseでいい感じにできた」訳ですが, 「こんな時どうする」編を少々.

ホストをApp Engineじゃないサービスに

結論から言っちゃうと, IAPで保護をかけられるサービスならなんでもOKです.

現実的な線で言うと, 今回紹介したApp Engine以外だと,

  • Cloud Run
  • GKE
  • GCE

あたりがよく使うんじゃないかなと思います.

ESPv2を立てる時のx-google-backend の設定をいじるとだいたい行けると思います.

「認証」じゃなくて「認可」をやりたい

これは自分の認識として,

あくまでL7(アプリケーションのレイヤー)での認証を実現するものであり, 「特定の機能を特定のユーザーに許可する」ような認可はできない

かなと思っています.

IAPみたいにIAMユーザーで絞る感じならある程度コントロールできるかと思います*4が, API Proxyはあくまでも「土管」なので, 特定のユーザーに対する処理の許可はアプリケーション上で自分で作れや!が正解かなと.

もし認識違ってたらツッコミください.

結び

というわけで, 「App Engine上のAPI認証をCloud Endpoints + Firebaseでいい感じにしてみた」という話を紹介しました.

このエントリーは自分の作業メモ・考え整理として書きましたが, おそらく仕事でも役たちそうだなと思いました.

認証周りは自分で作ると結構きっつい(個人的には苦手意識もめっちゃある)ので, こういう機構は積極的に使おうと思います.

最近ちゃんと触れてないですがこれってきっとAWSでも似たような事できますよね?もしご存知な方がいましたら紹介してもらえると嬉しいです.

最後までお読み頂きありがとうございました.

参考文献

cloud.google.com

blog.sora-riku.com

qiita.com

*1:正直な話, いい感じに白髪も入ってるのだが何故😇

*2:一般的じゃない認証はカスタムで作る必要あります&後ほどふれますが, 「認証」であり「認可」では無いのでそこも注意が必要

*3:swaggerの定義など. PythonのFast APIとかで開発してる人は楽に出力できるんじゃないかなと思います, スキーマ含めて.

*4:上記のApp EngineのIAP認証周りを参照. IAM単位での許可なので「認可」という見方もできますが, 「特定にパス・機能云々」は実現できないので注意が必要です.