サービス間の認証

ユーザーの認証だけでなく、他のサービスに API の使用を許可する場合があります。ユーザーが認証情報を送信できるように、クライアント アプリケーションでウェブログイン プロンプトを表示することがありますが、サービス間で安全な通信を行うには別の手段が必要になります。このページでは、サービス間で認証を実装する際のおすすめの方法とサンプルコードを紹介します。

概要

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

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

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

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

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

前提条件

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

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

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

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

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

Cloud Console

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

    1. Cloud Console で [サービス アカウントの作成] ページに移動します。

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

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

    3. [サービス アカウント名] フィールドに名前を入力します。

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

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

    6. [ロールを選択] フィールドをクリックします。[すべての役割] で、[サービス アカウント] > [サービス アカウント トークン作成者] を選択します。

    7. [完了] をクリックします。

      ブラウザ ウィンドウは閉じないでください。次のステップでこれを使用します。

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

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

gcloud

ローカルマシン上の Cloud SDK で以下のコマンドを実行します。このコマンドは、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 を構成する

ESP が署名済みの JWT でクレームを検証できるように、OpenAPI ドキュメントにセキュリティ要件オブジェクトセキュリティ定義オブジェクトが設定されている必要があります。

  1. OpenAPI ドキュメントの発行元として、サービス アカウントを追加します。

    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 は、サービス アカウントのメールアドレスに置き換えます。
    • OpenAPI ドキュメントでは複数のセキュリティ定義を記述できますが、定義ごとに、異なる 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 を追加しない場合、ESP を使用するには、JWT に https://SERVICE_NAME 形式の "aud"(audience)クレームが必要になります。SERVICE_NAMEは、OpenAPI ドキュメントの host フィールドで構成した Endpoints サービスの名前です。

  3. 必要に応じて、x-google-jwt-locationssecurityDefinitions セクションを追加してください。この値を使用して、カスタム JWT の場所を定義できます。デフォルトの JWT の場所は、Authorization ヘッダー(接頭辞「Bearer」)、X-Goog-Iap-Jwt-Assertion ヘッダー、または access_token クエリ パラメータです。注意:

    • x-google-jwt-locations を指定すると、Endpoints はデフォルトの場所をすべて無視します。
    • x-google-jwt-locations は ESPv2 でのみサポートされています。
  4. API 全体に適用するには、ファイルの最上位に security セクションを追加します。インデントされない場合は、特定のメソッドに適用します。API レベルとメソッドレベルの両方で security セクションを使用する場合、メソッドレベルの設定は API レベルの設定より優先されます。

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

      security:
        - service-1: []
        - service-2: []
      
  5. 更新された OpenAPI ドキュメントをデプロイします。OPENAPI_DOC は、OpenAPI ドキュメントの名前に置き換えます。

    gcloud endpoints services deploy OPENAPI_DOC

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

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

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

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

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

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

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

  1. 呼び出し側のサービスに以下の関数を追加し、次のパラメータを渡します。
    Java
    • saKeyfile: サービス アカウントの秘密鍵ファイルへのフルパス。
    • saEmail: サービス アカウントのメールアドレス。
    • audience: x-google-audiences フィールドを OpenAPI ドキュメントに追加すると、x-google-audiences で指定したいずれかの値に audience を設定します。それ以外の場合は、audiencehttps://SERVICE_NAME を設定します。ここで、SERVICE_NAME は、Endpoints のサービス名です。
    • expiryLength: JWT の有効期限(秒)。
    Python
    • sa_keyfile: サービス アカウントの秘密鍵ファイルへのフルパス。
    • sa_email: サービス アカウントのメールアドレス。
    • audience: x-google-audiences フィールドを OpenAPI ドキュメントに追加すると、x-google-audiences で指定したいずれかの値に audience を設定します。それ以外の場合は、audiencehttps://SERVICE_NAME を設定します。ここで、SERVICE_NAME は、Endpoints のサービス名です。
    • expiry_length: JWT の有効期限(秒)。
    Go
    • saKeyfile: サービス アカウントの秘密鍵ファイルへのフルパス。
    • saEmail: サービス アカウントのメールアドレス。
    • audience: x-google-audiences フィールドを OpenAPI ドキュメントに追加すると、x-google-audiences で指定したいずれかの値に audience を設定します。それ以外の場合は、audiencehttps://SERVICE_NAME を設定します。ここで、SERVICE_NAME は、Endpoints のサービス名です。
    • 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 'expiraryLength' 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: %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)
    }
    
  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: %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}" \
  "${ENDPOINTS_HOST}/echo"

ENDPOINTS_HOSTTOKEN はそれぞれ、API のホスト名と認証トークンを格納する環境変数です。

API での認証結果の受信

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

ESP は認証結果を X-Endpoint-API-UserInfo でバックエンド API に送信します。元の Authorization ヘッダーではなく、このヘッダーを使用することをおすすめします。このヘッダーは、base64url が次の JSON オブジェクトをエンコードする文字列です。

{
  "id": "from-sub",
  "issuer": "from-iss",
  "email": "from-email",
  "audiences": ["from-aud"],
  "claims": {
     original-jwt-payload
   }
}

ESPv2 を使用している場合は、ヘッダーの値の形式が異なります。新しいフォーマットの詳細については、Extensible Service Proxy V2 ベータ版への移行をご覧ください。

次のステップ