第1回、第2回に続き、Cloud Endpoints入門第3回です。
今回は公式ドキュメントに沿って、Google App Engine Standard Environment(GAE) にCloud Endpointsを紐づける方法をご紹介します。
単純に、GAEにデプロイしたAPIに対して認証を設けたいのであれば、アプリ側でMetadataサーバにアクセスして Id Tokenを検証する方法や、IAP(Identity-Aware Proxy)を利用する方法 などで事足りるケースがあります。
もし、流量制御を行いたい、API仕様書とAPIの実装内容の乖離を避けたい、などのニーズがあれば、API GatewayとしてCloud Endpointsを利用するのが効果的です。
ポイントは2つです。
- GAEへ流入する全てのトラフィックがCloud Endpointsを通過するように、GAE側でCloud Endpoints以外を遮断する
- Cloud EndpointsのAPI Gatewayとして、 ESP(Extensible Service Proxy)を利用する
なお、本稿では「エンドユーザ -> Cloud Endpointsの認証設定」については説明しません。
第1回と第2回の記事にて、APIキー、JWTトークンを利用した認証方式を解説しております。他のエンドユーザ認証方式については、公式ドキュメントをご確認ください。
目次
構成
今回はCloud EnspointsのESPをCloud Runにデプロイします。
GCE、GKEや別プラットフォームでもデプロイ可能です。
手順
GAEにAPIをdeployする
適当なAPIをdeployします。
package main
import (
"fmt"
"log"
"net/http"
"os"
)
func main() {
http.HandleFunc("/attack/", indexHandler)
port := os.Getenv("PORT")
if port == "" {
port = "8080"
log.Printf("Defaulting to port %s", port)
}
log.Printf("Listening on port %s", port)
if err := http.ListenAndServe(":"+port, nil); err != nil {
log.Fatal(err)
}
}
func indexHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "ouch")
}
runtime: go112
main.go と app.yaml を同じディレクトリに保存し、以下のコマンドでGAEアプリをdeployします。
gcloud app deploy
IAPをGAEに設定する
IAP(Identity-Aware Proxy)で利用するOAuth Clientを設定します。
IAPを有効にするためには、まずOAuth同意画面を作成する必要があります。
【Google Cloud Console】API&Service -> OAuth consent screen
今回はブラウザ認証を利用しないため、Internalを指定し、Application Nameを入力すればOKです。
次にIAPの保護をenableします。
【Google Cloud Console】Security -> Identity-Aware Proxy
これで、GAEがIAPによって保護された状態となりました。
試しに gcloud app browseで表示されたURLにアクセスすると、Google User認証に遷移し、403となることが確認できます。
Cloud RunにESPコンテナをdeployする
ESPコンテナを作成します。これがAPI Gatewayの役割を担います。
–allow-unauthenticated フラグによってPublicにします。
gcloud run deploy esp-proxy --image="gcr.io/endpoints-release/endpoints-runtime-serverless:1.30.0" --allow-unauthenticated --project=[PROJECT ID]--platform managed --region asia-northeast1
サービスの起動を確認します。
gcloud run services list --platform managed
ESPコンテナのバックエンドにGAEを指定する
OpenAPI仕様書をアップロードすることで、ESPコンテナのバックエンドを設定できます。
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: esp-proxy-xxxxxxxxx-an.a.run.app
schemes:
- https
produces:
- application/json
x-google-backend:
address: https://ca-kubota-iap-test-2.appspot.com
jwt_audience: 290780858146-xxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com
paths:
/attack:
get:
summary: attack server
operationId: attack
responses:
'200':
description: A successful response
schema:
type: string
- host
ESPコンテナのURLです。gcloud run services list –platform managed で確認します。 -
x-google-backend
ESPのバックエンドを指定します。addressはGAEのホスト、jwt_audienceは先ほど作成したOAuthのClient IDです。
アップロードします。
gcloud endpoints services deploy openapi.yaml
ESPコンテナが、アップロードしたESP構成を参照できるように、必要なAPIを有効化します。
gcloud services enable servicecontrol.googleapis.com
gcloud services enable servicemanagement.googleapis.com
gcloud services enable endpoints.googleapis.com
環境変数を指定し、ESPコンテナを更新します。
gcloud run services update esp-proxy --set-env-vars ENDPOINTS_SERVICE_NAME=esp-proxy-xxxxxxxxx-an.a.run.app --platform managed --region asia-northeast1
これでESPのバックエンドにGAEを指定することができました。
ESP -> IAP のアクセス権限を付与する
最後に、ESPコンテナに権限を付与します。
Cloud Runのコンテナを作成すると、Compute Engine APIが有効となり、Compute Engineのデフォルトサービスアカウントが作成されます。
gcloud iam service-accounts list | grep "compute"
このサービスアカウントにIAPへの承認権限を付与します。
gcloud projects add-iam-policy-binding [PROJECT ID]\
--member "serviceAccount:xxxxxxxxxxxxx-compute@developer.gserviceaccount.com" \
--role "roles/iap.httpsResourceAccessor"
検証
ESPを介した場合、GAEへ直接リクエストした場合のレスポンスを比較します。
まず、ESPを介して、GAEへリクエストしてみます。
ESPコンテナのURLは、gcloud run services list –platform managed で確認します。
curl https://esp-proxy-xxxxxxxxx-an.a.run.app/attack/
ouch
予期したレスポンスが帰ってきました。
GAEに直接リクエストしてみます。
ユーザ認証に移行し、アクセスが遮断されることが確認できました。
コラム:ESP -> IAPの認証について
先ほど実行した下記のコマンドにより、ESPのデフォルトサービスアカウントは、IAPへアクセスするためのRoleを取得しました。
gcloud projects add-iam-policy-binding [PROJECT ID]\
--member "serviceAccount:xxxxxxxxxxxxx-compute@developer.gserviceaccount.com" \
--role "roles/iap.httpsResourceAccessor"
ESPは、このデフォルトサービスアカウントのクレデンシャル情報で、JWTトークンを署名し、それを元に、OIDCトークンを発行します。すなわち、ESP -> IAPの認証のトラフィックの裏側では、OAuth2 JWT Bearer Grant Flow が走っています。
(引用:http://farasath.blogspot.com/2015/06/jwt-bearer-grant-oauth2.html)
この図において、
1. Client/Issuer = ESPのデフォルトサービスアカウント
2. Authorization Server=Google トークンエンドポイント
と読み替えることができます。
具体例を示します。
まず、先ほどroles/iap.httpsResourceAccessor を付加した、Cloud Runのデフォルトサービスアカウントのクレデンシャル情報を発行します。
gcloud iam service-accounts keys create credential.json --iam-account xxxxxxxxxxxxx-compute@developer.gserviceaccount.com
次のGoスクリプトは、クレデンシャル情報を用いて、署名付きJWTトークンを発行します。
スクリプト内 claims[“target_audience”] = “[OAuth Client ID]” は、適宜作成したOAuth Client IDを指定してください。
(API&Service -> Credentials -> OAuth 2.0 Client IDs -> 作成したクライアントのClient ID)
package main
import (
"fmt"
jwt "github.com/dgrijalva/jwt-go"
"time"
"io/ioutil"
"encoding/json"
)
func main() {
token, err := getToken("credential.json")
if err != nil {
fmt.Println(err)
} else {
fmt.Println(token)
}
}
func getToken(credfile string) (string, error) {
// parse credential file
var cred Credential
credbyte, err := ioutil.ReadFile(credfile)
if err != nil {
return "", err
}
json.Unmarshal(credbyte, &cred)
// set claims
token := jwt.New(jwt.SigningMethodRS256)
claims := token.Claims.(jwt.MapClaims)
claims["alg"] = "RS256"
claims["kid"] = cred.PrivateKeyID
claims["aud"] = "https://www.googleapis.com/oauth2/v4/token"
claims["iss"] = cred.ClientEmail
claims["sub"] = cred.ClientEmail
claims["iat"] = time.Now().Unix()
claims["exp"] = time.Now().Add(time.Hour * 1).Unix()
claims["target_audience"] = "[OAuth Client ID]"
// set signature
key := cred.PrivateKey
signKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(key))
if err != nil {
return "", err
}
tokenString, _ := token.SignedString(signKey)
return tokenString, nil
}
type Credential struct {
Type string `json:"type"`
ProjectID string `json:"project_id"`
PrivateKeyID string `json:"private_key_id"`
PrivateKey string `json:"private_key"`
ClientEmail string `json:"client_email"`
ClientID string `json:"client_id"`
AuthURI string `json:"auth_uri"`
TokenURI string `json:"token_uri"`
AuthProviderX509CertURL string `json:"auth_provider_x509_cert_url"`
ClientX509CertURL string `json:"client_x509_cert_url"`
}
先ほど発行したcredental.jsonと同一パスでこのスクリプトを実行し、署名付きJWTを生成します。
go run myjwt_oidc.go
続いて、このトークンをrequest bodyに含め、Google のトークンエンドポイントにPOSTします。
- grant_type: urn:ietf:params:oauth:grant-type:jwt-bearer
- assertion: 生成したJWTトークン
すると、Googleのトークンエンドポイントより、OIDCトークンが発行されます。
これをAuthorization: Bearerヘッダに含めてGAEのエンドポイントにリクエストをすると、GAEにdeployしたAPIから予期したレスポンスを得ることができます。
まとめると、ESP -> IAPのトラフィックの裏では、
- ESPのデフォルトサービスアカウントのクレデンシャル情報でJWTトークンを署名
- JWTトークンをhttps://www.googleapis.com/oauth2/v4/token へPOSTし、OIDCトークンを得る
- Authorization: Bearer にOIDCトークンをセットし、エンドユーザからのトラフィックをバックエンド(GAE・IAP)へ流す
という認証が行われています。
なお、GCP上のServer Application(GAE, GCE, Cloud Run etc…) は、Application Default Credentials というルールにのっとり、トークンを取得するためのクレデンシャル情報を探索します。ESPのデフォルトサービスアカウントを利用したくない場合は、GUIまたは、gcloudにて変更するか、ESPコンテナへ、カスタムのサービスアカウントのクレデンシャルファイル(gcloud iam service-accounts keys create…) を配置し、環境変数 GOOGLE_APPLICATION_CREDENTIALS でそのファイルのパスを指定することによって実現できます。
まとめ
App Engine + Cloud Endpointsの設定方法について説明しました。
Cloud Endpointsは手軽にAPIゲートウェイを構築できる上、プラットフォームへの移植性が非常に高いです。是非活用してみてください。