サービス間の認証

ユーザーの認証だけでなく、他のサービスに 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. Cloud Console で、[サービス アカウント キーの作成] ページに移動します。

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

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

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/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 に "aud"(audience)クレームを必要とし、その形式は https://SERVICE_NAME です。ここで、SERVICE_NAME は、Endpoints サービスの名前で、OpenAPI ドキュメントの host フィールドに入力したものです。

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

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

      security:
        - service-1: []
        - service-2: []
      
  4. 更新された 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-audiences の詳細については、OpenAPI 拡張機能をご覧ください。

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

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

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

次の例は、このプロセスを実行するサンプルコードです。

  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);
      GoogleCredential cred = GoogleCredential.fromStream(stream);
      RSAPrivateKey key = (RSAPrivateKey) cred.getServiceAccountPrivateKey();
      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)
    
        response.raise_for_status()
        return response.text
    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 のホスト名と認証トークンを格納する環境変数です。

リクエストの送信時にヘッダーを使用できない場合は、access_token というクエリ パラメータに認証トークンを入れることができます。次に例を示します。

curl --request POST \
  "${ENDPOINTS_HOST}/echo?access_token=${TOKEN}"

API で認証の詳細を受信する

ESP は、元の承認ヘッダーを含むすべてのヘッダーを API に転送します。さらに X-Endpoint-API-UserInfo ヘッダーで認証結果を API に送信しますこのヘッダーは base64 エンコードされ、次の JSON オブジェクトが含まれています。

{
  "issuer": TOKEN_ISSUER,
  "id": USER_ID,
  "email" : USER_EMAIL
}

次のステップ