こうさくきろく

つくるのたのしい

Google API Gateway 使って認証付き API を実装する

GCPAPI Gateway の機能提供するサービスは Apigee だけだと思っていたところ、API Gateway のサービスがベータで公開されていたので、試します。

やること

GCPAPI Gateway を使って Firebase で認証が必要な API を実装します。

cloud.google.com

サンプルアプリの構成

サンプルでは下記の API が実装されています。

f:id:mukopikmin:20210105223906p:plain

Next.js で実装したフロントエンドアプリは Cloud Run 経由で配信します。 このアプリから Firebase Authentication で認証し、API へのアクセスにはこの認証情報を使用します。

API サーバーも Cloud Run で動作しています。 この API へのアクセスはすべて API Gateway を経由するようにし、認証情報の検証は API Gateway で吸収します。

実装は GitHub で公開しています。

github.com

サンプル API の構成

今回は Go 言語を使って下記 3 つの API を実装します。

  • /unprotected
    • 認証が不要な API
  • /protected
    • 認証が必要な API
  • /mirror
    • 認証が必要で、認証しているユーザーの情報を返す API

/unprotected/protected では下記のレスポンスを返すようにしています。

[
  {
    "id": 1,
    "title": "Sample book 1",
    "description": "This is first sample book"
  },
  {
    "id": 2,
    "title": "Sample book 2",
    "description": "This is second sample book"
  },
  {
    "id": 3,
    "title": "Sample book 3",
    "description": "This is third sample book"
  }
]

/mirror では API Gateway が接続先の API に渡している下記の認証情報をそのまま返します。

{
  "name": "**********",
  "picture": "https://**********",
  "iss": "https://securetoken.google.com/**********",
  "aud": "**********",
  "auth_time": 0000000000,
  "user_id": "**********",
  "sub": "**********",
  "iat": 0000000000,
  "exp": 0000000000,
  "email": "**********@**********",
  "email_verified": true,
  "firebase": {
    "identities": {
      "google.com": ["**********"],
      "email": ["**********@**********"]
    },
    "sign_in_provider": "google.com"
  }
}

ただし、この認証情報は API Gateway が接続先の API にアクセスするときに X-Apigateway-Api-Userinfo ヘッダーに Base64 エンコードされた状態で渡されています。 したがって、API を実装しているアプリケーション側でデコードし、JSON としてパースする必要があります。

API Gateway のデプロイ

API 定義の作成

API Gatewayゲートウェイを作成するためには、Swagger 2.0 で API の定義を作成する必要があります。 試しに、Open API Specification 3.0 のフォーマットで作成したファイルを使ってみましたがアップロードできませんでした。

Firebase Authentication で API へのアクセスを制限するには、Swagger 2.0 の定義に対して、API Gateway の拡張構文を追記します。

cloud.google.com

認証方法を定義します。

securityDefinitions:
  firebase:
    authorizationUrl: ""
    flow: "implicit"
    type: "oauth2"
    ### Replace YOUR-PROJECT-ID with your project ID
    x-google-issuer: "https://securetoken.google.com/YOUR-PROJECT-ID"
    x-google-jwks_uri: "https://www.googleapis.com/service_accounts/v1/metadata/x509/securetoken@system.gserviceaccount.com"
    x-google-audiences: "YOUR-PROJECT-ID"

そして、認証が必要なスコープに対して、security を定義します。 すべての API に対して アクセス制限をするならルート要素として、特定の API のみの場合は path や 特定の HTTP メソッド以下に記述します。

security:
  - firebase: []

API ゲートウェイの作成

作成するゲートウェイに必要な情報を設定しておきます。

PROJECT_ID=REPLACE_WITH_PROJECT
API_ID=sample-api-id
CONFIG_ID=samle-config-id
GATEWAY_ID=samle-gateway-id
GCP_REGION=asia-east1

まず、APIAPI Gateway 内のプロジェクトのような扱いのもの)を作成します。

cloud.google.com

gcloud beta api-gateway apis create $API_ID --project=$PROJECT_ID

次に、作成した API 定義をアップロードして、ゲートウェイの設定を作成します。

cloud.google.com

gcloud beta api-gateway api-configs create $CONFIG_ID \
  --api=$API_ID \
  --openapi-spec=openapi.yaml \
  --project=$PROJECT_ID

そして、ゲートウェイを作成します。

cloud.google.com

gcloud beta api-gateway gateways create $GATEWAY_ID \
  --api=$API_ID \
  --api-config=$CONFIG_ID \
  --location=$GCP_REGION \
  --project=$PROJECT_ID

作成したゲートウェイの情報を確認します。

gcloud beta api-gateway gateways describe $GATEWAY_ID \
  --location=$GCP_REGION \
  --project=$PROJECT_ID

以下のように表示されます。 defaultHostnameAPI Gateway にアクセスするためのエンドポイントのホスト名です。

apiConfig: projects/PROJECT_ID/locations/global/apis/API_ID/configs/CONFIG_ID
createTime: '2021-01-05T14:12:16.021807663Z'
defaultHostname: *********.de.gateway.dev
displayName: GATEWAY_ID
name: projects/PROJECT_ID/locations/GCP_REGION/gateways/GATEWAY_ID
state: ACTIVE
updateTime: '2021-01-05T14:14:00.175372465Z'

試しにアクセスして、レスポンスが変えることを確認します。

curl https://*********.de.gateway.dev

/API として定義していないので 404 が返ります。 (作成したアプリケーションは/でフロントエンドアプリケーションを返すようにしています)

{
  "code": 404,
  "message": "Request `GET /` is not defined by this API."
}

一方、認証が必要な API にアクセスすると 401 を返します。

curl https://*********.de.gateway.dev/protected
{
  "message": "Jwt is missing",
  "code": 401
}

当然ですが、認証が必要ない API は 200 を返します。

curl https://*********.de.gateway.dev/unprotected
[
  {
    "id": 1,
    "title": "Sample book 1",
    "description": "This is first sample book"
  },
  {
    "id": 2,
    "title": "Sample book 2",
    "description": "This is second sample book"
  },
  {
    "id": 3,
    "title": "Sample book 3",
    "description": "This is third sample book"
  }
]

クライアントアプリを使った動作確認

サンプルアプリでは Fireabase Authentication を使った認証と、バックエンドの各 API を呼び出せます。

認証していない状態では、/mirror/protectedにアクセスできません。

f:id:mukopikmin:20210118021400p:plain

一方、認証している状態ではすべての API にアクセスできていることがわかります。

f:id:mukopikmin:20210118021345p:plain

その他の動作確認

カスタムヘッダーを付加する

Swagger 2.0 の拡張できる内容は下記の URL にまとめられています。

cloud.google.com

よくある処理として、API Gateway で CORS ヘッダーを追加したりがあると思いますが、現時点ではできないようです。

前段に Cloud Load Balancing を配置した場合、Cloud Load Balancing 側でヘッダーを付加することはできます。

cloud.google.com

カスタムドメインを使用する

現時点(2021/01/18)ではカスタムドメインは使用できないようです。 ただ、Cloud Load Balancing を前段に置く形であればカスタムドメインを使用できるようです。

cloud.google.com

For the Beta release, custom domain names are not supported on GCP for API Gateway. If you want to customize the domain name for the Beta, you have to create a load balancer to use your custom domain name and then direct requests to the gateway.dev domain of your deployed API.

バックエンド API への API Gateway 以外からのアクセスを拒否する

API 定義から設定を作成するときに、--backend-auth-service-accountをつけることでサービスアカウントを指定できます。

実際に試したわけではありませんが、このサービスアカウントの権限を適切に設定することで、アクセス制御が実現できそうです。

まとめ

GCPAPI Gateway を使ってバックエンド API に認証機能を付加することができました。 API の定義に対して設定を記述することで機能を付加できる点は、個人的にはわかりやすくて好みでした。

ベータ版ということもあって機能は少なかったり、実装されていない機能もあるようですが、Cloud Load Balancing を使うことである程度補完できそうです。