サービス間の認証

エンドユーザーのリクエストを認証するだけでなく、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 Console と 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 Gateway で、サービスの呼び出しで使用される署名付き JWT のクレームを検証できるようにします。

  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. API 全体に適用するには、ファイルの最上位に security セクションを追加します。インデントされない場合は、特定のメソッドに適用します。API レベルとメソッドレベルの両方で security セクションを使用する場合、メソッドレベルの設定は API レベルの設定より優先されます。

    security:
      - DEFINITION_NAME: []
    
    • securityDefinitions セクションで使用した名前は DEFINITION_NAME と置き換えます。
    • securityDefinitions セクションに定義が複数ある場合は、それらを security に追加します。例を次に示します。

      security:
        - service-1: []
        - service-2: []
      
  4. 更新した API 構成をデプロイします。

API Gateway がリクエストを API に転送する前に、次のことを確認します。

  • API 構成の x-google-jwks_uri フィールドで指定された URI にある公開鍵を使用した JWT の署名。
  • その "iss" JWT での発行者のリクエストが、x-google-issuer フィールドに入力します。
  • その "aud" JWT 内の API Gateway サービス名が含まれているか、または x-google-audiences フィールドに入力します。
  • トークンが期限切れになっていないことを確認するには、"exp"(有効期限)クレームを使用します。

x-google-issuerx-google-jwks_urix-google-audiences の詳細については、OpenAPI 拡張機能をご覧ください。

認証されたリクエストを 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 := ioutil.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. 呼び出し側のサービスに、以下の関数を追加し、署名付き JWT を Authorization: Bearer ヘッダー内で API に送信します。
    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 := ioutil.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}/echo"

ここで、GATEWAY_URLTOKEN は、デプロイされたゲートウェイの URL と認証トークンを格納する環境変数です。

API での認証結果の受信

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

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

次のステップ