Skip to content

Instantly share code, notes, and snippets.

@utamori
Created November 17, 2020 00:34
Show Gist options
  • Save utamori/22c611f923f052c8309d53ea512ce972 to your computer and use it in GitHub Desktop.
Save utamori/22c611f923f052c8309d53ea512ce972 to your computer and use it in GitHub Desktop.

サービス間認証

Authentication between services  |  API Gateway Documentationの翻訳

エンドユーザーのリクエストを認証することに加えて、API にリクエストを行うサービス(人間以外のユーザー)を認証したい場合があります。このページでは、サービスアカウントを使用して、人間またはサービスに認証を提供する方法について説明します。

概要

API にリクエストを送信するサービスを識別するには、サービス アカウントを使用します。呼び出し側のサービスは、サービス アカウントの秘密鍵を使用して安全な JSON Web Token(JWT)に署名し、署名した JWT をリクエストに含めて API に送信します。

API と呼び出し側のサービスにサービス間認証を実装するには、次の手順に従います。

  1. 呼び出し側のサービスのサービス アカウントとキーを作成します。
  2. API Gateway サービスの OpenAPI ドキュメントに認証サポートを追加します。
  3. 次の処理を行うコードを呼び出し側のサービスに追加します。
    • JWT を作成し、サービス アカウントの秘密鍵で署名する。
    • リクエストで署名済みの JWT を API に送信する。

API Gateway は、リクエストを API に転送する前に、JWT のクレームが API Config の構成と一致することを検証します。API Gateway は、サービス アカウントに付与されている Cloud Identity の権限を確認しません。

Cloud Identity - サービスアカウントの IAM 権限のこと

前提条件

すでに以下を行っていることを前提としています。

  • Google Cloud プロジェクトが作成されていること
  • API の OpenAPI ドキュメントが作成済みであること
  • API Config が作成済みであること

サービス アカウントとキーを作成する

「呼び出し元のサービスが JWT の署名に使用する秘密鍵ファイル」を持つサービスアカウントが必要です。API にリクエストを送信するサービスが複数ある場合は、1 つのサービスアカウントを作成してすべての呼び出しサービスを表すことができます。サービス間で区別する必要がある場合 (例えば、サービスの権限が異なる場合など)、呼び出し元のサービスごとにサービスアカウントとキーを作成することができます。

このセクションでは、Google Cloud Console と gcloud コマンドライン ツールを使用してサービス アカウントと秘密鍵ファイルを作成し、サービス アカウントに Service Account Token Creator ロールを割り当てる方法を説明します。API を使用してこのタスクを実行する方法については、「サービス アカウントの作成と管理」を参照してください。

サービス アカウントとキーを作成するには、次の手順に従います。

  1. Cloud Console で、[サービス アカウント キーの作成] ページに移動します。
    • [サービス アカウント キーの作成] ページに移動
  2. 使用するプロジェクトを選択します。
  3. [サービス アカウント] プルダウン メニューから [新しいサービス アカウント] を選択します。
  4. [サービス アカウント名] フィールドに名前を入力します。
  5. [役割] プルダウン メニューで、[サービス アカウント]、[サービス アカウント トークン作成者] の順に選択します。
  6. キータイプは、デフォルト タイプ JSON を使用します。
  7. [作成] をクリックします。サービス アカウントの秘密鍵を含む JSON ファイルがパソコンにダウンロードされます。

秘密鍵を保護する方法については、認証情報を管理する際のベスト プラクティスをご覧ください。

認証をサポートするように API を構成する

ゲートウェイ用の API Config を作成するとき、ゲートウェイが他のサービスと対話するために使用するサービスアカウントを指定します。ゲートウェイを呼び出すサービスに対してサービスアカウント認証を有効にするには、API コンフィグのセキュリティ要件オブジェクトとセキュリティ定義オブジェクトを変更します。以下の手順を実行すると、API ゲートウェイが、呼び出しサービスが使用する署名済み JWT のクレームを検証できるようになります。

  1. API Config でサービスアカウントを発行者(issuer)として追加します。
securityDefinitions:
  DEFINITION_NAME:
    authorizationUrl: ""
    flow: "implicit"
    type: "oauth2"
    x-google-issuer: "{SA_EMAIL_ADDRESS}"
    x-google-jwks_uri: "https://www.googleapis.com/robot/v1/metadata/x509/{SA_EMAIL_ADDRESS}"
  • DEFINITION_NAME は、このセキュリティ定義を識別する文字列に置き換えます。サービス アカウント名または呼び出し側のサービスを識別する名前に置き換えることをおすすめします。
  • SA_EMAIL_ADDRESS は、サービス アカウントのメールアドレスに置き換えます。
  • API Config で複数のセキュリティ定義を定義することができますが、それぞれの定義は異なる x-google-issuer を持つ必要があります。呼び出し元のサービスごとに別々のサービスアカウントを作成している場合は、例えば、サービスアカウントごとにセキュリティ定義を作成することができます。
securityDefinitions:
  service-1:
    authorizationUrl: ""
    flow: "implicit"
    type: "oauth2"
    x-google-issuer: "service-1@example-project-12345.iam.gserviceaccount.com"
    x-google-jwks_uri: "https://www.googleapis.com/robot/v1/metadata/x509/service-1@example-project-12345.iam.gserviceaccount.com"
  service-2:
    authorizationUrl: ""
    flow: "implicit"
    type: "oauth2"
    x-google-issuer: "service-2@example-project-12345.iam.gserviceaccount.com"
    x-google-jwks_uri: "https://www.googleapis.com/robot/v1/metadata/x509/service-2@example-project-12345.iam.gserviceaccount.com"
  1. 必要に応じて、x-google-audiencessecurityDefinitions セクションに追加します。x-google-audiences を追加しない場合、API Gateway は JWT の"aud"(audience)クレームがhttps://SERVICE_NAMEの形式であることを要求します。SERVICE_NAME は API Gateway サービスの名前で、OpenAPI ドキュメントのhostフィールドで設定したものです。

  2. API 全体に適用する場合はファイルのトップ レベル (インデントやネストされていない場所) に、特定のメソッドに適用する場合はメソッド レベルにsecurityセクションを追加します。API レベルとメソッド レベルの両方でsecurityセクションを使用する場合は、メソッド レベルの設定が API レベルの設定を上書きします。

security:
  - DEFINITION_NAME: []
  • DEFINITION_NAMEを、securityDefinitionsセクションで使用した名前に置き換えます。
  • securityDefinitionsセクションに複数の定義がある場合は、次のようにsecurityセクションに追加します。
security:
  - service-1: []
  - service-2: []
  1. 新しい API Config をデプロイします。

API Gateway がリクエストを API に転送する前に、APIGateway は以下を検証します

  • API Config の x-google-jwks_uri フィールドで指定した URI にある公開鍵を利用した JWT の署名
  • JWT の "iss"(issuer) のクレームが x-google-issuer フィールドで指定された値と一致していること
  • JWT の "aud"(audience) クレームに API Gateway サービス名が含まれているか、x-google-audiences フィールドで指定した値と一致している
  • "exp(expiration time)クレームを使った、トークンの有効期限の確認

x-google-issuerx-google-jwks_urix-google-audiences の詳細については、OpenAPI の拡張を参照してください

認証されたリクエストを API Gateway API に送信する

認証されたリクエストを行う場合、呼び出し側のサービスは、OpenAPI ドキュメントに指定されたサービス アカウントで署名された JWT を送信します。呼び出し側のサービスは、次の処理を行う必要があります。

  1. JWT を作成し、サービス アカウントの秘密鍵で署名します。
  2. リクエストで署名済みの JWT を API に送信します。

次のサンプルコードは、このプロセスを一部の言語で示しています。他の言語で認証済みリクエストを行うには、jwt.io で、サポートされているライブラリの一覧を参照してください。

  1. 呼び出し側のサービスに以下の関数を追加し、次のパラメータを渡します。
  • saKeyfile: サービス アカウントの秘密鍵ファイルへのフルパス。
  • saEmail: サービス アカウントのメールアドレス。
  • audience: x-google-audiences フィールドを API config に追加している場合、x-google-audiencesで指定したいずれかの値をaudienceに設定します。それ以外の場合は、audiencehttps://SERVICE_NAME を設定します。ここで、SERVICE_NAME は、API Gateway のサービス名です。
  • expiryLength: JWT の有効期限(秒)。

この関数は JWT を作成して秘密鍵ファイルで署名し、署名済みの JWT を返します。

// generateJWT creates a signed JSON Web Token using a Google API Service Account.
func generateJWT(saKeyfile, saEmail, audience string, expiryLength int64) (string, error) {
        now := time.Now().Unix()

        // Build the JWT payload.
        jwt := &jws.ClaimSet{
                Iat: now,
                // expires after 'expiryLength' seconds.
                Exp: now + expiryLength,
                // Iss must match 'issuer' in the security configuration in your
                // swagger spec (e.g. service account email). It can be any string.
                Iss: saEmail,
                // Aud must be either your Endpoints service name, or match the value
                // specified as the 'x-google-audience' in the OpenAPI document.
                Aud: audience,
                // Sub and Email should match the service account's email address.
                Sub:           saEmail,
                PrivateClaims: map[string]interface{}{"email": saEmail},
        }
        jwsHeader := &jws.Header{
                Algorithm: "RS256",
                Typ:       "JWT",
        }

        // Extract the RSA private key from the service account keyfile.
        sa, err := ioutil.ReadFile(saKeyfile)
        if err != nil {
                return "", fmt.Errorf("Could not read service account file: %v", err)
        }
        conf, err := google.JWTConfigFromJSON(sa)
        if err != nil {
                return "", fmt.Errorf("Could not parse service account JSON: %v", err)
        }
        block, _ := pem.Decode(conf.PrivateKey)
        parsedKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
        if err != nil {
                return "", fmt.Errorf("private key parse error: %v", err)
        }
        rsaKey, ok := parsedKey.(*rsa.PrivateKey)
        // Sign the JWT with the service account's private key.
        if !ok {
                return "", errors.New("private key failed rsa.PrivateKey type assertion")
        }
        return jws.Encode(jwsHeader, jwt, rsaKey)
}
  1. 呼び出し側のサービスに、以下の関数を追加し、署名付き JWT を Authorization: Bearer ヘッダー内で API に送信します。
// makeJWTRequest sends an authorized request to your deployed endpoint.
func makeJWTRequest(signedJWT, url string) (string, error) {
        client := &http.Client{
                Timeout: 10 * time.Second,
        }

        req, err := http.NewRequest("GET", url, nil)
        if err != nil {
                return "", fmt.Errorf("failed to create HTTP request: %v", err)
        }
        req.Header.Add("Authorization", "Bearer "+signedJWT)
        req.Header.Add("content-type", "application/json")

        response, err := client.Do(req)
        if err != nil {
                return "", fmt.Errorf("HTTP request failed: %v", err)
        }
        defer response.Body.Close()
        responseData, err := ioutil.ReadAll(response.Body)
        if err != nil {
                return "", fmt.Errorf("failed to parse HTTP response: %v", err)
        }
        return string(responseData), nil
}

JWT を使用してリクエストを送信する場合は、セキュリティ上の理由から、認証トークンを Authorization: Bearer ヘッダーに入れることをおすすめします。例:

curl --request POST \
  --header "Authorization: Bearer ${TOKEN}" \
  "${GATEWAY_URL}/echo"

GATEWAY_URLTOKEN はそれぞれ、Gateway の URL と認証トークンを格納する環境変数です。

API での認証結果の受信

通常、API Gateway は受信したすべてのヘッダーを転送します。ただし、バックエンド アドレスが OpenAPI 仕様の x-google-backendで指定されている場合は、元の Authorization ヘッダーより優先します。

API Gateway は認証結果を X-Apigateway-Api-Userinfo でバックエンド API に送信します。元の Authorization ヘッダーではなく、このヘッダーを使用することをおすすめします。このヘッダーは base64url エンコードされ、JWT payload が含まれています。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment