サービス間の認証

エンドユーザーのリクエストを認証するだけでなく、API にリクエストを送信するサービス(人間以外のユーザー)も認証しなければならない場合があります。このページでは、サービス アカウントを使用して人間またはサービスの認証を行う方法について説明します。

概要

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

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

  1. 呼び出し側のサービスのサービス アカウントとキーを作成します。
  2. API Gateway サービスの API 構成に認証サポートを追加します。
  3. 次の処理を行うコードを呼び出し側のサービスに追加します。

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

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

前提条件

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

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

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

このセクションでは、 Google Cloud コンソールと gcloud コマンドライン ツールを使用して、サービス アカウントと秘密鍵ファイルを作成し、 サービス アカウントに サービス アカウント トークン作成者 役割を割り当てる方法を説明します。この操作を API で行う方法については、 サービス アカウントの作成と管理をご覧ください。

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

Google Cloud コンソール

サービス アカウントの作成:

  1. コンソール Google Cloud で、[**サービス アカウントの作成**] に移動します。

    [サービス アカウントの作成] に移動

  2. プロジェクトを選択します。

  3. [サービス アカウント名] フィールドに名前を入力します。コンソールでは、 Google Cloud この名前に基づいて [サービス アカウント ID] フィールドに値が入力されます。

  4. 省略可: [サービス アカウントの説明] フィールドに説明を入力します。

  5. [作成] をクリックします。

  6. [ロールを選択] フィールドをクリックします。

    [すべてのロール] で、[サービス アカウント] > [サービス アカウント トークン作成者] を選択します。

  7. [続行] をクリックします。

  8. [完了] をクリックして、サービス アカウントの作成を完了します。

    ブラウザ ウィンドウを閉じないでください。次の手順でこのページを使用します。

サービス アカウント キーを作成します。

  1. Google Cloud コンソールで、作成したサービス アカウントのメールアドレスをクリックします。
  2. [キー] をクリックします。
  3. [鍵を追加]、[新しい鍵を作成] の順にクリックします。
  4. [作成] をクリックします。JSON キーファイルがパソコンにダウンロードされます。
  5. [閉じる] をクリックします。

gcloud

ローカルマシン上の Google Cloud CLI を使用して以下のコマンドを実行します。このコマンドは、Cloud Shell 内でも実行できます。

  1. gcloud にデフォルトのアカウントを設定する。複数のアカウントがある場合は、 使用する Google Cloud プロジェクト のアカウントを選択してください。

    gcloud auth login
  2. Google Cloud プロジェクトのプロジェクト ID を表示します。

    gcloud projects list
  3. デフォルト プロジェクトを設定します。PROJECT_ID は、使用する Google Cloud プロジェクト ID に置き換えます。

    gcloud config set project PROJECT_ID
  4. サービス アカウントを作成します。SA_NAMESA_DISPLAY_NAME は、それぞれ使用する名前と表示名に置き換えます。

    gcloud iam service-accounts create SA_NAME \
      --display-name "SA_DISPLAY_NAME"
  5. 作成したサービス アカウントのメールアドレスを表示します。

    gcloud iam service-accounts list
  6. サービス アカウント トークン作成者の役割を追加します。SA_EMAIL_ADDRESS は、サービス アカウントのメールアドレスに置き換えます。

    gcloud projects add-iam-policy-binding PROJECT_ID \
      --member serviceAccount:SA_EMAIL_ADDRESS \
      --role roles/iam.serviceAccountTokenCreator
  7. 現在の作業ディレクトリにサービス アカウント キー ファイルを作成します。FILE_NAME は、キーファイルに使用する名前に置き換えます。デフォルトでは、gcloud JSON ファイルが作成されます。

    gcloud iam service-accounts keys create FILE_NAME.json \
      --iam-account SA_EMAIL_ADDRESS

詳しくは gcloud リファレンス をご覧ください。

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

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

ゲートウェイの API 構成を作成するときに、ゲートウェイが他のサービスと通信するために使用するサービス アカウントを指定します。ゲートウェイを呼び出すサービスのサービス アカウント認証を有効にするには、API 構成のセキュリティ オブジェクトを変更します。 変更内容は、使用する OpenAPI 仕様のバージョンによって異なります。

呼び出し側のサービスで使用される署名付き JWT のクレームを検証するように API Gateway を構成するには:

OpenAPI 2.0

  1. API 構成にサービス アカウントを発行者として追加します。
    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 構成では複数のセキュリティ定義を定義できますが、 各定義の 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"
  2. 必要に応じて、x-google-audiencessecurityDefinitions セクションに追加してください。 x-google-audiences を追加しない場合、API Gateway では JWT 内の "aud"(オーディエンス)クレームの形式が https://SERVICE_NAME である必要があります。ここで SERVICE_NAME は、OpenAPI ドキュメントの host フィールドで構成した API Gateway サービスの名前です。
  3. security セクションを追加します。API 全体に適用する場合はファイルのトップレベル(インデントやネストがないレベル)に、特定のメソッドに適用する場合はメソッドレベルに追加します。security セクションを API レベルとメソッドレベルの両方で指定した場合、API レベルの設定よりもメソッドレベルの設定が優先されます。
    security:
      - DEFINITION_NAME: []
    • DEFINITION_NAME は、 securityDefinitions セクションで使用した名前に置き換えます。
    • `securityDefinitions` セクションに複数の定義がある場合は、`security` セクションに追加します。次に例を示します。
      security:
        - service-1: []
        - service-2: []
  4. 更新した API 構成 をデプロイします。API Gateway がリクエストを API に転送する前に、次のことを確認します。
    • API 構成の x-google-jwks_uri フィールドで指定された URI にある公開鍵を使用した JWT の署名。
    • JWT の "iss"(発行者) クレームが x-google-issuer フィールドで指定された値と一致していること。
    • その "aud"JWT 内の API Gateway サービス名が含まれているか、または x-google-audiences フィールドに入力します。
    • トークンが期限切れになっていないことを確認するには、"exp"(有効期限)クレームを使用します。

OpenAPI 3.x

  1. API 構成にサービス アカウントを発行者として追加します。
    components:
      securitySchemes:
        SCHEME_NAME:
          type: oauth2
          flows:
           implicit:
             authorizationUrl: ""
             scopes: {}
          x-google-auth:
            issuer: SA_EMAIL_ADDRESS
            jwksUri: https://www.googleapis.com/robot/v1/metadata/x509/SA_EMAIL_ADDRESS
    security:
      - SCHEME_NAME: []
    • SCHEME_NAME は、このセキュリティ スキームを識別する文字列に置き換えます。サービス アカウント名または呼び出し側のサービスを識別する名前に置き換えることをおすすめします。
    • SA_EMAIL_ADDRESS は、サービス アカウントのメールアドレスに置き換えます。
    • API 構成では複数のセキュリティ スキームを定義できますが、 各定義の issuer は異なっている必要があります。呼び出し側のサービスごとに別々のサービス アカウントを作成した場合、サービス アカウントごとにセキュリティ定義を作成できます。次に例を示します。
      components:
        securitySchemes:
          service-1:
            type: oauth2
            flows:
             implicit:
               authorizationUrl: ""
               scopes: {}
            x-google-auth:
              issuer: "service-1@example-project-12345.iam.gserviceaccount.com"
              jwksUri: https://www.googleapis.com/robot/v1/metadata/x509/service-1@example-project-12345.iam.gserviceaccount.com
              jwtLocations:
                - header: Authorization
                  valuePrefix: "Bearer "
          service-2:
            type: oauth2
            flows:
             implicit:
               authorizationUrl: ""
               scopes: {}
            x-google-auth:
              issuer: "service-2@example-project-12345.iam.gserviceaccount.com"
              jwksUri: "https://www.googleapis.com/robot/v1/metadata/x509/service-2@example-project-12345.iam.gserviceaccount.com"
              
  2. 必要に応じて、audiencessecuritySchemes セクションに追加してください。 audiences を追加しない場合、API Gateway では JWT 内の "aud"(オーディエンス)クレームの形式が https://SERVICE_NAME である必要があります。ここで SERVICE_NAME は、OpenAPI ドキュメントの servers.url フィールドで構成した API Gateway サービスの名前です。
  3. security セクションを追加します。API 全体に適用する場合はファイルのトップレベル(インデントやネストがないレベル)に、特定のメソッドに適用する場合はメソッドレベルに追加します。security セクションを API レベルとメソッドレベルの両方で指定した場合、API レベルの設定よりもメソッドレベルの設定が優先されます。
    security:
      - SCHEME_NAME: []
    • SCHEME_NAME は、 securitySchemes セクションで使用した名前に置き換えます。
    • `securitySchemes` セクションに複数の定義がある場合は、`security` セクションに追加します。次に例を示します。
      security:
        - service-1: []
        - service-2: []
  4. 更新した API 構成 をデプロイします。API Gateway がリクエストを API に転送する前に、次のことを確認します。
    • API 構成の jwksUri フィールドで指定された URI にある公開鍵を使用した JWT の署名。
    • JWT の "iss"(発行者) クレームが issuer フィールドで指定された値と一致していること。
    • その "aud"JWT 内の API Gateway サービス名が含まれているか、または audiences フィールドに入力します。
    • トークンが期限切れになっていないことを確認するには、"exp"(有効期限)クレームを使用します。

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

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

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

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

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

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

    Java
    /**
     * Generates a signed JSON Web Token using a Google API Service Account
     * utilizes com.auth0.jwt.
     */
    public static String generateJwt(final String saKeyfile, final String saEmail,
        final String audience, final int expiryLength)
        throws FileNotFoundException, IOException {
    
      Date now = new Date();
      Date expTime = new Date(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(expiryLength));
    
      // Build the JWT payload
      JWTCreator.Builder token = JWT.create()
          .withIssuedAt(now)
          // Expires after 'expiryLength' seconds
          .withExpiresAt(expTime)
          // Must match 'issuer' in the security configuration in your
          // swagger spec (e.g. service account email)
          .withIssuer(saEmail)
          // Must be either your Endpoints service name, or match the value
          // specified as the 'x-google-audience' in the OpenAPI document
          .withAudience(audience)
          // Subject and email should match the service account's email
          .withSubject(saEmail)
          .withClaim("email", saEmail);
    
      // Sign the JWT with a service account
      FileInputStream stream = new FileInputStream(saKeyfile);
      ServiceAccountCredentials cred = ServiceAccountCredentials.fromStream(stream);
      RSAPrivateKey key = (RSAPrivateKey) cred.getPrivateKey();
      Algorithm algorithm = Algorithm.RSA256(null, key);
      return token.sign(algorithm);
    }
    Python
    def generate_jwt(
        sa_keyfile,
        sa_email="account@project-id.iam.gserviceaccount.com",
        audience="your-service-name",
        expiry_length=3600,
    ):
        """Generates a signed JSON Web Token using a Google API Service Account."""
    
        now = int(time.time())
    
        # build payload
        payload = {
            "iat": now,
            # expires after 'expiry_length' seconds.
            "exp": now + expiry_length,
            # iss must match 'issuer' in the security configuration in your
            # swagger spec (e.g. service account email). It can be any string.
            "iss": sa_email,
            # 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": sa_email,
            "email": sa_email,
        }
    
        # sign with keyfile
        signer = google.auth.crypt.RSASigner.from_service_account_file(sa_keyfile)
        jwt = google.auth.jwt.encode(signer, payload)
    
        return jwt
    
    
    Go
    
    // 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 := os.ReadFile(saKeyfile)
    	if err != nil {
    		return "", fmt.Errorf("could not read service account file: %w", err)
    	}
    	conf, err := google.JWTConfigFromJSON(sa)
    	if err != nil {
    		return "", fmt.Errorf("could not parse service account JSON: %w", err)
    	}
    	block, _ := pem.Decode(conf.PrivateKey)
    	parsedKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
    	if err != nil {
    		return "", fmt.Errorf("private key parse error: %w", 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)
    }
    
  2. 呼び出し側のサービスに以下の関数を追加し、API に対するリクエストの Authorization: Bearer ヘッダーで署名済みの JWT を送信します:
    Java
    /**
     * Makes an authorized request to the endpoint.
     */
    public static String makeJwtRequest(final String signedJwt, final URL url)
        throws IOException, ProtocolException {
    
      HttpURLConnection con = (HttpURLConnection) url.openConnection();
      con.setRequestMethod("GET");
      con.setRequestProperty("Content-Type", "application/json");
      con.setRequestProperty("Authorization", "Bearer " + signedJwt);
    
      InputStreamReader reader = new InputStreamReader(con.getInputStream());
      BufferedReader buffReader = new BufferedReader(reader);
    
      String line;
      StringBuilder result = new StringBuilder();
      while ((line = buffReader.readLine()) != null) {
        result.append(line);
      }
      buffReader.close();
      return result.toString();
    }
    Python
    def make_jwt_request(signed_jwt, url="https://your-endpoint.com"):
        """Makes an authorized request to the endpoint"""
        headers = {
            "Authorization": "Bearer {}".format(signed_jwt.decode("utf-8")),
            "content-type": "application/json",
        }
        response = requests.get(url, headers=headers)
        print(response.status_code, response.content)
        response.raise_for_status()
    
    
    Go
    
    // 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: %w", 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: %w", err)
    	}
    	defer response.Body.Close()
    	responseData, err := io.ReadAll(response.Body)
    	if err != nil {
    		return "", fmt.Errorf("failed to parse HTTP response: %w", err)
    	}
    	return string(responseData), nil
    }
    

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

curl --request POST \
  --header "Authorization: Bearer TOKEN" \
  "GATEWAY_URL/hello"

ここで、GATEWAY_URLTOKEN は、それぞれデプロイされたゲートウェイ URL と認証トークンに置き換える必要があります。

API で認証済みの結果を受け取る

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

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

次のステップ