クラウド同上

認証機能付きAPIが簡単に作れるCloud Endpoints入門3 Google App Engine

Author
kubosuke
Lv:4 Exp:1783

module kubosuke

require cloud-ace.jp/basketball/rock/backpacker v0.0.1

第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つです。

  1. GAEへ流入する全てのトラフィックがCloud Endpointsを通過するように、GAE側でCloud Endpoints以外を遮断する
  2. Cloud EndpointsのAPI Gatewayとして、 ESP(Extensible Service Proxy)を利用する

なお、本稿では「エンドユーザ -> Cloud Endpointsの認証設定」については説明しません。
第1回と第2回の記事にて、APIキー、JWTトークンを利用した認証方式を解説しております。他のエンドユーザ認証方式については、公式ドキュメントをご確認ください。

構成

今回はCloud EnspointsのESPをCloud Runにデプロイします。
GCE、GKEや別プラットフォームでもデプロイ可能です。

fig1

手順

GAEにAPIをdeployする

適当なAPIをdeployします。

main.go
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")
}
app.yaml
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です。

img1

次にIAPの保護をenableします。

【Google Cloud Console】Security -> Identity-Aware Proxy

img2

これで、GAEがIAPによって保護された状態となりました。
試しに gcloud app browseで表示されたURLにアクセスすると、Google User認証に遷移し、403となることが確認できます。

img3

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コンテナのバックエンドを設定できます。

openapi.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: 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です。

4.png

アップロードします。

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に直接リクエストしてみます。

img5

ユーザ認証に移行し、アクセスが遮断されることが確認できました。

コラム: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 が走っています。

img6
(引用: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)

myjwt_oidc.go
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します。

  1. grant_type: urn:ietf:params:oauth:grant-type:jwt-bearer
  2. assertion: 生成したJWTトークン

img7

すると、Googleのトークンエンドポイントより、OIDCトークンが発行されます。

img8

これをAuthorization: Bearerヘッダに含めてGAEのエンドポイントにリクエストをすると、GAEにdeployしたAPIから予期したレスポンスを得ることができます。

img9

まとめると、ESP -> IAPのトラフィックの裏では、

  1. ESPのデフォルトサービスアカウントのクレデンシャル情報でJWTトークンを署名
  2. JWTトークンをhttps://www.googleapis.com/oauth2/v4/token へPOSTし、OIDCトークンを得る
  3. 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ゲートウェイを構築できる上、プラットフォームへの移植性が非常に高いです。是非活用してみてください。